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:
- Invokes
enhance::enhance()to merge static defaults with user-providedconfig.yamland relevantverge.yamlfields - Sanitizes tunnel configurations
- 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:
- Static defaults embedded in the binary provide baseline values
- User-provided
config.yamloverrides defaults for core-specific items - User-provided
verge.yamlsupplies UI-level overrides merged into the runtime before core handoff - 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 byCoreManager::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), andprofiles.yaml(drafts) operate through draft patterns insrc-tauri/src/config/config.rs - Runtime generation:
Config::generate()merges defaults, core config, and UI preferences intoruntime.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()andsave_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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →