[BUG] Multiple concurrent local sessions race on OAuth refresh-token rotation → near-daily forced /login
Bug: multiple concurrent local sessions race on OAuth refresh-token rotation → near-daily forced /login
Environment
- Claude Code: 2.1.218
- OS: macOS 26.5.1 (Darwin 25.5.0), Apple Silicon
- Auth: subscription (Team plan,
default_claude_max_5x), OAuth credential in macOS Keychain (Claude Code-credentials) - Not a proxy issue: a corporate Zscaler CA is installed but is not intercepting Anthropic endpoints on the affected network (
platform.claude.comserves a genuine Let's Encrypt cert;api.anthropic.coma genuine Google Trust cert). Ruled out.
Summary
When several claude processes are alive concurrently for longer than the access-token lifetime (~8h), they independently attempt to refresh the single, shared keychain OAuth credential. Anthropic's refresh grant appears to be single-use (it rotates the refresh token), so the first refresher succeeds and the others fail with an invalid_grant-class error. The failure clears the keychain credential, which forces every session — including the active foreground one — to re-authenticate. Result: a forced /login roughly once a day.
The daemon-level refresh coordination that exists (there is clearly a lock — see logs) does not cover independently-launched foreground sessions (e.g. multiple claude / claude --dangerously-skip-permissions invocations started from different shells over a workday).
Impact
Users who leave multiple long-lived sessions open (a normal workflow — background/--dangerously-skip-permissions agents, resumed sessions, several terminals) are logged out about once per day with no user error. Re-login is disruptive and interrupts in-flight work across all sessions at once.
Evidence
1. Daemon log shows the lock race + a refresh failure that demands re-auth
[2026-07-04T00:02:25.190Z] [supervisor] another daemon won the lock race (pid=48098) — exiting
[2026-07-04T08:14:35.601Z] [supervisor] auth: proactive refresh failed, signalling re-auth required
[2026-07-04T08:14:35.610Z] [supervisor] auth: headless daemon cannot complete OAuth — run `claude auth login` to refresh
[2026-07-04T08:14:35.611Z] [supervisor] auth: no token found, will re-check keychain every 30s
The supervisor has a lock (good), but the failure path ends in the credential being unusable and re-auth required.
2. Instrumented capture of an actual logout (keychain credential wiped)
A 30s poller hashing the keychain credential and snapshotting concurrent processes caught the moment of a logout. The refresh token went from a valid value to empty, and expiresAt to 0 — i.e. the credential was cleared — while two independently-launched foreground claude sessions were alive:
[2026-07-23 04:50:32] refresh token: eb6c0ca376d7 -> (empty), expiresAt: <valid> -> 0
concurrent claude processes at the wipe:
pid=13275 claude --dangerously-skip-permissions -r (up 7h52m)
pid=29138 claude --dangerously-skip-permissions (up 7h51m)
+ background helper/MCP processes
Neither foreground session reported an error to the user beyond the subsequent /login prompt.
Expected behavior
Either of:
- All local
claudeprocesses share the refresh coordination, not just daemon supervisors — a machine-wide lock so exactly one process performs a rotation and the others adopt the rotated token (re-read the keychain) instead of failing. - A failed refresh does not clear/invalidate the stored credential when a valid rotated credential exists — a losing racer should re-read the keychain and retry with the winner's freshly written token rather than wiping it and forcing global re-auth.
Actual behavior
A losing refresh races to invalid_grant, the keychain credential is cleared, and every session is forced to /login.
Reproduction
- Sign in via subscription OAuth (macOS keychain).
- Start 2+
claudesessions from separate shells and leave them idle/alive. - Wait past the access-token lifetime (~8h) so more than one session attempts a refresh around the same window.
- Observe one session's refresh fail and the keychain credential get cleared → all sessions prompt for
/login.
Notes / ruled out
- Not caused by a third-party plugin. The
claude-memplugin was suspected but exonerated: it injects a static access token into the SDK children it spawns and refuses to inject expired tokens, so it never performs a refresh/rotation. The racers are ordinary user-launchedclaudesessions. - macOS has no per-process credential isolation to work around this from the user side: credentials are keychain-only under a fixed service name,
CLAUDE_CONFIG_DIRdoes not relocate them on macOS, andclaude setup-tokenyields an inference-only token (missinguser:profile/user:mcp_serversscopes) so it can't be used globally without breaking claude.ai connectors.