Repository navigation
codex login removes the existing auth.json before the flow can succeed, so a failed login logs the user out #46430
Description
[Bug] codex login removes the existing auth.json before the flow can succeed, so a failed login logs the user out
Component: Codex CLI (codex login, codex login --device-auth)
Version: tested on codex-cli 0.153.4 (linux-x86_64, musl standalone build)
Summary
codex login appears to delete the existing credentials at the start of the flow
rather than writing new ones atomically on success. If the flow fails at any later step
(browser not reachable, token exchange transport error, expired device code, wrong account
in the browser, …), the user is left logged out even though they were logged in before.
This turns a recoverable, transient failure into a destructive one, and it is especially
painful on headless/remote hosts where the only way to fix it is to re-run a login flow
that just failed.
Steps to reproduce
-
Have a working login:
codex login status→Logged in using ChatGPT. -
Start a new login that will fail late, for example while the token endpoint is
temporarily unreachable:codex login --device-auth # complete the browser step # -> ERROR: Error logging in with device code: device code exchange failed: # error sending request for url (https://auth.openai.com/oauth/token)
-
Check the state:
ls ~/.codex/auth.json # No such file or directory codex login status # Not logged in
Observed twice in a live environment (a remote Linux host): the previously valid
auth.json was gone after a failed login attempt.
Expected
Either:
- the existing credentials are left untouched until the new ones are successfully
obtained (write to a temp file, then atomically rename), or - the old credentials are backed up and restored automatically when the flow fails,
or at minimum - the CLI warns that continuing will remove the current login.
Actual
The previous credentials are removed before the flow completes; a failed login means the
user must successfully complete a login (possibly under the same broken network
conditions) before Codex works again.
Impact
- On remote/headless machines, a single transient network error can take a working Codex
installation offline until an operator can re-authenticate. - Makes it risky to attempt a login (e.g. to switch accounts) while anything about the
network path is uncertain.
Suggestion
- Write credentials atomically; only replace the previous file on success.
- On failure, restore the previous credentials and say so in the error output.
- Consider printing the target
CODEX_HOMEand whether an existing login will be
replaced, sinceCODEX_HOMEmay point at a non-default directory.
Environment
codex-cli 0.153.4
Linux x86_64 (headless server), CODEX_HOME=/<user>/.codex (non-default path)
Reached OpenAI through a local HTTP proxy (HTTP_PROXY/HTTPS_PROXY set for the process)
Related issues
- [macOS 26.818.41705] Thread resume signs out desktop — rotated refresh token never persisted to auth.json, fresh login invalidated in 76s #40267 — desktop sign-out caused by refresh-token rotation not being persisted
(different mechanism: the token is invalidated, whereas here the file is removed by the
login command itself before it can succeed). - Codex Windows: fresh OAuth refresh token immediately invalidated when two ChatGPT accounts are signed in on the web #39199 — two ChatGPT accounts invalidating each other's refresh tokens
(adjacent symptom: credentials lost on a machine, but again not caused bycodex login).
Minimal fix sketch
// pseudo-code: make credential replacement atomic and non-destructive on failure
let tmp = auth_path.with_extension("json.tmp");
write(tmp, &new_credentials)?; // only after the flow has fully succeeded
fs::rename(tmp, auth_path)?; // atomic replace
// on any failure path: leave the previous auth.json untouched
Activity
84dnnvbdvp-debug commentedon Sep 18, 2026
Current
main(7498521d288b9b3b96ffba4eedf089d8d6e06a84) makes this more destructive than anauth.jsonatomic-write bug, so the minimal fix sketch should move one boundary earlier.codex-rs/cli/src/login.rshasclear_existing_auth_before_login(), and both browser ChatGPT login and device-code login call it before starting the replacement flow. That helper callslogout_with_revoke(...).The auth manager's
logout_with_revokepath first loads the existing stored auth, callsrevoke_auth_tokens(...)for it, and then deletes the auth stores. Revocation failure is only warned about; local logout still proceeds. So by the time a later device-code/token exchange fails, the previous login can be gone in two independent senses:That means a temp-file + atomic rename at the eventual
save_authsite is not sufficient by itself: restoring the oldauth.jsonafter failure can restore a credential whose refresh token was intentionally revoked at flow start.I would make replacement login transactional at the login lifecycle, not only at the file-write layer: acquire and validate the replacement credentials first while leaving the current auth untouched; persist the replacement successfully to the configured auth backend; only after that commit should any cleanup/revocation of the superseded credential occur (and cleanup failure should not roll back the now-valid replacement).
A regression that catches the current ordering specifically:
codex login --device-auth(and separately browser login), then force a late replacement-flow failure after startup;The same invariant should be exercised for every configured
AuthCredentialsStoreModerather than being expressed only as anauth.jsonrename property, because the destructive helper is above the storage backend.This keeps explicit
codex logoutdestructive while makingcodex logina replacement transaction instead of logout-then-hope-login-succeeds.trinity0506 commentedon Sep 18, 2026
Thanks — I verified this against
main(7498521d288b) and agree that the file-level sketch is the wrong boundary.Concretely:
codex-rs/cli/src/login.rs:122—clear_existing_auth_before_login()callslogout_with_revoke(...)and onlytracing::warn!s on failure (:128). It runs before the replacement flow inlogin_with_chatgpt(:147),run_login_with_device_code(:335) andrun_login_with_device_code_fallback_to_browser(:386).codex-rs/login/src/auth/manager.rs:979—logout_with_revoke()loads the stored auth, callsrevoke_auth_tokens(...)(failure only warned,:996), thenlogout_all_stores(...).So the superseded credential can be gone in both senses described above, and restoring
auth.jsonon a late failure may restore an already-revoked refresh token. Superseding the "minimal fix sketch" in the issue body with the lifecycle version:AuthCredentialsStoreMode(not as anauth.jsonrename property);Field evidence from our side (headless Linux server,
codex-cli 0.153.4): aftercodex login --device-authwas started and the flow failed late at the token exchange (error sending request for url (https://auth.openai.com/oauth/token)),auth.jsonwas already absent while the flow was still running, andcodex login statusreportedNot logged in. We did not capture whether the revoke endpoint was called, so we cannot confirm the server-side half from our logs.One adjacent asymmetry worth checking while the invariant is being moved:
run_login_with_api_key(codex-rs/cli/src/login.rs:204) does not go throughclear_existing_auth_before_loginat all — it callslogin_with_api_keydirectly and overwrites the store, i.e. the previous ChatGPT tokens are dropped locally without being revoked.84dnnvbdvp-debug commentedon Sep 19, 2026
I checked the adjacent paths on current
main, and the asymmetry is broader than only--with-api-key.run_login_with_api_keycallslogin_with_api_keydirectly; that function constructs a freshAuthDotJsoncontaining only the API key and callssave_auth. Likewise,run_login_with_access_tokencallslogin_with_access_tokenwithoutclear_existing_auth_before_login; both PAT and agent-identity branches construct a fresh replacementAuthDotJsonand thensave_auth. So a prior ChatGPT token set is replaced locally on those successful transitions without going throughrevoke_auth_tokens.That suggests the transactional repair should define post-commit cleanup by credential transition, rather than mechanically moving the existing
logout_with_revokecall after every successful login:I would make those transitions a table-driven regression family across
AuthCredentialsStoreMode. The important invariant is still “never destroy the only working credential before the replacement commits,” but the cleanup action is credential-type-specific. Otherwise a fix for the reported destructive ChatGPT re-login path could accidentally preserve the current stale-server-token asymmetry or revoke something that was never revocable in the first place.