Desktop app: OAuth session_stale_relogin latches and force-logs-out active sessions

Status Open
Maintainer reply None cached
Activity 6 comments · opened Sep 9, 2026

App: Claude desktop app (macOS), version 1.49585.0
OS: macOS (Darwin 25.6.0)

Summary

Claude Code sessions in the desktop app get force-logged-out because the OAuth token refresh flow fails with invalid_grant, and the subsequent fresh /authorize attempt is rejected by the server with 403 session_stale_relogin ("sessionKey is valid but too old for the requested scope expansion"). This forces a full re-login even though the user had an active, valid login session.

Observed behavior (from ~/Library/Logs/Claude/main.log)

[info] [oauth] CLI requested token refresh after 401
[info] [oauth-v2] refreshing token for orgId=<redacted>
[error] OAuth token refresh failed: status=400, response=<json error=invalid_grant len=72>
[info] [oauth-v2] refresh rejected (auth_error) for orgId=<redacted>; tombstoning and falling through
[info] [oauth-v2] performing fresh /authorize for orgId=<redacted>
[info] oauth failed: authorize returned 403 <json error=permission_error code=session_stale_relogin len=238>
[info] oauth authorize rejected with session_stale_relogin; sessionKey is valid but too old for the requested scope expansion
[warn] [CCD CycleHealth] <session> api_error (success): Failed to authenticate. API Error: 401 OAuth access token has expired. Re-authenticate to continue.

Once this fires, the failure "latches" — every session warm-up attempt short-circuits with the same session_stale_relogin error (short-circuiting fresh /authorize on latched session_stale_relogin) until it clears on its own several minutes later (clearing latched session_stale_relogin failures). In this occurrence it took about 8 minutes (06:00:04 → 06:08:40) and knocked out every active local session in the meantime.

I also found a much larger burst of the same invalid_grant/session_stale_relogin pair (~116 occurrences) in an older rotated log (main4.log, ~2026-08-22), suggesting this is a recurring issue rather than a one-off.

What appears to be happening

The failing /authorize lookup requests a wider OAuth scope set (user:inference user:file_upload user:profile user:sessions:claude_code) than the existing sessionKey was granted under. It looks like an app update started requiring an additional scope, but the client doesn't proactively prompt for a fresh interactive login when this happens — it just keeps retrying silently, fails, and logs the user out with all their active local sessions torn down.

Expected behavior

When the server determines the current sessionKey can't cover a newly-required scope, the client should prompt for a clean re-login immediately (or at least surface a clear "please sign in again" state) instead of latching into repeated silent-refresh failures that tear down active sessions for several minutes.

Local factors ruled out

  • System clock is in sync (~0.13s offset from Apple's time server) — not a clock-skew issue.
  • App was already on the latest available build when this occurred.

View original on GitHub ↗

3 Comments

Chief-Detektor · 25 days ago

Same failure on Linux desktop (Arch, app 2.110.0, claude-code 2.1.271) — but the latch did not self-clear. It held for 10.5 hours and only released on a manual /login.

Trigger: system wake after ~8 h suspend, concurrent with a network change.

[LAM] Timeout detector: tick arrived 29581s late — treating as system wake
[oauth] network error on fetch, flushing connection pool and retrying: net::ERR_NETWORK_CHANGED
OAuth token refresh failed (auth_error status=400 detail=<json error=invalid_grant len=85>)
[oauth-v2] refresh rejected (auth_error) for orgId=<redacted>; tombstoning and falling through
[oauth-v2] performing fresh /authorize for orgId=<redacted>
oauth failed: authorize returned 403 <json error=permission_error code=session_stale_relogin len=238>
oauth authorize rejected with session_stale_relogin; sessionKey is valid but too old for the requested scope expansion

The scope expansion in question was user:plugins (3 plugins enabled). Note it was requested as part of a routine wake-up token refresh — a privilege expansion riding along on an ordinary refresh, which turns a recoverable operation into a hard failure.

Duration of the latch — 22 consecutive short-circuits, every 30 min, 10:15 → 20:45:

[oauth-v2] short-circuiting fresh /authorize on latched session_stale_relogin for cacheKey=<redacted>
[oauth] CLI-requested token refresh failed (session_stale_relogin detail=<text len=80>)

Additional impact not covered in the original report — Remote Control dies silently:

10:15:25 [remote-control] bridge_state: "failed" — "Claude.ai login expired — run /login to restore Remote Control"
   ... identical line repeated every 30 min for 10.5 h ...
21:15:25 [remote-control] bridge_state: "connected"

The user was away from the machine. No notification was raised at any point despite agentPushNotifEnabled: true. A remote session that had been running was killed at 10:15 and the machine reported nothing — the failure was only discovered on returning ~11 h later.

Suggested handling: the latch stores a state that only a user action can resolve, so it should surface immediately (notification / in-app banner) rather than being retried silently. Self-clearing after 8 minutes as in the macOS report masks this; on Linux it did not clear at all.

Recovery was additionally blocked by the login callback being dropped — filed separately as #95599.

Alex-AXO · 24 days ago

Same on macOS, and still reproducing on the latest desktop build 2.2553.1 (2026-09-18). Adding 45 days of data in case the timings and the exact scope set help.

Environment

  • Claude Desktop 2.2553.1 (arm64), installed 2026-09-18
  • macOS 26.7 (25G229), Apple M4 Pro
  • Individual account, single org
  • Heavy Code tab / Cowork user, app typically running all day
  • Timestamps below are local machine time, UTC+7

Symptom

The "For your security, sign in again to keep using Claude" banner appears every 1–3 days. Chat itself keeps working the whole time, which matches what the server says: the sessionKey is valid, only the scope expansion is refused.

Log excerpt

~/Library/Logs/Claude/main.log, ids masked:

[oauth] looking up token for orgId=<org-uuid>,
        cacheKey=acct:<acct-uuid>|<device-uuid>:<org-uuid>:https://api.anthropic.com:user:inference user:office
[oauth] using cached token for orgId=<org-uuid>
...
oauth failed: authorize returned 403 <json error=permission_error code=session_stale_relogin len=238>
oauth authorize rejected with session_stale_relogin; sessionKey is valid but too old for the requested scope expansion
[oauth-v2] fresh exchange failed for orgId=<org-uuid> (session_stale_relogin)
[sessions-bridge] Cowork OAuth stale-session (session_stale_relogin); parking bridge until re-login

The scope set being requested on this machine is user:inference user:office.

Recovery happens one of two ways — either a manual re-login, or the latch clearing by itself:

[oauth] clearing latched session_stale_relogin failures
[oauth] sessionKey re-inserted with known-stale value; skipping bridge revive on zombie re-stamp
[sessions-bridge] sessionKey rotated; restarting bridge after session_stale_relogin park

Latch duration is much longer than the ~8 minutes originally reported

Measured from the first authorize rejected to clearing latched session_stale_relogin failures:

| Episode | Latched for |
|---|---|
| 2026-09-14 07:16:29 → 07:56:28 | ~40 min |
| 2026-09-17 16:47:17 → 20:30:12 | ~3 h 43 min |
| 2026-09-18 04:13:17 → 08:58:05 | ~4 h 45 min |
| 2026-09-18 13:43:34 → 14:58:12 | ~1 h 15 min |

In several of these episodes the latch cleared without an accompanying sessionKey rotated line, i.e. it self-recovered rather than being fixed by a re-login. So the failure is neither reliably self-healing nor reliably fatal, which makes the banner look arbitrary from the user's side.

Timeline — first authorize rejected of each episode

2026-08-04 13:18:28      2026-08-28 22:35:50
2026-08-06 08:46:42      2026-08-29 10:25:41
2026-08-12 05:57:25      2026-08-31 22:53:38
2026-08-13 16:37:39      2026-09-02 01:52:20
2026-08-14 08:22:35      2026-09-02 16:23:26
2026-08-18 19:20:03      2026-09-11 09:35:49
2026-08-21 16:22:30      2026-09-11 10:46:05
2026-08-24 13:11:13      2026-09-14 07:16:29
2026-08-26 21:02:17      2026-09-16 23:58:25
                         2026-09-17 14:17:55
                         2026-09-17 16:47:17
                         2026-09-18 04:13:17
                         2026-09-18 13:43:34

22 episodes in 45 days. Typical spacing is 24–72 h, consistent with the intervals reported in #81512 and #87950. (No events recorded 2026-09-03 … 2026-09-10; the machine saw little use in that window.)

Additional observations

  1. The recent auth fixes do not cover this path. 2.2553.0 (2026-09-17) fixed background renewal of an expired OAuth token, and 2.110.0 (2026-09-15) fixed re-login prompts after a single network error during renewal. This is a third path — the /authorize scope-expansion rejection — and it is still failing on 2.2553.1.
  2. sessionKey re-inserted with known-stale value; skipping bridge revive on zombie re-stamp suggests the client re-stamps a value it already knows the server rejected, instead of escalating to an interactive login.
  3. Each forced re-login creates a new account session. After months of this, the account's Active sessions list has grown into the hundreds. That list is now effectively unusable for spotting a genuinely suspicious session, which is a security-usability regression in its own right.
  4. One re-login attempt failed at the login redirect itself, on a machine behind a VPN whose route can switch mid-request:

``
Failed to load URL: https://claude.ai/login?returnTo=%2F...%3Freason%3Delevated_auth
%26auth_kind%3Dsession_stale_relogin%26org_uuid%3D<org-uuid>&reauth=1&from=logout
{ errorCode: -21, errorDescription: 'ERR_NETWORK_CHANGED' }
``
Worth handling with a retry, since the forced logout has already torn down the session by that point.

Ask

  • Satisfy the elevated scope requirement with a scoped step-up rather than a full account re-login, or extend the session freshness window for a device that has already been trusted.
  • If a re-login genuinely is required, prompt immediately instead of latching and silently retrying for hours.
  • Don't mint a brand-new account session for what is effectively a scope refresh of an existing trusted device.

Happy to provide fuller logs or run a build with extra auth tracing.

sombandy · 24 days ago

Same on macOS, still reproducing on 2.2553.1 (macOS 26.6.2, M3 Max, laptop that sleeps/wakes through the day, Google SSO login). Forced re-login on Aug 25, Sep 5, 8, 12, 16, 18, 20 — now every ~2 days. Adding this because 7 weeks of main*.log point at a specific trigger: a refresh request interrupted by a network change, followed by invalid_grant on the next attempt — i.e. it looks like a lost-response race on refresh-token rotation.

The pattern

In 8 of the 10 distinct incidents, the refresh attempt immediately before the first invalid_grant died with a network error (almost always on wake from sleep):

| Refresh hit network error | Next refresh → invalid_grant |
|---|---|
| Aug 07 22:56:12 ERR_NETWORK_CHANGED | Aug 07 22:57:50 |
| Aug 19 10:25:44 ERR_NETWORK_CHANGED | Aug 19 13:43:45 |
| Aug 25 19:23:02 ERR_NETWORK_CHANGED | Aug 25 19:46:40 |
| Sep 07 00:35:33 ERR_NAME_NOT_RESOLVED | Sep 07 08:08:14 |
| Sep 12 07:08:39 ERR_NETWORK_CHANGED | Sep 12 07:26:30 |
| Sep 16 04:13:33 ERR_NETWORK_CHANGED (in-request retry) | Sep 16 04:13:34 |
| Sep 18 00:32:48 ERR_NETWORK_CHANGED | Sep 18 08:24:49 |
| Sep 19 09:08:52 ERR_NETWORK_CHANGED | Sep 19 09:14:18 |

Representative chain (ids redacted):

09-19 09:08:52 [info]  [oauth-v2] refreshing token for orgId=<redacted>
09-19 09:08:52 [warn]  [oauth] network error on fetch, flushing connection pool and retrying: net::ERR_NETWORK_CHANGED
09-19 09:08:52 [error] OAuth token refresh failed (network_error detail=net::ERR_NETWORK_CHANGED)
09-19 09:08:52 [info]  [oauth-v2] transient refresh failure (network_error) for orgId=<redacted>; entry left for retry
09-19 09:14:17 [info]  [oauth-v2] refreshing token for orgId=<redacted>
09-19 09:14:18 [error] OAuth token refresh failed (auth_error status=400 detail=<json error=invalid_grant len=85>)
09-19 09:14:18 [info]  [oauth-v2] refresh rejected (auth_error); tombstoning and falling through
09-19 09:14:18 [info]  [oauth-v2] performing fresh /authorize for orgId=<redacted>
09-19 09:14:18 [info]  oauth failed: authorize returned 403 <json error=permission_error code=session_stale_relogin len=238>
09-19 09:14:18 [info]  oauth authorize rejected with session_stale_relogin; sessionKey is valid but too old for the requested scope expansion

Older builds logged the body unredacted: {"error": "invalid_grant", "error_description": "Refresh token not found or invalid"}.

Hypothesis

The "transient" refresh request actually reaches the server, which rotates the refresh token and invalidates the old one; the response is lost to the network change. The client correctly treats it as transient and keeps the old token ("preserving chain" / "entry left for retry") — but that token is now dead server-side, so the retry gets invalid_grant. The fallback /authorize with the session cookie is then refused as session_stale_relogin, so a single dropped HTTP response ends in a full browser re-login.

A sleeping laptop makes this nearly inevitable: this machine logged ~1,500 ERR_NETWORK_CHANGED in 10 days (dark-wake network re-attach), and the renewal timer tends to fire exactly at wake. (2 of the 10 incidents had no visible network error beforehand, so there may be a second trigger as well.)

Suggested fixes

  • Server: grace window / idempotent reuse for the just-rotated refresh token (accept the previous token for a short period, or return the same new pair on replay), which is standard practice for rotating refresh tokens.
  • Server or client: when the refresh chain is lost but the sessionKey is still valid, allow /authorize to re-mint the same scopes without the freshness requirement — it's not really a "scope expansion".
  • Client: defer the post-wake renewal until the network has been stable for a few seconds rather than firing on the wake tick.

Happy to provide full logs privately if useful.

Showing cached comments. Read the full discussion on GitHub ↗