[BUG] Multiple concurrent local sessions race on OAuth refresh-token rotation → near-daily forced /login

Status Open
Reported on v2.1.218
Maintainer reply None cached
Activity 0 comments · opened Jul 23, 2026

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.com serves a genuine Let's Encrypt cert; api.anthropic.com a 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:

  1. All local claude processes 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.
  2. 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

  1. Sign in via subscription OAuth (macOS keychain).
  2. Start 2+ claude sessions from separate shells and leave them idle/alive.
  3. Wait past the access-token lifetime (~8h) so more than one session attempts a refresh around the same window.
  4. 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-mem plugin 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-launched claude sessions.
  • 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_DIR does not relocate them on macOS, and claude setup-token yields an inference-only token (missing user:profile / user:mcp_servers scopes) so it can't be used globally without breaking claude.ai connectors.

View original on GitHub ↗