Configuration Overrides
Configuration override systems solve the same fundamental problem: how to let users customize behavior at multiple scopes (global, project, session) with predictable precedence. Every AI coding agent has reinvented this wheel differently. The challenge is not just “read a config file” — it’s merging nested structures across layers, interpolating environment variables, handling untrusted project configs, and providing escape hatches for enterprise administrators who need to constrain what users can override.
1. Aider Implementation
Section titled “1. Aider Implementation”Reference: references/aider/ at commit b9050e1d
Aider leans entirely on Python’s configargparse library, which unifies config files, environment variables, and CLI arguments into a single argument parser.
Config File Search
Section titled “Config File Search”The entry point is aider/main.py:main() (line 451). Config file discovery follows this order:
# aider/main.py:464-477conf_fname = Path(".aider.conf.yml")
default_config_files = []default_config_files += [conf_fname.resolve()] # CWDif git_root: git_conf = Path(git_root) / conf_fname # git root if git_conf not in default_config_files: default_config_files.append(git_conf)default_config_files.append(Path.home() / conf_fname) # homedirThe search order is CWD → git root → home directory. After initial parsing, the list is reversed (line 494) so the parser re-reads with homedir as the lowest-priority default and CWD as the highest.
Parser Setup
Section titled “Parser Setup”# aider/args.py:35-42def get_parser(default_config_files, git_root): parser = configargparse.ArgumentParser( description="aider is AI pair programming in your terminal", add_config_file_help=True, default_config_files=default_config_files, config_file_parser_class=configargparse.YAMLConfigFileParser, auto_env_var_prefix="AIDER_", )The auto_env_var_prefix="AIDER_" setting means every CLI flag automatically maps to an environment variable. For example, --model becomes AIDER_MODEL, --dark-mode becomes AIDER_DARK_MODE. This is handled entirely by configargparse — Aider doesn’t write any env-var-to-flag mapping code.
Precedence
Section titled “Precedence”From lowest to highest priority:
- Defaults — Hardcoded in
args.pyviaparser.add_argument(default=...) - Home directory
.aider.conf.yml— User-wide defaults - Git root
.aider.conf.yml— Project-level settings - CWD
.aider.conf.yml— Directory-specific overrides - Environment variables —
AIDER_*prefix, auto-mapped .envfiles — Loaded viapython-dotenvwithoverride=Truefrom home → git root → CWD → custom path- CLI flags — Highest priority, always wins
Special Override Mechanisms
Section titled “Special Override Mechanisms”--api-key provider=key— Shortcut that sets{PROVIDER}_API_KEYenv var (line 601-610)--set-env NAME=value— Arbitrary env var injection (line 590-598)- Model settings files — Separate
.aider.model.settings.ymlchain with its owngenerate_search_path_list()(home → git root → CWD → custom path), loaded viamodels.register_models() - Model metadata —
.aider.model.metadata.jsonwith similar search path, plus a bundled resource file
Key Design Decision
Section titled “Key Design Decision”Everything flows through argparse. There is no separate “config object” or “config layer stack.” The parser itself IS the merge engine. This makes the system simple but means there’s no way to inspect which layer a value came from at runtime.
2. Codex Implementation
Section titled “2. Codex Implementation”Reference: references/codex/codex-rs/ at commit 4ab44e2c5
Codex has the most sophisticated config system of the three: a formal layer stack with precedence ordering, TOML-based configs, trust-gated project layers, enterprise MDM support, and runtime CLI overrides.
Layer Architecture
Section titled “Layer Architecture”The core abstraction is ConfigLayerStack in codex-rs/config/src/state.rs. It holds a Vec<ConfigLayerEntry> ordered from lowest to highest precedence:
// codex-rs/app-server-protocol/src/protocol/v2.rs:290-303impl ConfigLayerSource { pub fn precedence(&self) -> i16 { match self { ConfigLayerSource::Mdm { .. } => 0, ConfigLayerSource::System { .. } => 10, ConfigLayerSource::User { .. } => 20, ConfigLayerSource::Project { .. } => 25, ConfigLayerSource::SessionFlags => 30, ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => 40, ConfigLayerSource::LegacyManagedConfigTomlFromMdm => 50, } }}Each layer is a ConfigLayerEntry containing a toml::Value tree, a version fingerprint, and an optional disabled_reason (for untrusted project configs).
Layer Loading
Section titled “Layer Loading”The main loader is load_config_layers_state() in codex-rs/core/src/config_loader/mod.rs (line 102). It constructs the stack in this order:
- System —
/etc/codex/config.toml(Unix) or%ProgramData%\OpenAI\Codex\config.toml(Windows) - User —
$CODEX_HOME/config.toml(typically~/.codex/config.toml) - Project —
.codex/config.tomlin each ancestor directory from project root to CWD, subject to trust checks - SessionFlags — CLI
-c key=valueoverrides - Legacy managed —
managed_config.tomlfrom file and/or macOS MDM (being phased out in favor ofrequirements.toml)
CLI Override Mechanism
Section titled “CLI Override Mechanism”The -c key=value flag is defined in codex-rs/utils/cli/src/config_override.rs:
// CliConfigOverrides struct#[arg(short = 'c', long = "config", value_name = "key=value", action = ArgAction::Append, global = true)]pub raw_overrides: Vec<String>,The value portion is parsed as TOML first, falling back to a raw string. This means -c model="gpt-5.2-codex" produces a TOML string, while -c 'sandbox_permissions=["disk-full-read-access"]' produces a TOML array. Dotted paths like -c shell_environment_policy.inherit=all drill into nested tables.
The override tree is built by build_cli_overrides_layer() in codex-rs/config/src/overrides.rs, which walks each dotted path and calls apply_toml_override() to insert values into a TOML table hierarchy.
Merge Strategy
Section titled “Merge Strategy”pub fn merge_toml_values(base: &mut TomlValue, overlay: &TomlValue) { if let TomlValue::Table(overlay_table) = overlay && let TomlValue::Table(base_table) = base { for (key, value) in overlay_table { if let Some(existing) = base_table.get_mut(key) { merge_toml_values(existing, value); } else { base_table.insert(key.clone(), value.clone()); } } } else { *base = overlay.clone(); }}Tables merge recursively; non-table values replace entirely. Arrays are replaced, not concatenated. The effective config is computed by ConfigLayerStack::effective_config(), which merges all enabled layers from lowest to highest precedence.
Trust System
Section titled “Trust System”Project configs from .codex/config.toml are loaded but potentially disabled. The trust decision is per-directory, falling back to project root, then repo root. The user must explicitly add directories to projects_trust in their user config. Untrusted layers get a disabled_reason and are skipped during merge.
Profiles
Section titled “Profiles”The Config struct (v2.rs:408) includes profile: Option<String> and profiles: HashMap<String, ProfileV2>. A profile bundles model, approval policy, reasoning effort, and other model-specific settings that can be switched as a unit.
Origin Tracking
Section titled “Origin Tracking”ConfigLayerStack::origins() returns a HashMap<String, ConfigLayerMetadata> that maps each config key path to the layer that set it. This enables the TUI to show users where each setting came from.
3. OpenCode Implementation
Section titled “3. OpenCode Implementation”Reference: references/opencode/ at commit 7ed449974
OpenCode uses a multi-tier JSONC loading system with Zod schema validation, environment variable interpolation, and a custom merge() wrapper built on mergeDeep() from remeda.
Loading Hierarchy
Section titled “Loading Hierarchy”The full chain is in packages/opencode/src/config/config.ts, in the Config.state() function (line ~56):
| Priority | Source | Location |
|---|---|---|
| 1 (lowest) | Remote | .well-known/opencode from auth URL |
| 2 | Global | ~/.config/opencode/{config,opencode}.json{c} |
| 3 | Custom | $OPENCODE_CONFIG env var path |
| 4 | Project | opencode.json{c} found via findUp() |
| 5 | .opencode dirs | .opencode/opencode.json{c} in project/global dirs |
| 6 | Inline | $OPENCODE_CONFIG_CONTENT env var (raw JSON) |
| 7 (highest) | Managed | /etc/opencode/ (Linux), /Library/Application Support/opencode/ (macOS), %ProgramData%\opencode\ (Windows) |
The managed config directory (getManagedConfigDir(), line 43) is intended for enterprise deployments and has the highest precedence, overriding everything.
File Format
Section titled “File Format”Config files are JSONC (JSON with Comments), parsed via the jsonc-parser library with allowTrailingComma: true. This is a significant UX improvement over strict JSON — users can comment out settings and use trailing commas.
Variable Interpolation
Section titled “Variable Interpolation”The load() function (line 1244) performs two kinds of interpolation before parsing JSONC:
- Environment variables:
{env:VAR_NAME}→process.env[VAR_NAME] - File references:
{file:path/to/file}→ contents of the referenced file, with~expansion and relative-to-config-dir resolution
Commented-out {file:...} references are skipped (line 1257).
Merge Behavior
Section titled “Merge Behavior”OpenCode uses a custom merge(target, source) helper layered on mergeDeep(). The helper explicitly concatenates and de-duplicates plugin and instructions arrays (Array.from(new Set([...]))), while other keys follow normal deep-merge behavior.
// config.ts:1203-1209 (global config loading)let result: Info = pipe( {}, mergeDeep(await loadFile(path.join(Global.Path.config, "config.json"))), mergeDeep(await loadFile(path.join(Global.Path.config, "opencode.json"))), mergeDeep(await loadFile(path.join(Global.Path.config, "opencode.jsonc"))),)Schema Validation
Section titled “Schema Validation”Every loaded config file is validated against the Config.Info Zod schema (line 1311: Info.safeParse(data)). Invalid configs throw InvalidError with detailed issue information. The schema is strict(), meaning unknown keys cause validation failures.
Environment Flags
Section titled “Environment Flags”OpenCode uses a Flag module (flag/flag.ts) to access environment variables:
OPENCODE_CONFIG— Custom config file pathOPENCODE_CONFIG_CONTENT— Inline config as JSON stringOPENCODE_CONFIG_DIR— Custom config directory (dynamically read)OPENCODE_DISABLE_PROJECT_CONFIG— Skip project-level configs entirely
Config Update API
Section titled “Config Update API”OpenCode exposes Config.update() and Config.updateGlobal() for programmatic config modification. Updates to JSONC files use patchJsonc() which preserves comments, while JSON files do a full serialize/deserialize round-trip. After writes, Instance.dispose() is called to force re-loading.
4. Pitfalls & Hard Lessons
Section titled “4. Pitfalls & Hard Lessons”Array Merge Semantics Are a Landmine
Section titled “Array Merge Semantics Are a Landmine”Codex replaces arrays. OpenCode mostly deep-merges objects, but explicitly concatenates some arrays (plugin, instructions). Both choices have tradeoffs. Replacement makes extension awkward; concatenation can make subtraction awkward without an explicit “remove” mechanism. Aider sidesteps most of this by not having a deeply nested config object model.
Trust is Hard to Get Right
Section titled “Trust is Hard to Get Right”Codex’s trust system loads untrusted configs but marks them disabled. This is a security-conscious choice, but it means users get confusing “config loaded but not applied” states. The trust check is per-directory, so mono-repos with multiple .codex/ directories can have partially trusted configs.
Environment Variable Explosion
Section titled “Environment Variable Explosion”Aider’s auto_env_var_prefix creates hundreds of implicit env vars. Any typo in AIDER_MODLE (vs AIDER_MODEL) silently falls back to the default. There’s no validation that an env var name actually maps to a real flag.
JSONC vs TOML vs YAML
Section titled “JSONC vs TOML vs YAML”Each tool chose a different config format. TOML (Codex) is strict about types but verbose for nested structures. YAML (Aider) is concise but whitespace-sensitive. JSONC (OpenCode) allows comments but is still fundamentally JSON. None of these is clearly superior.
Origin Tracking is Underrated
Section titled “Origin Tracking is Underrated”Codex’s origins() API is unique — it tells you which layer set each value. Aider and OpenCode have no equivalent, making it hard to debug “why is this setting active?” problems. This should be a first-class feature.
Enterprise Config Must Be Highest Priority
Section titled “Enterprise Config Must Be Highest Priority”Both Codex and OpenCode learned this: managed/enterprise configs must override everything, including CLI flags. Codex’s LegacyManagedConfigTomlFromMdm (precedence 50) sits above SessionFlags (30). OpenCode’s managed config dir loads last and wins.
5. OpenOxide Blueprint
Section titled “5. OpenOxide Blueprint”Config Format: TOML
Section titled “Config Format: TOML”Follow Codex. TOML is the natural choice for a Rust project — serde + toml crate gives zero-cost deserialization into typed structs. The toml crate handles all parsing, and TOML’s explicit type system prevents the “is this a string or a number?” ambiguity common in YAML.
Layer Stack Architecture
Section titled “Layer Stack Architecture”Adopt Codex’s ConfigLayerStack pattern. Each layer is a (source, toml::Value) pair with a numeric precedence:
pub enum ConfigSource { System, // /etc/openoxide/config.toml User, // ~/.openoxide/config.toml Project, // .openoxide/config.toml (per-directory, trust-gated) Session, // -c key=value CLI overrides}Merge Strategy
Section titled “Merge Strategy”Use recursive table merge with explicit array strategy:
pub enum ArrayMerge { Replace, // Default: project array replaces user array Append, // Opt-in: project array appends to user array}Array keys that should concatenate (like instructions, plugins) can be annotated in the schema. This avoids both Codex’s “can’t extend” and OpenCode’s “can’t remove” problems.
CLI Override Parser
Section titled “CLI Override Parser”Port Codex’s -c key=value approach:
#[derive(clap::Parser)]pub struct ConfigOverrides { #[arg(short = 'c', long = "config", value_name = "key=value", action = ArgAction::Append, global = true)] pub overrides: Vec<String>,}Parse values as TOML, fall back to raw string. Use dotted paths for nested access. This is the highest-precedence layer.
Environment Variables
Section titled “Environment Variables”Support OPENOXIDE_* prefix for common settings, but map explicitly rather than auto-generating. Only a curated set of env vars should be recognized:
OPENOXIDE_MODEL— Default modelOPENOXIDE_HOME— Config directory overrideOPENOXIDE_LOG— Log levelOPENOXIDE_CONFIG— Custom config file path
This avoids Aider’s env-var explosion while keeping the most useful overrides.
Origin Tracking
Section titled “Origin Tracking”Implement from day one. Every resolved config value should carry metadata about which layer defined it:
pub struct ResolvedConfig { pub effective: Config, pub origins: HashMap<String, ConfigOrigin>,}
pub struct ConfigOrigin { pub source: ConfigSource, pub file: Option<PathBuf>,}This enables the TUI to show “model: o4-mini (from ~/.openoxide/config.toml)” and helps debug precedence conflicts.
Recommended Crates
Section titled “Recommended Crates”| Crate | Purpose |
|---|---|
toml | TOML parsing and serialization |
clap | CLI argument parsing with -c override support |
serde + serde_json | Deserialization and layer merging |
dirs | Platform-specific config directory paths |
dunce | Path canonicalization without UNC on Windows |
Schema Validation
Section titled “Schema Validation”Validate config files at load time using serde’s #[serde(deny_unknown_fields)] on the config struct. Unknown keys should produce clear error messages pointing to the offending file and line, similar to Codex’s diagnostics.rs module.