Repository navigation
[bug] first-run auto app-bind can orphan ~/.codex/config.toml with no restorable backup #614
Description
Summary
ensureFirstRunSetup can bind the real ~/.codex/config.toml to the runtime proxy (model_provider = "codex-multi-auth-runtime-proxy" + a [model_providers.codex-multi-auth-runtime-proxy] block) and then end up in a state where the app-bind state/backup files no longer exist. When that happens:
- The config is left pointing at a runtime-proxy
base_url(e.g.http://127.0.0.1:51758) with no router running, so the officialcodexCLI/Desktop is routed to a dead port. codex-multi-auth rotation unbind-appreports "Codex app bind was not configured" and does nothing, becauseunbindCodexAppRuntimeRotationLockedkeys restoration offapp-bind/runtime-rotation-app-bind.json/codex-config-backup.json, which are gone.rotation statusreports "Codex app bind: not configured" and "Runtime rotation proxy: disabled", even though the real config is still bound — so the user gets no signal that their config is broken, and no automated way to fix it.
Net effect: the user's normal Codex usage breaks (wrong provider, dead port), and ironically codex resume history is hidden under the proxy provider — the exact symptom #612 was about. The only recovery is hand-editing config.toml.
Observed on
codex-multi-auth@2.3.1(global install), Windows 11, Node 24.- Reproduced incidentally while exercising the CLI: a first CLI invocation against a machine whose
first-run-setup.jsonmarker was not yet set triggered the lazy bind; the app-bind state dir (~/.codex/multi-auth/app-bind/) contained only an emptyruntime-rotation-app-router.log— noruntime-rotation-app-bind.json/codex-config-backup.json.
Resulting real config (abridged):
model_provider = "codex-multi-auth-runtime-proxy"
...
[model_providers.codex-multi-auth-runtime-proxy]
name = "codex-multi-auth"
base_url = "http://127.0.0.1:51758"
requires_openai_auth = false
experimental_bearer_token = "…"
wire_api = "responses"rotation status simultaneously: Runtime rotation proxy: disabled / Codex app bind: not configured.
Why it's a gap
There are two independent robustness problems, either of which alone would prevent silent breakage:
-
No restorable backup guarantee.
bindCodexAppRuntimeRotationLockedwritesbackupPathbefore rewritingconfig.toml, but the backup and state can later be removed (cleanup, partial unbind, manual deletion, crash) while the rewritten config persists. There is no "the config is bound" fact stored in a place that survives state-file loss. -
Status/unbind trust the state files, not the config. Both
getAppBindStatusandunbind-appinfer "bound" purely fromapp-bind/*.json. When those are missing they conclude "not configured" even thoughconfig.tomlliterally contains the runtime-proxymodel_provider+ provider block. So the one command meant to fix it is a no-op exactly when it's needed.
Suggested fixes (pick some)
- Self-healing unbind: if
config.toml's top-levelmodel_provider === codex-multi-auth-runtime-proxy(or the proxy[model_providers.…]block is present),unbind-appshould restore it even with no backup —restoreConfigTomlFromRuntimeRotationProvidercan already strip the block; for the top-level provider, fall back to"openai"(or a configurable default) when no backup line exists. - Status truth from config:
getAppBindStatus/rotation statusshould detect a bound config directly (scanconfig.toml) and report "bound but unmanaged (no state/backup)" instead of "not configured", so the breakage is visible. doctorcheck + fix: add a doctor probe that flags "config.toml points at runtime proxy but no router/state" and offers--fixto unbind.- Harden bind durability: treat a successful bind as not-complete until the backup is fsynced and re-readable; consider an idempotent marker in
config.toml(a comment sentinel) so even total state loss is recoverable.
Workaround (for affected users today)
Manually edit ~/.codex/config.toml: set model_provider = "openai" and delete the [model_providers.codex-multi-auth-runtime-proxy] block. (Back up the file first.)
Notes
- Not caused by the
historycommand (feat(history): add provider-agnostic local session browser (#612) #613) — that command is read-only. This is in the first-run / app-bind path. - Related: the existing
docs/troubleshooting.mdrow "Codex history disappears after app bind" assumesunbind-app/disablework; they don't in this no-backup state.
Activity
ndycode commentedon Jun 16, 2026
Sharper root cause (confirmed via testing on 2.3.1)
While smoke-testing I reproduced this deterministically and traced the exact mechanism — it's worse than "first-run only," and the no-backup part is confirmed.
Trigger: the first-run marker is keyed off
getCodexMultiAuthDir():ensureFirstRunSetupearly-returns only ifexistsSync(markerPath)(first-run.ts:409). So any invocation that pointsCODEX_MULTI_AUTH_DIRat a directory without that marker re-runs first-run setup — including re-running app-bind. This isn't limited to a true first run; a new/relocated multi-auth dir (or anything that resolvesgetCodexMultiAuthDir()to a fresh path) re-arms it.Blast radius: the re-run binds against the real
CODEX_HOME(~/.codex), not the relocated dir —bindCodexAppRuntimeRotation()resolves the codex home independently ofCODEX_MULTI_AUTH_DIR. So a command run with an isolated/alternate multi-auth dir still mutates the real~/.codex/config.toml.No backup persists: after each re-bind,
~/.codex/multi-auth/app-bind/contained only an emptyruntime-rotation-app-router.log— noruntime-rotation-app-bind.json, nocodex-config-backup.json. SobindCodexAppRuntimeRotationLocked's pre-writeatomicWriteFile(paths.backupPath, …)is either not landing or being cleaned up, leaving the rewritten config unrecoverable byunbind-app. Observed it re-bind to a fresh port on a second invocation (51758→51707), each time with no backup.Observed sequence:
CODEX_MULTI_AUTH_DIR=<fresh dir> codex-multi-auth list→ printsBound Codex app config C:\Users\neil\.codex\config.toml to http://127.0.0.1:51707+Routed 2 existing Codex app shortcut(s).~/.codex/config.tomlnow hasmodel_provider = "codex-multi-auth-runtime-proxy"+ the proxy block, pointing at a port with no live router.rotation status→Codex app bind: not configured;unbind-app→Codex app bind was not configured(no-op).Note on first-run marker: even the normal marker recorded
appBind: "skipped"while the config was still bound on a later run — consistent with the bind coming from the marker-less re-run path rather than the recorded first-run outcome, and with status/unbind trusting state files over the actual config.Reliable workaround for testers: to exercise the CLI without touching the real config, set all of:
CODEX_HOME=<temp>(so any bind targets the temp home), plusCODEX_MULTI_AUTH_APP_BIND=0,CODEX_MULTI_AUTH_APP_BIND_INSTALL=0,CODEX_MULTI_AUTH_APP_LAUNCHER_INSTALL=0,CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=0. With those, no bind fires and the real~/.codexis untouched (verified).This reinforces the two fixes proposed above: (a) make
unbind-app/statusderive "bound" fromconfig.tomlitself, not the state files; and (b) guarantee a restorable backup (or an in-config sentinel) before the provider rewrite — and gate the bind so it doesn't fire just because the multi-auth dir lacks a marker.coderabbitai commentedon Jun 16, 2026
🔗 Related PRs
#439 - Enable runtime rotation and auto-update by default [merged]
#460 - Fix app bind history repair [merged]
#462 - Fix Codex TUI goal proxy handling [merged]
#485 - fix: preserve hook trust in runtime shadows [merged]
#607 - fix(rotation): break stale-recovery deadlock on transient account state [merged]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
fix(config-toml): avoid duplicate model_provider in half-orphan recov…
coderabbitai commentedon Jun 16, 2026
Coding Plan
Summary
isConfigBoundToRuntimeProxyto detect proxy provider in config.toml independent of state filesAppBindStatuswithconfigBoundfield so callers can identify orphaned state (bound: false, configBound: true)unbindCodexAppRuntimeRotationLockedto restore orphaned configs by stripping proxy block and defaulting tomodel_provider = "openai"when no backup existsunbind-appDesign Choices
Design Choice 1: fallback model_provider when no backup exists
Options Considered:
model_provider = "openai"as the safe defaultmodel_providerline entirely (leave unset)Chosen Option: 1
Rationale: openai is the documented default for codex; users who had a different provider before bind lost their backup anyway and openai is the least surprising restore target.
💡 User Tips
Regenerate the plan with different choices with
@coderabbitai <feedback>.Implementation Steps
Phase 1: add config.toml proxy detection utility
this phase adds a function to detect whether
config.tomlcurrently hasmodel_providerset to the runtime proxy id, independent of state files. this is the foundation for both status reporting and self-healing unbind.Task 1: create `isConfigBoundToRuntimeProxy` helper
add an exported function in
lib/runtime/config-toml.tsthat takes raw config string and returns boolean indicating whethermodel_providerequalsRUNTIME_ROTATION_PROXY_PROVIDER_ID.extractTopLevelModelProviderLine(already exists) to get the current provider lineRUNTIME_ROTATION_PROXY_PROVIDER_IDfromlib/runtime-constants.tstrueif match,falseotherwise (including when no model_provider line exists)Task 2: add fallback restoration function
extend
lib/runtime/config-toml.tswith logic to restore config.toml when no backup exists — strip proxy block and setmodel_provider = "openai".restoreConfigTomlFromOrphanedBind(currentConfig: string): stringthat:removeRuntimeRotationProviderBlockto strip the proxy tablemodel_providerline withmodel_provider = "openai"restoreTopLevelResponseStoragewith empty original (removes the forced setting)restoreTopLevelModelProviderto accept a fallback value parameter whenoriginalConfighas no provider line🤖 Prompt for AI agents
Phase 2: extend status and unbind to handle orphaned state
this phase wires the detection utility into
getAppBindStatusandunbindCodexAppRuntimeRotationLockedso they can identify and fix orphaned configs.Task 1: extend `AppBindStatus` type with orphaned indicator
update the
AppBindStatustype inlib/runtime/app-bind.tsto include a new field indicating orphaned bind state.configBound: booleanfield — true when config.toml has proxy provider regardless of state filesbound: false, configBound: truemeans orphanedTask 2: update `getAppBindStatus` to scan config.toml
modify
getAppBindStatusatlib/runtime/app-bind.ts:611to also read and scan config.toml for proxy provider presence.readConfigIfExists(paths.configPath)isConfigBoundToRuntimeProxyon the content to setconfigBoundboundfield remains state-file based for backward compat!status.bound && status.configBoundas orphanedTask 3: update `unbindCodexAppRuntimeRotationLocked` for orphaned recovery
modify the unbind function at
lib/runtime/app-bind.ts:751to restore config.toml even when state files are missing.state === null && backup === null, read config.tomlisConfigBoundToRuntimeProxy(configContent)is true, call the new orphan restoration functionatomicWriteFile"Restored orphaned Codex app config (no backup available, defaulted to openai)"Task 4: update `formatAppBindStatus` for orphaned display
update the status formatter in
lib/codex-manager/commands/rotation.tsto display orphaned state clearly.!status.bound && status.configBound, display:"Codex app bind: orphaned (config bound but state files missing)"codex-multi-auth rotation unbind-appto fix🤖 Prompt for AI agents
Phase 3: tests for orphaned bind scenarios
add comprehensive test coverage for the orphaned state detection and recovery, following existing patterns in
test/app-bind.test.ts.Task 1: test `isConfigBoundToRuntimeProxy` detection
add unit tests in
test/config-toml-restore.test.tsor newtest/config-toml-detection.test.tsfor the detection helper.model_provider = "codex-multi-auth-runtime-proxy"presentmodel_provider = "openai"or other valuemodel_providerline existsTask 2: test orphan restoration function
add unit tests for the fallback restoration when no backup exists.
model_provideris set to"openai"disable_response_storageforced setting is removedTask 3: test `getAppBindStatus` orphaned detection
add integration tests in
test/app-bind.test.tsfor orphaned state detection.app-bind/getAppBindStatusand assertbound: false, configBound: truebound: true, configBound: truebound: false, configBound: falseTask 4: test `unbindCodexAppRuntimeRotation` orphan recovery
add integration tests for unbind self-healing in
test/app-bind.test.ts.unbindCodexAppRuntimeRotationmodel_provider = "openai"and no proxy blockTask 5: test `formatAppBindStatus` orphaned output
add test in
test/codex-manager-rotation-command.test.tsfor orphaned status display.getAppBindStatusreturning{ bound: false, configBound: true, ... }🤖 Prompt for AI agents
Research
the app-bind subsystem lives in
lib/runtime/app-bind.tswith state files under~/.codex/multi-auth/app-bind/.getAppBindStatusatlib/runtime/app-bind.ts:611reads onlyruntime-rotation-app-bind.jsonto determinebound.unbindCodexAppRuntimeRotationLockedatlib/runtime/app-bind.ts:751keys restoration offstatePath/backupPath— when both are missing it early-returns "not configured" without inspectingconfig.toml. toml manipulation happens inlib/runtime/config-toml.tswithrestoreConfigTomlFromRuntimeRotationProvidercapable of stripping the proxy block even without a backup. the provider id constant isRUNTIME_ROTATION_PROXY_PROVIDER_ID = "codex-multi-auth-runtime-proxy"inlib/runtime-constants.ts.🚀 Next Steps
🤖 All AI agent prompts combined
💡 Iterate on the plan with:
`@coderabbitai` <feedback>💬 Have feedback or questions? Drop into our discord!
coderabbitai commentedon Jun 16, 2026
🔗 Related PRs
#439 - Enable runtime rotation and auto-update by default [merged]
#460 - Fix app bind history repair [merged]
#462 - Fix Codex TUI goal proxy handling [merged]
#485 - fix: preserve hook trust in runtime shadows [merged]
#607 - fix(rotation): break stale-recovery deadlock on transient account state [merged]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
test(app-bind): add integration coverage for half-orphan unbind (#614)
test(app-bind): assert disable_response_storage cleanup in orphan unb…
fix(app-bind): self-heal orphaned runtime-proxy bind with no backup (#…
chore(release): cut v2.3.2