[macOS] Freshly issued refresh token rejected as refresh_token_invalidated ~10s later, reproduced with a single local process (trigger: opening a thread)
Edited for concision. An earlier version blamed the app-server for not adopting new credentials; that was wrong and is retracted (comment).
TL;DR
- Severity: P0. Sign in succeeds, then opening a thread signs you out 6–17 s later, indefinitely. It is impossible to do anything in Codex Desktop until this is resolved. Still present after updating to 26.820.60940 / core 0.150.0-alpha.8. 20+ open issues in the past week report the same thing on macOS and Windows (list at the bottom).
- Cause: the Desktop calls
getAuthStatus{refreshToken:true}on thread open, which forces a refresh-token rotation, and the auth server rejects the just-issued token withrefresh_token_invalidated. - Reproduced with one local
codexprocess and no intervening refresh POST, so the invalidation is server-side (or the closed-source Desktop shell) — not in this repo. - Client code doesn't cause it but amplifies it: it rotates on every thread open with no freshness check. Hardening suggestions below.
- In-app feedback thread:
01a036de-1b6d-71d0-8de3-e1595279a0dd
What version of the Codex App are you using (From "About Codex" dialog)?
26.818.61809 (build 7019) — bundled core codex-cli 0.149.0-alpha.4.3
What subscription do you have?
ChatGPT Pro
What platform is your computer?
macOS 26.6.2 (build 25G83), Apple Silicon (arm64)
What issue are you seeing?
One process, 10 seconds, freshly issued token:
21:23:06 oauth token exchange succeeded status=200 OK
21:23:07 Reloading auth
21:23:07 Reloaded auth, changed: false <-- logging artifact, see below
21:23:16 app-server request: thread/resume <-- user opened a thread
21:23:16 Reloading auth for account <account-C>
21:23:16 Reloaded auth, changed: false
21:23:16 Refreshing token
21:23:16 ERROR Failed to refresh token: 401 Unauthorized
{"error":{"code":"refresh_token_invalidated", ...}}
<details>
<summary>Why the token presented at :16 is provably the one issued at :06</summary>
- Reaching
Refreshing tokenmeans the guarded reload returnedReloadedNoChange, i.e.auths_equal_for_refreshwas true — for ChatGPT that compares the fullauth.jsoncontents (manager.rs:2471-2473). Cache and disk were byte-identical. - The cache was loaded at
:07from the file written by the:06exchange (persist is awaited before the success redirect,server.rs:437-457). - The only refresh-grant POST in the codebase (
manager.rs:1583) is reachable only through a path that always logsRefreshing tokenfirst (manager.rs:2818). No such line exists from this pid between:07and:16.
Caveat: the Desktop shell is closed-source and could present the same refresh token itself — "one codex process" ≠ "one client".
</details>
<details>
<summary>The <code>changed: false</code> log line is misleading</summary>
CodexAuth::eq (manager.rs:154-163) compares only the auth mode for ChatGPT. set_cached_auth logs changed from that coarse comparison while installing the new auth unconditionally (manager.rs:2575-2582); the content-sensitive auth_changed_for_refresh is computed but never logged. So a ChatGPT→ChatGPT re-login always logs changed: false on a fully successful adoption.
</details>
<details>
<summary>Client-side amplifiers (not causes)</summary>
- Unconditional rotation.
refresh_token_if_requested(account_processor.rs:1019-1032) POSTs wheneverrefreshToken:trueis passed — no freshness check, even though therefreshToken:falsepath already gates onshould_refresh_proactively. This log: 256 rotations vs 3 skips. - No cross-process safety.
refresh_lock(manager.rs:2045) is per-process;FileAuthStorage::save(storage.rs:206-223) is truncate+write, and torn reads are silently cached asNone(manager.rs:2559-2560). - Stranded after account switch. A process whose cached account no longer matches
auth.jsonhitsReloadOutcome::Skippedforever (manager.rs:2446-2452) — 128 occurrences here — and the error is never recorded, sogetAuthStatuskeeps reporting the stale account. - Three account ids were active on this machine that day; every failing cycle postdates the third. Correlation only (cf. #39199).
</details>
What steps can reproduce the bug?
- Sign in to Codex Desktop on macOS.
- Open an existing thread.
- Signed out (
refresh_token_invalidated). Sign in again → repeats.
<details>
<summary>Pull the same evidence from any affected machine</summary>
sqlite3 -readonly "file:$HOME/.codex/logs_2.sqlite?mode=ro" "
SELECT datetime(ts,'unixepoch','localtime'), process_uuid, feedback_log_body
FROM logs
WHERE feedback_log_body LIKE '%token exchange succeeded%'
OR feedback_log_body LIKE '%Reloading auth for account%'
OR feedback_log_body LIKE '%Refreshing token%'
OR feedback_log_body LIKE '%account id mismatch%'
OR feedback_log_body LIKE '%refresh_token_invalidated%'
ORDER BY ts;"
</details>
What is the expected behavior?
A refresh token issued seconds earlier should not be rejected, and opening a thread should not force a rotation at all.
Suggested hardening (none is the root cause):
- Log
auth_changed_for_refreshnext tochanged(manager.rs:2581) — one line. - Make
refreshToken:truehonorshould_refresh_proactively(or a short min interval) instead of always POSTing. - temp-file+rename in
FileAuthStorage::save; file lock around refresh→persist. - On
Skipped, surface an "account switched — sign in again" state instead of stranding the process. - If the POST succeeds but persist fails (
manager.rs:3019-3025), keep the rotated token in memory rather than losing it.
Additional information
Latent, probably unrelated: AutoAuthStorage (storage.rs:431-450) — save() falls back to the file on keyring failure without clearing the stale keyring entry, and load() prefers the keyring, so later loads return old tokens indefinitely. Worth its own issue.
Other reports of the same bug
Same mechanism — a fresh refresh token rejected as refresh_token_invalidated / auth lost on thread open or right after login:
- macOS: #39162 (58 comments), #39803 (18), #39886, #39684, #40267, #40395, #40373, #40143, #40664, #40242
- Windows: #39189 (22), #39170 (15), #39696 (12), #40036 (12), #39491 (10), #39925, #39966, #39199, #40632, #40699, #40704 (on 26.820.60940), #40374, #40761, #40603, #40074
- Onset: #40138 (401s begin on update 0.148.0 → 0.149.0-alpha.4.1)
Roughly 200 user comments across these in eight days, with no maintainer response on any of them as of this edit.
Temporary workaround for Desktop users (patched core, tested on both 0.149.0-alpha.4.3 and 0.150.0-alpha.8): see this comment.
Account ids replaced with placeholders. Full log extract available privately on request.
10 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
@cooper-oai @pakrym-oai @etraut-openai — apologies for the direct ping; I know your time is scarce, so tagging you specifically because you wrote the code paths involved (
should_refresh_proactively,reload_if_account_id_matches/refresh_token_if_requested, andauths_equal_for_refresh+ #15798).Flagging priority: this makes the Codex desktop app completely unusable right now — every thread open signs the user out within ~10 s, and re-signing in doesn't help. Several open issues (#40267, #39803, #39162, #39925) describe the same symptom on the current build, and none have a maintainer response yet.
The TL;DR at the top of the issue is five bullets. The short version: a refresh token issued seconds earlier is rejected as
refresh_token_invalidatedwith one local process and no intervening refresh POST, which points server-side (or at the Desktop shell). The client-side hardening suggestions (mainly: don't force a rotation on everygetAuthStatus{refreshToken:true}) would at least shrink the blast radius.Happy to share the full log extract privately or test a build. Thanks.
Independent reproduction on the same current build, with an additional trigger and endpoint-level evidence.
Environment:
26.818.61809(build7019), bundledcodex-cli 0.149.0-alpha.4.3hostId=local); no Remote Control host involvedI reproduced the signed-out transition twice today:
07:40:57Z; opening Quick Chat at07:46:04Zwas followed byGET /backend-api/accounts/:param/settingsreturning 401 withhadToken=true, thenhasToken=false / auth_token_missingat07:46:06Z.12:18:34Z; an ordinary localthread/resumesucceeded at12:24:02Z, followed bymaybe_resume_success, then the same account-settings 401 at12:24:03Z. The forced OAuth refresh returnedrefresh_token_invalidated, and the app changed toauth_token_missingat12:24:04Z.The access token issued during the second login was valid until 2026-09-04, so this was not normal access-token expiry. The persisted credential file still contained access, ID, and refresh token fields after the UI transitioned to signed out.
Failed refresh request metadata (no token or account data):
x-request-id: 79cb6283-a741-4d20-87d6-5bf209a873ddcf-ray: a30a92506e578ec0-NRTThis confirms the issue is not Remote Control-specific and can be triggered by Quick Chat as well as local thread resume. It also confirms the sequence in which the settings-endpoint 401 causes a forced refresh and then an app-wide auth drop. Please consider preserving/classifying the settings response before treating it as token expiry, and avoid replacing the app-wide auth state with
auth_token_missingwhile a still-valid access token remains.Raw logs contain local paths and conversation/account metadata, so I am not posting them publicly. I can provide a sanitized timeline or private log excerpts if needed.
Temporary workaround for Desktop users (tested, working on my machine)
Since the invalidation is server-side, the only client-side lever is to stop rotating the refresh token when nothing needs it. The access token issued at login stays valid after the refresh token is killed (authenticated
GET /backend-api/codex/models→200at +3 min and +6 min after arefresh_token_invalidated401), and it has ~10 days of validity, so skipping the forced rotation is enough to keep the app usable.The change —
823fd73f(16 source lines + tests, on top ofmain):AuthManager::refresh_token_if_stale(): no-op when the cached ChatGPT access token isn't near expiry (reuses the existingshould_refresh_proactivelygate); otherwise falls through torefresh_token().refresh_token_if_requested(thegetAuthStatus{refreshToken:true}/account/readpath) calls it instead ofrefresh_token(). The 401-recovery path is unchanged, so a genuinely rejected access token still triggers a real refresh.Result on this machine — after swapping the patched core into Codex Desktop
26.818.61809, five thread opens produced fiveSkipping token refresh because the access token is not near expiry.lines, zero calls to/oauth/token, zero 401s, no sign-out. Before the patch, every thread open was a 401.To try it (macOS, Apple silicon; needs a Rust toolchain; at your own risk):
Notes:
rust-v0.149.0-alpha.4.3tag your app bundles plus the fix, with the version string set to match. Check your bundled version first:/Applications/ChatGPT.app/Contents/Resources/codex --version. For a different version, cherry-pick823fd73fonto that tag.cpfails withOperation not permitted. Don't usecp -fbefore you have that permission — it unlinks the target before the write is refused.cp ~/codex.orig /Applications/ChatGPT.app/Contents/Resources/codex.TOKEN_REFRESH_INTERVAL), and that will fail until the server side is fixed — so expect one sign-in a week instead of one per thread open.For maintainers — three more single-process reproductions this morning, all
refresh_token_invalidated≤11 s after issue, withopenai-processing-ms≈ 100 (a real server decision, not edge).x-request-ids:9161acb5-0ad9-4eac-abf3-40c8df98b9b3(13:34:19Z),1810536b-0bd4-4450-824f-d7619046b52a(13:35:32Z),501910d8-932c-4936-a7b7-0c5a9f290fd4(13:38:24Z). Also observed: the shell'sdesktop-authgetAuthStatusimmediately after login does not force a refresh; the one fired ~1 s afterthread/resumedoes — so the forced rotation is specifically tied to opening a thread.I can reproduce what appears to be the same issue on Windows.
Environment
26.820.609400.150.0My logs showed a successful OAuth token exchange (
200 OK), followed shortly afterwards by:Refreshing token→401 Unauthorized→refresh_token_invalidatedSimply deleting
~/.codex/auth.jsonand signing into Codex again did not fix the issue. The newly issued credentials would still become invalid shortly after login.However, the following sequence has resolved the authentication loop for me so far:
~/.codex/auth.json.After doing these steps in this order, the authentication loop stopped.
This may be relevant to the observation in this issue about multiple account IDs and to #39199. It looks possible that the browser ChatGPT session/account state and the locally stored Codex OAuth credentials can get out of sync, resulting in a newly issued refresh token being invalidated.
I still have the
logs_2.sqlitelogs from the failing state and can provide additional timestamps/log entries if useful.Thank you @Suhaibinator , it worked for me.
Facing the same issue after redownloading the the app as required after the npm attack. Every time I click a button or send a message or do anything in the ChatGPT app, it logs itself out and wants to re-login over and over again. I already uninstalled the app, downloaded a fresh installer and tried again. No error messages are shown. Not critical for me so will not be trying the workaround and will wait for a fix.
no-active-thread-01a03ed8-278b-77b2-807f-af15a5cc7cd6
This is literally a P0 (or at least P1) bug, why aren't they prioritizing this...
Independent macOS confirmation of the browser-sign-out reset sequence
I was experiencing the same repeated authentication loop on:
macOS Tahoe 26.5.2, Apple Silicon
Codex/ChatGPT Desktop 26.820.60940
Sign-in itself succeeded, but opening a Codex thread caused the app to lose authentication and return to the sign-in screen. Re-signing in repeatedly did not resolve it.
The sequence that has resolved it for me so far was:
Fully quit the Codex/ChatGPT desktop app.
Sign out of ChatGPT in the web browser.
Move the local Codex credential file aside:
mv ~/.codex/auth.json ~/.codex/auth.json.before-desktop-reset
Relaunch the desktop app.
Complete a fresh ChatGPT sign-in through the desktop app.
After confirming Codex Desktop was functioning, sign back into ChatGPT in the browser.
So far the desktop app has remained authenticated through:
normal Codex tasks;
reopening existing threads;
opening a new thread;
fully quitting/relaunching the desktop app.
I cannot determine which step was decisive, so I am not claiming this establishes the root cause or a general fix. The notable difference from my previous unsuccessful reauthentication attempts was explicitly signing out of the browser ChatGPT session before moving aside ~/.codex/auth.json and then performing the fresh desktop OAuth login.
This appears consistent with the workaround reported above where synchronizing/resetting the browser session and local Codex OAuth state stopped the loop.
I’ll update this comment if the failure returns.
Thanks for this. Your five steps worked perfectly. I went through all the actions that triggered the sign-out behavior earlier, and I was not able to reproduce the issue again.