Understanding Configuration File Precedence and Runtime Patching in Clash Verge Rev

Clash Verge Rev employs a three-file draft architecture where config.yaml defines core proxy rules, verge.yaml stores UI preferences, and runtime.yaml is dynamically generated by merging both, ensuring the Clash core always receives the latest effective configuration without directly reading UI-level settings.

Clash Verge Rev manages network proxy configurations through a sophisticated layering system that separates core engine settings from interface preferences. This analysis of the clash-verge-rev/clash-verge-rev repository explores how configuration file precedence works and how runtime patching propagates UI changes to the underlying Clash core without file corruption or conflicts.

The Three-Layer Configuration Architecture

The application persists settings across three distinct YAML files, each managed by the Config singleton in src-tauri/src/config/config.rs.

Core Configuration (config.yaml)

The clash core configuration contains proxy rules, DNS settings, and traffic routing logic. Accessed via Config::clash(), this file stores the Draft<IClashTemp> struct, which holds both the last persisted content and a mutable in-memory copy for editing.

UI Preferences (verge.yaml)

The verge configuration manages interface-level settings including hotkeys, language selection, system-proxy toggles, and auto-light-weight mode. Retrieved through Config::verge(), this operates as a Draft<IVerge> that allows mutation without immediate disk writes.

Profile Drafts (profiles.yaml)

The profiles list maintains multiple configuration drafts that can be applied to the core. Accessed via Config::profiles(), this enables rapid switching between different proxy rule sets without overwriting the base configuration.

The Draft Pattern

Each configuration type uses a draft structure that decouples in-memory editing from file persistence. When Config::global() initializes, it lazily loads draft objects containing the last saved state plus a working copy. This pattern allows the UI to modify settings extensively before atomically committing changes to disk.

Runtime Configuration Generation and Precedence

The Clash core never reads verge.yaml directly. Instead, the application generates a runtime snapshot (IRuntime) that merges multiple sources into a single authoritative configuration.

How the Runtime is Built

The merging process occurs in Config::generate() at lines 90-104 of src-tauri/src/config/config.rs:

pub async fn generate() -> Result<()> {
    let (mut config, exists_keys, logs) = enhance::enhance().await;   // combine core+verge data
    sanitize_tunnels_proxy(&mut config);                           // sanity checks
    Self::runtime().await.edit_draft(|d| {
        *d = IRuntime { config: Some(config), exists_keys, chain_logs: logs }
    });
    Ok(())
}

This function:

  1. Invokes enhance::enhance() to merge static defaults with user-provided config.yaml and relevant verge.yaml fields
  2. Sanitizes tunnel configurations
  3. Writes the merged result into the runtime draft

The Config::generate_file(ConfigType::Run) method (lines 70-88) then persists this merged state to runtime.yaml, which is the only file the Clash core reads.

Precedence Order

Configuration values resolve through this hierarchy:

  1. Static defaults embedded in the binary provide baseline values
  2. User-provided config.yaml overrides defaults for core-specific items
  3. User-provided verge.yaml supplies UI-level overrides merged into the runtime before core handoff
  4. Runtime patches (patch_verge_config) replace in-memory drafts and trigger regeneration

Because Config::generate() runs after every Verge-side modification, the core always sees the latest effective configuration with correct precedence applied.

Runtime Patching: From UI to Core

When the frontend modifies a setting, the change propagates through a structured command chain that determines whether the core requires restarting, reloading, or simple UI refresh.

The Command Chain

The propagation begins in the TypeScript layer at src/services/cmds.ts:

export async function patchVergeConfig(payload: IVergeConfig) {
  return invoke<void>("patch_verge_config", { payload });
}

This invokes the Tauri command defined in src-tauri/src/cmd/verge.rs:

#[tauri::command]
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
    feat::patch_verge(&payload, false).await.stringify_err()
}

The core implementation resides in src-tauri/src/feat/config.rs:

pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> {
    // 1. Apply the patch to the in-memory draft
    Config::verge().await.edit_draft(|d| d.patch_config(patch));

    // 2. Determine which areas must be refreshed
    let update_flags = determine_update_flags(patch);
    process_terminated_flags(update_flags, patch).await?;

    // 3. Commit the draft (makes values visible to the rest of the app)
    Config::verge().await.apply();

    // 4. Persist to verge.yaml unless caller requested otherwise
    if !not_save_file {
        let verge_data = Config::verge().await.data_arc();
        verge_data.save_file().await?;
    }
    Ok(())
}

