How configuration is resolved at runtime from files, env, and defaults. This is the maintainer-facing walkthrough; the complete field/env inventory is CONFIG_FIELDS.md and the user-facing guide is ../configuration.md.
Runtime root priority (getCodexMultiAuthDir in lib/runtime-paths.ts):
CODEX_MULTI_AUTH_DIRwhen set — wins unconditionally.- If
CODEX_HOMEis an explicit non-default path: only$CODEX_HOME/multi-auth(no cross-root scan). - Otherwise probe ordered candidates —
$CODEX_HOME/multi-auth,~/DevTools/config/codex/multi-auth,~/.codex/multi-auth, and the legacy dir — preferring any that already hold account storage (openai-codex-accounts.json/codex-accounts.jsonor rotated siblings), then any with weaker storage signals. - Fall back to
~/.codex/multi-auth(canonical default).
Canonical target is ~/.codex/multi-auth when no override is set. Everything the project owns — settings.json, account pools, projects/, cache/, logs/, app-bind/, first-run-setup.json, runtime-observability and app-helper status files — resolves under this root.
<multi-auth root>/settings.json is read for two independent sections:
dashboardDisplaySettingspluginConfig
A settings.json.bak sibling is kept for recovery: reads fall back to .bak when the primary is unreadable (transient errors are rethrown). Writes force version: 1, snapshot a backup, then temp-write + rename under an in-process queue and a wx lockfile, with EBUSY/EPERM retries and (async path) an mtime compare-and-swap re-read/re-merge loop. Unknown top-level keys survive a save.
If legacy config exists, compatibility load and migration path still apply (next section).
For runtime values stored in pluginConfig, loadPluginConfig (lib/config.ts) selects a source in this order:
- Fallback file from
CODEX_MULTI_AUTH_CONFIG_PATHwhen set and the file exists (also the preferred save target when set; read with boundedEBUSY/EPERM/EAGAINretry and a UTF-8 BOM strip) - Unified settings
pluginConfigfromsettings.json(if present and valid) - Legacy compatibility ladder when the unified record is missing/invalid — first hit wins, each emits a one-time migrate warning:
a. env path (set + exists — rechecked here so a set-but-uncreated path cannot mask a real file on disk)
b.
<multi-auth root>/config.jsonc.$CODEX_HOME/codex-multi-auth-config.json(customCODEX_HOMEonly) d.~/.codex/codex-multi-auth-config.jsone.$CODEX_HOME/openai-codex-auth-config.json(customCODEX_HOMEonly) f.~/.codex/openai-codex-auth-config.json - Hardcoded default in
DEFAULT_PLUGIN_CONFIG
The chosen record is sanitized per-field (each key is safeParsed against its zod schema; invalid fields are dropped with a one-time warning; unknown keys are dropped on the load path but preserved on the save path), then shallow-merged over DEFAULT_PLUGIN_CONFIG. A thrown error anywhere in load warns once and yields full defaults rather than a partial record.
After source selection, environment variables apply per-setting overrides.
Each get* accessor resolves env → config value → hardcoded default → min/max clamp, so env always wins over the persisted file and the file always wins over the default. Boolean envs accept 1/0/true/false/yes/no; unparseable values warn once and are ignored.
A CODEX_MULTI_AUTH_CONFIG_PATH that is set but not yet created is ignored for load; the first save still creates/writes that path when the env var remains set.
Save path (savePluginConfig): when the env var is set, the patch is merged into that file under the same queue + wx lockfile with an mtime CAS retry that re-reads and re-merges on ESTALE and preserves unknown keys; otherwise the same lock wraps a merge into the pluginConfig section of settings.json. An unreadable save target aborts with StorageError (UNREADABLE) instead of clobbering the file.
For dashboard display values:
- Persisted
dashboardDisplaySettings - Normalization + fallback defaults
- Resolve root directory (§1).
- Use global accounts file by default.
- If project-scoped mode is active (
perProjectAccounts), use the project-namespaced path under root, keyed byresolveProjectStorageIdentityRootso linked worktrees share one pool. - Attempt legacy project-file migration when applicable.
- Standalone manager receives
codex-multi-auth ...and normalizes bare subcommands toauth ...before dispatch. - Optional wrapper receives
codex-multi-auth-codex ..., normalizes compatibility aliases, and runs auth-manager commands locally. - If a wrapper command is not in auth-manager scope, discover and forward to the official Codex CLI binary (
CODEX_MULTI_AUTH_REAL_CODEX_BIN→ npm resolve → prefix roots →npm root -g→ PATH). - For forwarded request-bearing commands, check whether runtime rotation is enabled.
- Resolve
CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY; if unset, readpluginConfig.codexRuntimeRotationProxy, which defaults to enabled. - If disabled or the forwarded command is help/non-requesting, forward directly to official Codex. This includes a help flag on a root launch that also carries a prompt (
codex "prompt" --help): it prints help and exits clean, and the interactive branch detaches its helper on a clean exit, so a helper started just to print help would idle until its detached timeout. The scan stops at--, so help-looking text inside a forced prompt still routes normally (#673). - If enabled, start a loopback Responses proxy with a per-process client token.
- Select a transport from the forwarded argv:
- No forwarded root subcommand (interactive TUI) — bare
codex [OPTIONS], andcodex [OPTIONS] [PROMPT]carrying the optional initial prompt (including one forced by--), whose provider overrides are injected ahead of that prompt rather than appended (#673). Keep the canonicalCODEX_HOMEand passcodex-multi-auth-runtime-proxyas ephemeral-c model_providers.*overrides. Nothing is copied, no provider or transport config is written intoconfig.toml, and the helper detaches on exit. The transport-independentcli_auth_credentials_storereconcile below still applies. resume/fork— the same canonical-home transport as the interactive TUI. These resume an existing thread, and the shadow home omits the runtime SQLite state, so the shadow transport could not see the requested thread (#647).codex app— run the app runtime helper against a shadowCODEX_HOME.- Any other request-bearing command — create a temporary shadow
CODEX_HOMEand rewrite itsconfig.tomlto usecodex-multi-auth-runtime-proxy.
- No forwarded root subcommand (interactive TUI) — bare
- Forward official Codex with the selected home.
- Proxy request handling selects/refreshes managed accounts and rotates on rate limit, auth, network, or server failure before streaming starts.
- On process exit, shadow-home transports sync refreshed official Codex state files back and remove the shadow home. The canonical-home transport wrote state in place, so there is nothing to sync.
Independently of the transport, the wrapper reconciles the top-level cli_auth_credentials_store = "file" assignment in the real ~/.codex/config.toml at startup (idempotent; see lib/codex-cli/writer.ts). That is the only key this project persists into the official config.
- Transform request for Codex backend compatibility.
- Resolve account candidate set (health, cooldown, quota, affinity).
- Execute request with timeout/retry policy.
- Apply failover/rotation/cooldown decisions.
- Persist account/cache/session updates.
- Detect unsupported model or entitlement failures.
- Record in entitlement cache.
- Apply capability penalties for account/model pair.
- Use fallback model policy if enabled.
- Re-evaluate account scoring and retry path.
- File watcher detects account-file updates.
- Debounce and reload in-memory account manager.
- Session affinity and guardian processes continue with updated state.
Use:
codex-multi-auth config explain # every pluginConfig field: value, default, source
codex-multi-auth config explain --json # machine-readable report
codex-multi-auth config template # starter config template (modern|legacy|minimal)
codex-multi-auth status
codex-multi-auth report --json
codex-multi-auth rotation statusconfig explain mirrors the real load precedence in §3 — env path first, then unified settings, then the legacy ladder — and reports each field's source (env, unified, file, default, plus unreadable/none when the active source cannot be read) together with the env names that could override it. Because it resolves the same way as loadPluginConfig, it is the authoritative answer to "which file and which env var produced this value?"
Check files:
~/.codex/multi-auth/settings.json~/.codex/multi-auth/openai-codex-accounts.json~/.codex/multi-auth/runtime-observability.json