[macOS] Freshly issued refresh token rejected as refresh_token_invalidated ~10s later, reproduced with a single local process (trigger: opening a thread)
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.4k
- PR merge metrics
- PR metrics pending
Description
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", ...}}
Why the token presented at :16 is provably the one issued at :06
- 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".
The changed: false log line is misleading
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.
Client-side amplifiers (not causes)
- 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).
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.
Pull the same evidence from any affected machine
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;"
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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with refresh_token_if_requested in account_processor.rs, then trace the auth reload and persistence paths in manager.rs and storage.rs, especially the cited line ranges. Reproduce the thread-open flow and inspect the existing auth tests or logs; done means the reported refresh-token failure is addressed and the expected sign-in state remains usable.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- authentication, cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100