Skip to content

[macOS] Freshly issued refresh token rejected as refresh_token_invalidated ~10s later, reproduced with a single local process (trigger: opening a thread) #40541

Open

Description

@Suhaibinator

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 with refresh_token_invalidated.
  • Reproduced with one local codex process 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 token means the guarded reload returned ReloadedNoChange, i.e. auths_equal_for_refresh was true — for ChatGPT that compares the full auth.json contents (manager.rs:2471-2473). Cache and disk were byte-identical.
  • The cache was loaded at :07 from the file written by the :06 exchange (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 logs Refreshing token first (manager.rs:2818). No such line exists from this pid between :07 and :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 whenever refreshToken:true is passed — no freshness check, even though the refreshToken:false path already gates on should_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 as None (manager.rs:2559-2560).
  • Stranded after account switch. A process whose cached account no longer matches auth.json hits ReloadOutcome::Skipped forever (manager.rs:2446-2452) — 128 occurrences here — and the error is never recorded, so getAuthStatus keeps reporting the stale account.
  • Three account ids were active on this machine that day; every failing cycle postdates the third. Correlation only (cf. Codex Windows: fresh OAuth refresh token immediately invalidated when two ChatGPT accounts are signed in on the web #39199).

What steps can reproduce the bug?

  1. Sign in to Codex Desktop on macOS.
  2. Open an existing thread.
  3. 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):

  1. Log auth_changed_for_refresh next to changed (manager.rs:2581) — one line.
  2. Make refreshToken:true honor should_refresh_proactively (or a short min interval) instead of always POSTing.
  3. temp-file+rename in FileAuthStorage::save; file lock around refresh→persist.
  4. On Skipped, surface an "account switched — sign in again" state instead of stranding the process.
  5. 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:

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.

Activity

  1. added
    bugSomething isn't working
    appIssues related to the Codex desktop app
    app-serverIssues involving app server protocol or interfaces
    authIssues related to authentication and accounts
    on Aug 25, 2026
  2. github-actions commented on Aug 25, 2026

    @github-actions
    Contributor
  3. changed the title [-][macOS] app-server does not adopt tokens from its own OAuth exchange ("Reloaded auth, changed: false"), then refreshes with the stale token → refresh_token_invalidated within seconds[/-] [+][macOS] Freshly issued refresh token rejected as refresh_token_invalidated ~10s later, reproduced with a single local process (trigger: opening a thread)[/+] on Aug 25, 2026
  4. Suhaibinator commented on Aug 25, 2026

    @Suhaibinator
    Author

    @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, and auths_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_invalidated with 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 every getAuthStatus{refreshToken:true}) would at least shrink the blast radius.

    Happy to share the full log extract privately or test a build. Thanks.

  5. Aodaruma commented on Aug 25, 2026

    @Aodaruma

    Independent reproduction on the same current build, with an additional trigger and endpoint-level evidence.

    Environment:

    • ChatGPT desktop 26.818.61809 (build 7019), bundled codex-cli 0.149.0-alpha.4.3
    • macOS 15.7.3 (24G419), Apple Silicon
    • Local app server only (hostId=local); no Remote Control host involved

    I reproduced the signed-out transition twice today:

    1. Fresh login completed at 07:40:57Z; opening Quick Chat at 07:46:04Z was followed by GET /backend-api/accounts/:param/settings returning 401 with hadToken=true, then hasToken=false / auth_token_missing at 07:46:06Z.
    2. Fresh login completed at 12:18:34Z; an ordinary local thread/resume succeeded at 12:24:02Z, followed by maybe_resume_success, then the same account-settings 401 at 12:24:03Z. The forced OAuth refresh returned refresh_token_invalidated, and the app changed to auth_token_missing at 12: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-5bf209a873dd
    • cf-ray: a30a92506e578ec0-NRT

    This 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_missing while 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.

  6. Suhaibinator commented on Aug 25, 2026

    @Suhaibinator
    Author

    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 → 200 at +3 min and +6 min after a refresh_token_invalidated 401), 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 of main):

    • New AuthManager::refresh_token_if_stale(): no-op when the cached ChatGPT access token isn't near expiry (reuses the existing should_refresh_proactively gate); otherwise falls through to refresh_token().
    • refresh_token_if_requested (the getAuthStatus{refreshToken:true} / account/read path) calls it instead of refresh_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 five Skipping 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):

    git clone -b local/0.149.0-alpha.4.3-gated-refresh https://github.com/Suhaibinator/codex.git
    cd codex/codex-rs && cargo build --release --bin codex      # ~8 min on M-series
    # Quit Codex fully (Cmd-Q), then:
    cp /Applications/ChatGPT.app/Contents/Resources/codex ~/codex.orig     # backup
    cp target/release/codex /Applications/ChatGPT.app/Contents/Resources/codex
    codesign -s - -f --options runtime /Applications/ChatGPT.app/Contents/Resources/codex

    Notes:

    • That branch is the exact rust-v0.149.0-alpha.4.3 tag 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-pick 823fd73f onto that tag.
    • Writing into the app bundle needs System Settings → Privacy & Security → App Management enabled for your terminal, or the cp fails with Operation not permitted. Don't use cp -f before you have that permission — it unlinks the target before the write is refused.
    • The app's auto-update will restore the official binary. Restore manually with cp ~/codex.orig /Applications/ChatGPT.app/Contents/Resources/codex.
    • You'll still need one real refresh every ~8 days (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, with openai-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's desktop-auth getAuthStatus immediately after login does not force a refresh; the one fired ~1 s after thread/resume does — so the forced rotation is specifically tied to opening a thread.

  7. bazhahei123 commented on Aug 26, 2026

    @bazhahei123

    I can reproduce what appears to be the same issue on Windows.

    Environment

    • Windows
    • Codex Desktop: 26.820.60940
    • Codex core: 0.150.0

    My logs showed a successful OAuth token exchange (200 OK), followed shortly afterwards by:

    Refreshing token → 401 Unauthorized → refresh_token_invalidated

    Simply deleting ~/.codex/auth.json and 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:

    1. Fully quit Codex Desktop.
    2. Sign out of ChatGPT in the browser.
    3. Delete ~/.codex/auth.json.
    4. Start Codex Desktop and Sign in.
    5. Complete OAuth authentication again.

    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.sqlite logs from the failing state and can provide additional timestamps/log entries if useful.

  8. s1n9t9b9r commented on Aug 26, 2026

    @s1n9t9b9r

    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 → 200 at +3 min and +6 min after a refresh_token_invalidated 401), 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 of main):

    • New AuthManager::refresh_token_if_stale(): no-op when the cached ChatGPT access token isn't near expiry (reuses the existing should_refresh_proactively gate); otherwise falls through to refresh_token().
    • refresh_token_if_requested (the getAuthStatus{refreshToken:true} / account/read path) calls it instead of refresh_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 five Skipping 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):

    git clone -b local/0.149.0-alpha.4.3-gated-refresh https://github.com/Suhaibinator/codex.git
    cd codex/codex-rs && cargo build --release --bin codex # ~8 min on M-series

    Quit Codex fully (Cmd-Q), then:

    cp /Applications/ChatGPT.app/Contents/Resources/codex ~/codex.orig # backup
    cp target/release/codex /Applications/ChatGPT.app/Contents/Resources/codex
    codesign -s - -f --options runtime /Applications/ChatGPT.app/Contents/Resources/codex
    Notes:

    • That branch is the exact rust-v0.149.0-alpha.4.3 tag 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-pick 823fd73f onto that tag.
    • Writing into the app bundle needs System Settings → Privacy & Security → App Management enabled for your terminal, or the cp fails with Operation not permitted. Don't use cp -f before you have that permission — it unlinks the target before the write is refused.
    • The app's auto-update will restore the official binary. Restore manually with cp ~/codex.orig /Applications/ChatGPT.app/Contents/Resources/codex.
    • You'll still need one real refresh every ~8 days (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, with openai-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's desktop-auth getAuthStatus immediately after login does not force a refresh; the one fired ~1 s after thread/resume does — so the forced rotation is specifically tied to opening a thread.

    Thank you @Suhaibinator , it worked for me.

  9. sanieakhtar commented on Aug 26, 2026

    @sanieakhtar

    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

  10. Suhaibinator commented on Aug 26, 2026

    @Suhaibinator
    Author

    This is literally a P0 (or at least P1) bug, why aren't they prioritizing this...

  11. etsabary commented on Aug 26, 2026

    @etsabary

    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.

  12. HaroldZhong commented on Aug 26, 2026

    @HaroldZhong

    I can reproduce what appears to be the same issue on Windows.

    Environment

    • Windows
    • Codex Desktop: 26.820.60940
    • Codex core: 0.150.0

    My logs showed a successful OAuth token exchange (200 OK), followed shortly afterwards by:

    Refreshing token → 401 Unauthorized → refresh_token_invalidated

    Simply deleting ~/.codex/auth.json and 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:

    1. Fully quit Codex Desktop.
    2. Sign out of ChatGPT in the browser.
    3. Delete ~/.codex/auth.json.
    4. Start Codex Desktop and Sign in.
    5. Complete OAuth authentication again.

    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.sqlite logs from the failing state and can provide additional timestamps/log entries if useful.

    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.

  13. metacircu1ar commented on Oct 7, 2026

    @metacircu1ar

    I reproduced the FileAuthStorage::save torn-read hazard mentioned here with a deterministic regression test. Saving shorter credentials while another reader has the old file open produces invalid JSON.
    Proposed fix and tests: [metacircu1ar/codex#1](metacircu1ar#1).

    The fix uses atomic file replacement. All 256 tests pass in release mode on macOS; Windows compilation passes, but native Windows execution remains unverified.

    This addresses file publication only—it does not establish the cause of the reported server-side token invalidation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    appIssues related to the Codex desktop appapp-serverIssues involving app server protocol or interfacesauthIssues related to authentication and accountsbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions