Desktop app: OAuth session_stale_relogin latches and force-logs-out active sessions
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.
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
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.
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:
Additional impact not covered in the original report — Remote Control dies silently:
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.
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
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: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:
Latch duration is much longer than the ~8 minutes originally reported
Measured from the first
authorize rejectedtoclearing 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 rotatedline, 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 rejectedof each episode22 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
/authorizescope-expansion rejection — and it is still failing on 2.2553.1.sessionKey re-inserted with known-stale value; skipping bridge revive on zombie re-stampsuggests the client re-stamps a value it already knows the server rejected, instead of escalating to an interactive login.``
``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
Happy to provide fuller logs or run a build with extra auth tracing.
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*.logpoint at a specific trigger: a refresh request interrupted by a network change, followed byinvalid_granton 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_grantdied 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):
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/authorizewith the session cookie is then refused assession_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_CHANGEDin 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
sessionKeyis still valid, allow/authorizeto re-mint the same scopes without the freshness requirement — it's not really a "scope expansion".Happy to provide full logs privately if useful.