Repository navigation
[macOS 26.818.41705] Thread resume signs out desktop — rotated refresh token never persisted to auth.json, fresh login invalidated in 76s #40267
Description
Summary
On macOS, opening/resuming an existing thread signs the app out. The desktop auth poller attempts an OAuth refresh, receives 401 refresh_token_invalidated, and the UI drops to signed-out. Logging out, having auth.json removed, and signing in fresh does not fix it — a brand-new token family is invalidated within ~76 seconds.
The distinguishing detail: the rotated refresh token is never written to auth.json. After a fresh sign-in, auth.json mtime and last_refresh never advance, so the client keeps presenting a refresh token the server has already retired.
Environment
| App | ChatGPT.app 26.818.41705 (CFBundleVersion 6971), bundle id com.openai.codex |
| Bundled core | codex-cli 0.149.0-alpha.4.1 (Contents/Resources/codex) |
| Workspace bundle | 26.819.11345 (node v24.19.0, python 3.12.13), arch arm64 |
| OS | macOS (Darwin 24.6.0), Apple silicon |
| Auth mode | chatgpt (ChatGPT sign-in, not API key) |
Reproduction
- Sign out from the app.
- Confirm no
ChatGPT/codexprocesses remain and~/.codex/auth.jsonis gone. - Relaunch, sign in. A new token family is written.
- Open any existing thread.
- Signed out.
Evidence
Fresh sign-in, then failure 76 seconds later:
14:39:01 sign-in; auth.json written; id_token iat 14:39:01, exp 15:39:01
14:40:17 ERROR codex_login::auth::manager
Failed to refresh token: 401 Unauthorized
{
"error": {
"message": "Your refresh token has been invalidated. Please try signing in again.",
"type": "invalid_request_error",
"code": "refresh_token_invalidated"
}
}
After that failure, ~/.codex/auth.json still holds the same refresh token written at sign-in, and last_refresh is unchanged at the sign-in timestamp. Nothing was persisted.
Every invalidation is attributed to the desktop auth poller — rpc.method="getAuthStatus" with rpc.request_id=desktop-auth:<uuid>:
caller count
desktop-auth 12
other callers 0
Several fire in the same second as thread/resume, which is the user-visible trigger.
Error attribution by client version across full log history (token_invalidated):
bundled core 0.14x 668
desktop shell 26.818 12
Onset correlates exactly with the update. The previous desktop build (26.727.51351) last logged at 20:24:51; the new build's first log is 20:25:01, and the first refresh_token_invalidated is 20:49:05 the same evening.
Notably still working
The iOS remote client (codex_chatgpt_ios_remote) has zero auth errors in the entire log history, and successfully performed a thread/resume while the desktop was failing. Its requests ride the long-lived access_token (10-day expiry) rather than driving the hourly id_token refresh. This isolates the fault to the desktop shell's auth-refresh path rather than the account, the credentials, or the backend.
Ruled out locally
- Not the workspace-settings 401 from [Windows 26.814] Opening an existing thread signs out a personal Pro account after workspace-only settings 401 #39189 — zero
/backend-api/accounts/*/settingsrequests and zero "Must use workspace account" messages in logs. - Not architecture — app is
arm64-only on Apple silicon, no translation involved. - Not filesystem/permissions —
auth.jsonis writable, nouchg/schgflags, 173 GB free; writes succeed at sign-in and simply never happen again. - Not the workspace dependency bundle — intact and correctly built for
arm64. - Not concurrent clients — 0 of 12 invalidations came from any client other than
desktop-auth. - Not plan-related — the first failures occurred while on a paid plan.
Expected
A 401 on refresh should not drop a session whose access_token is still valid, and a successful refresh should persist the rotated token to auth.json.
Related
- [Windows 26.814] Opening an existing thread signs out a personal Pro account after workspace-only settings 401 #39189 — same symptom (resume thread → sign out), different root cause, Windows
- [Windows app 26.727] "refresh token was already used" persists after logout, deleting auth.json, and signing in again #36525 — refresh token reuse survives logout + deleting
auth.json - Codex Desktop gets stuck with refresh-token-revoked auth error during active session #25443 — desktop stuck on refresh-token-revoked mid-session
- Error loading configuration: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again #15754 — "refresh token was already used"
Workaround
None on the desktop. Driving Codex from the iOS remote client continues to work.
Activity
github-actions commentedon Aug 23, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
jeremiahstjames commentedon Aug 24, 2026
Same exact issue. i've been totally locked out of a $200 codex account for a week due to this error. ive literally tried all of the exact steps you have, deleted auth.json, reset passwords, uninstalled Codex, changed wifi.... everything i could think of. Nothing. Im about one more Troubleshoot day from just canceling.
LuoYuY commentedon Aug 24, 2026
I can reproduce this on macOS as well, with an additional potentially useful trigger: the problem started immediately after I set a ChatGPT account password in ChatGPT Settings → Account security & login → Password. This account had previously been used primarily via Sign in with Apple.
Environment:
26.818.61809chatgptObserved behavior:
/backend-api/codex/modelsreturn200 OK.getAuthStatusand logsRefreshing token.POST https://auth.openai.com/oauth/tokenreturns401 Unauthorized:The access token itself is freshly issued and unexpired when this happens.
I also ruled out stale local credentials:
~/.codex/auth.jsonaside~/.codex/auth.jsonwas createdrefresh_token_invalidatederror with the newly issued refresh token immediately afterwardSo deleting/regenerating
auth.jsondoes not fix it.The timing with setting a ChatGPT password may be relevant: changing/setting the password could reasonably invalidate existing sessions, but a fresh Codex OAuth login performed afterward also receives a refresh token that is immediately rejected, leaving Desktop in a repeated login loop.
No account IDs, email addresses, access tokens, refresh tokens, or other credentials are included here.
LuoYuY commentedon Aug 24, 2026
One additional reproduction detail that may be important: setting the ChatGPT password was not an unrelated/manual account-security change. It was prompted by the Remote Connection setup flow.
The sequence was:
refresh_token_invalidatedstate described above.~/.codex/auth.jsonand performing a fresh OAuth login does not recover Desktop; the newly issued refresh token is also rejected.So there may be a product-level reproduction path along the lines of:
social/Apple-login account → Remote Connection setup → password authentication required → set ChatGPT password → Desktop OAuth refresh-token invalidation loopThis currently leaves Desktop effectively unusable and also prevents completing the Remote Connection workflow that prompted the password setup in the first place.
LuoYuY commentedon Aug 24, 2026
Update: I found a workaround that restored Desktop for me, which may help narrow the root cause.
What worked:
This is notable because the issue had started immediately after I set a ChatGPT password for an account that had previously been used primarily via Sign in with Apple.
So the failure may involve inconsistent server/browser session state after introducing password auth to an existing social-login account, rather than a permanently broken local Desktop installation.
A possibly useful recovery path for affected users is therefore:
sign out everywhere → establish a clean web session using the original social login provider → re-authenticate DesktopThis workaround restored my Desktop session after deleting/regenerating
auth.jsonalone had not fixed the issue.ellebee2 commentedon Aug 24, 2026
Workaround that resolved this for me on 26.818.41705 (build 6971)
Still on the affected build — no rollback, no reinstall. The fix was clearing the
stale credential from both places at once. Either one alone did nothing.
What did not work: signing in from a fresh browser profile with no existing
OpenAI session. On launch the app never opened a browser at all — it silently
reused the refresh token already in
~/.codex/auth.json, so no new token wasever minted and the workaround never actually ran.
auth.jsonwas byte-identicalbefore and after (4142 b, same mtime).
What worked:
specifically (mine was timestamped identically to
auth.json's mtime, whichis how I spotted it). Logging out of all sessions is not necessary.
pgrep -fl ChatGPTto confirm — it signs out but keepsrunning, and it rewrites
auth.jsonon exit.~/.codex/auth.jsonaside. Note the app leaves the stale file in placeeven after being signed out server-side, so the app still has something to
fall back on until you remove it.
Thread resume no longer signs me out.
Evidence the rotation is now persisting, which is the actual defect in this
issue:
auth.jsonlast_refresh2026-08-24T13:55:54Z2026-08-24T20:40:11ZA new file with a new refresh token and a current
last_refresh— previously therotated token was never written to disk.
The important part for anyone still stuck: revoking the session server-side is
not enough on its own, because the client keeps reusing the stale local
auth.jsonwithout complaint. Both have to go.@jeremiahstjames — you mentioned deleting
auth.jsonand reinstalling withoutluck. If you did those without also revoking the specific Codex session from the
active-sessions list, that ordering may be why. Worth one more try.
Polarnova commentedon Aug 24, 2026
Independent reproduction: Microsoft SSO + password/2FA variant on current macOS build
This report is generated by 5.6 Sol Max
I can independently reproduce the same forced sign-out loop, with a social-login variant that may help narrow the account-side trigger.
Environment
26.818.61809(build7019)0.149.0-alpha.4.326.818.41509, bundled core0.149.0-alpha.4.115.7.9(24G830), Apple Silicon M4, arm64, 32 GB RAMchatgpt; one account, no intentional account switchingTrigger
The user-visible trigger is opening an older task, switching to a new task, or opening a task that is still running. A message does not need to be sent. Task/thread navigation is followed by loss of the authenticated account and another browser sign-in prompt.
Log evidence
The macOS app logs contain 26
account_info_token_unavailablerecords across August 23–24: 17 on August 23 and 9 on August 24. These include retry bursts, so I am not claiming 26 distinct visible sign-outs.First clear failure on release
26.818.41509:Strongest sequence on current release
26.818.61809:The freshly restored authenticated account therefore became unavailable again about 20 seconds after a successful account lookup. Additional transitions occurred later on the same build at
22:52:25Z,23:15:20Z, and23:39:24Z.The bundled
codex doctor --jsoncurrently reports:There is one installed
com.openai.codexdesktop application. No active VPN or macOS system proxy was present at collection time.This extends the social-login/account-security reproduction from Apple to Microsoft SSO. A password or 2FA change can reasonably revoke older sessions once, but it does not explain a newly authenticated desktop session becoming unavailable again 20 seconds later.
I have not compared or disclosed token values, so I am not independently asserting the rotated-token persistence mechanism described in the issue body. I am confirming the same thread-navigation trigger,
auth_token_missingstate whilerefreshToken=true, explicit server-sidetoken_revokedresponse, and rapid post-login recurrence on the newest build. No email address, account ID, tokens, cookies, device serial, task/thread IDs, or raw session transcripts are included.fabiocaseri commentedon Aug 26, 2026
I can confirm this issue on macOS with a very similar trigger and recovery path.
Environment:
chatgptTrigger/context:
Diagnostics:
session_index.jsonlwas also valid.thread/resumesucceeding first (errorCode=null, followed bymaybe_resume_success).refresh_token_invalidated.~/.codex/auth.json, but opening the thread still caused the same logout loop.A partial workaround did not fix it:
~/.codex/auth.jsonasideWhat finally fixed it was the full sequence:
~/.codex/auth.jsonaside.After this, thread resume stopped signing me out.
There was also a clear observable change in token rotation behavior: before opening the thread,
auth.jsonhad a freshly issued refresh token with length 196; after opening/resuming the thread successfully,auth.jsonwas rewritten,last_refreshadvanced, and the refresh token changed to length 211. This matches the successful persistence pattern reported above.So in my case the important distinction was that clearing the Codex session + local
auth.jsonwas not sufficient by itself. A complete browser logout followed by a fresh web login through the original social provider was required before recreating the Desktop OAuth state.No account identifiers, token values, hashes, email addresses, or local personal paths are included here.
MarcTCruz commentedon Oct 13, 2026
By running 8
codex execprocesses in parallel on oneCODEX_HOME, all 8 sent the same refresh token (rt-0) to the token endpoint. Codex leaves only the last writer's rotated pair inauth.json; under the fake authority's simulatedstrict-familypolicy, reuse revoked the family and the savedrt-1failed a probe withrefresh_token_invalidated. A later refresh needs sign-in again (inferred from code; this is not a claim about OpenAI's server policy). A concurrent reader also intermittently saw an emptyauth.jsonduring the in-place write.bash repro.sh 8 strict-familyWhy this matters for #40267
Observed, lenient mode: each grant using
rt-0succeeded and returned a distinct pair, but only one pair remained in the file; the other issued pairs were discarded.Observed with simulated
strict-family: one request usingrt-0issuedrt-1; later reuse of retiredrt-0revoked the simulated family. Codex persistedrt-1, then the separate authority probe receivedrefresh_token_invalidated. This optional mode simulates family revocation and does not assert the live authority's policy. With plainstrictrotation plus reuse rejection and no family revocation, the other 7 requests were rejected while the winner'srt-1remained inauth.jsonand usable; as observed, the 7 rejectedcodex execprocesses printed none of the permanent refresh-failure messages to stdout/stderr. The damaging revoked-file case therefore needs the simulatedstrict-familypolicy, while lenient mode shows the other half: N−1 issued pairs discarded.Inferred from the source: each process has its own
AuthManagerrefresh semaphore. A successful response is overlaid onto the then-current file and saved without checking that the file still contains the refresh token used for that request. The reproduction shows one way Codex can reach the same stale-token state described here; it does not establish the cause of the reported macOS desktop behavior.Empty reads during the in-place write
Across the five
lenient --observerounds (N=16), a parallel reader saw an emptyauth.json0, 0, 1, 4, and 3 times (8 total), and never saw a partial file; valid-read counts are in the table. A later N=8 round saw none (empty=0, partial=0, valid=32422), so this is intermittent.codex-rs/login/src/auth/storage.rs:193-211parses the file and returns an error for empty contents;codex-rs/login/src/auth/manager.rs:2149-2152,2178-2183documents and implements swallowing auth-load errors intoNone, so a process loading the file at that instant has no cached auth and appears logged out (user-visible outcome inferred from code).Reproduction
Tested with the installed
codex-cli 0.162.0on Linux aarch64, against source revision97239fd. Requires Bash and Python 3 standard library only. It creates a fresh temporaryCODEX_HOME, uses fake tokens, routes the token endpoint throughCODEX_REFRESH_TOKEN_URL_OVERRIDE, and routes ChatGPT and model-provider traffic to127.0.0.1. It needs no account and no network beyond127.0.0.1.The config sets both
chatgpt_base_urlandopenai_base_url: the former does not by itself override the built-in model provider's URL in this CLI. The authority delays each token response by 0.3 seconds by default so concurrent grants overlap; use--delay SECONDSto change it.Copy these two files into an empty directory; the command above reproduces the strict-family case. Add
--observeto collect concurrent file reads:fake_authority.pyrepro.shResults
Every row used the installed
codex-cli 0.162.0on Linux aarch64 and the source checkout at97239fd. The observer column is empty/partial/valid reads. The strict and strict-family usability probes are each one extra authority request, excluded from Codex grant counts. All Codex processes exited 1 on the fake backend; no permanent refresh-error line appeared in captured stdout/stderr. In the three--observerounds with only 5, 7, and 6 grants of 16, saved outputs do not record what the other processes loaded, so the cause is unverified; a likely explanation is that later starters loaded an already-refreshed, unexpired access token and skipped refresh (inferred).Evidence in the current code
codex-rs/login/src/auth/manager.rs:214definesCODEX_REFRESH_TOKEN_URL_OVERRIDE;manager.rs:1727-1729uses it for the refresh endpoint.codex-rs/login/src/oauth/error.rs:121-151parses the nestederror.code.manager.rs:1655-1667,1680-1707treats HTTP 401 andrefresh_token_reused/refresh_token_invalidatedas permanent failures and selects the corresponding sign-in-again message.manager.rs:2057,2214,2845-2878shows the refresh semaphore belongs to oneAuthManager; it cannot coordinate separate processes.manager.rs:1600-1622,3087-3105reloads auth after the response, overlays the returned token fields, saves, and reloads the cache without checking that the stored refresh token still matches the grant.codex-rs/login/src/auth/storage.rs:193-211parses the file;storage.rs:214-230opens it withtruncate(true), then writes and flushes in place.codex-rs/login/src/gateway_auth_storage.rs:19-49has a stable sidecarFile::try_lock()pattern.codex-rs/secrets/src/local.rs:301-377has a temp-file,sync_all, and rename helper; its Windows fallback removes the destination before rename (local.rs:345-365), so auth storage needs replacement semantics that preserve the old file until the new file is ready.codex-rs/config/src/config_toml.rs:415-416defineschatgpt_base_url;codex-rs/core/src/config/mod.rs:3864-3876appliesopenai_base_urlto the built-in provider.Expected behavior and proposed fix
File::try_lock()pattern.0600,write_all,sync_all, and an atomic replacement; use the same lock for logout and other persistent auth writes.Regression test
These files can become a regression test: launch independent workers against the same fake authority and
CODEX_HOME, then assert one grant per stored refresh token and that the final file contains the authority's current token.