Flag-Based Processing

The determine_update_flags function analyzes the patch and returns a bitmask (UpdateFlags). The process_terminated_flags function (lines 4-64) reacts to these flags:

  • RESTART_CORE: Triggers Config::generate() followed by CoreManager::restart_core()
  • CLASH_CONFIG: Calls CoreManager::update_config() for hot-reloading
  • VERGE_CONFIG: Refreshes the UI via Handle::refresh_verge()
  • SYS_PROXY: Updates system proxy settings through Sysopt::update_sysproxy()
  • LIGHT_WEIGHT: Toggles auto-light-weight mode via lightweight::enable_auto_light_weight_mode()

A single UI mutation may trigger one or multiple actions depending on which fields changed, ensuring efficient resource usage without unnecessary core restarts.

Practical Implementation Examples

Patching UI Settings

To disable auto-light-weight mode from the frontend:

import { patchVergeConfig } from "@/services/cmds";

async function disableAutoLightWeight() {
  await patchVergeConfig({
    enable_auto_light_weight_mode: false,
  });
}

The patch_verge function detects the LIGHT_WEIGHT flag, invokes the appropriate lightweight mode handler, updates the UI, and writes the new verge.yaml to disk.

Updating Core DNS Configuration

To modify core-specific DNS settings:

import { invoke } from "@tauri-apps/api/tauri";

await invoke<void>("patch_clash_config", {
  payload: {
    dns: {
      enable: true,
      nameserver: ["8.8.8.8", "1.1.1.1"]
    }
  }
});

This triggers process_terminated_flags with the CLASH_CONFIG flag, causing CoreManager::update_config() to hot-reload the core without full restart.

Manual Runtime Regeneration

To force a complete configuration rebuild and core restart:

import { invoke } from "@tauri-apps/api/tauri";

await invoke<void>("restart_core");

This command invokes Config::generate(), rebuilds the merged runtime, writes runtime.yaml, and restarts the Clash core with the fresh configuration.

Summary

  • Three-file architecture: config.yaml (core), verge.yaml (UI), and profiles.yaml (drafts) operate through draft patterns in src-tauri/src/config/config.rs
  • Runtime generation: Config::generate() merges defaults, core config, and UI preferences into runtime.yaml, the only file the Clash core reads
  • Precedence hierarchy: Static defaults < config.yaml < verge.yaml < runtime patches
  • Patch propagation: Frontend calls flow through patch_verge_config → feat::patch_verge → flag analysis → selective core reload/UI refresh
  • Atomic persistence: Draft pattern allows batching modifications before atomic commit via apply() and save_file()

Frequently Asked Questions

How does Clash Verge Rev prevent the core from reading incomplete configuration changes?

The application uses a draft indirection layer where all edits occur on in-memory copies via edit_draft() before explicit commitment. The core only reads runtime.yaml, which is generated atomically by Config::generate() after all merges complete. This ensures the core never sees partial writes or inconsistent states during active editing.

What happens when I toggle a setting that requires a core restart versus one that does not?

The determine_update_flags function in src-tauri/src/feat/config.rs analyzes the specific fields modified. Changes triggering RESTART_CORE (like proxy mode switches) call CoreManager::restart_core(), while UI-only changes (like theme selection) set VERGE_CONFIG flags that merely refresh the interface via Handle::refresh_verge() without touching the running core.

Can I modify verge.yaml manually without breaking the application?

Manual edits to verge.yaml are safe because the file is only read at startup or when explicitly reloaded. However, runtime changes made through the UI take precedence until the next application restart. To ensure consistency, modifications should align with the IVerge struct schema defined in the source code, as invalid fields will be ignored during the patch_config operation.

Why does the runtime configuration use a separate file instead of modifying config.yaml directly?

Separating runtime.yaml from config.yaml preserves the user's original core configuration while allowing the UI to inject overrides (like auto-light-weight settings) without permanently altering the user's proxy rules. This separation enables the profiles feature, where multiple configuration variants can generate different runtime files without destroying the base config.yaml.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →