macOS Keychain: concurrent sessions race on OAuth refresh; setup-token workaround strips claude.ai connectors, leaving no viable headless pattern

Open 💬 1 comment Opened Jul 12, 2026 by KillTheList

Summary

On macOS, multiple concurrent Claude Code processes sharing the Keychain item
Claude Code-credentials (one subscription login) intermittently force each
other logged out. The v2.1.133 / v2.1.136 fixes for the credential-refresh race
("parallel sessions all dead-ending at 401 after a refresh-token race wiped
shared credentials", "concurrent credential write could overwrite a
freshly-rotated OAuth token") appear to target the file-based
.credentials.json store; the macOS Keychain path still exhibits the race as
of v2.1.207.

Environment

  • macOS (always-on Mac mini), Claude Code v2.1.177 → reproduced across

versions; currently v2.1.207

  • Auth: subscription OAuth login stored in login Keychain

(Claude Code-credentials)

  • Workload: ~18 scheduled headless claude -p runs/day (launchd) + 1–3

long-lived interactive tmux sessions + ad-hoc interactive sessions — all one
account, one Keychain credential

Symptom

Intermittent forced logouts of interactive sessions (and, historically,
headless runs) — a session that has been idle for hours/days wakes up, fails
auth, and requires /login. Consistent with: access token expires (~5h),
several processes race to refresh, refresh tokens are single-use, the loser
presents an already-rotated refresh token → invalid_grant. Related:
#24317 (closed), #56339, #76561 (file-based variant, still open).

Observed Keychain side effect: a second Claude Code-credentials item with
acct="unknown" appeared alongside the primary (acct=<username>) item after
a re-login during a logout episode, and was never refreshed afterward.
Service-name-only queries return the stale item first — a trap for anything
reading the credential without pinning the account attribute.

Why the documented workaround doesn't work here

claude setup-token / CLAUDE_CODE_OAUTH_TOKEN isolates headless jobs from
the interactive credential — but a setup-token does not carry claude.ai
remote-connector grants (Gmail, Google Calendar, Drive, etc.). Headless runs
that depend on those connectors go silently blind: the MCP tools simply don't
exist in the session, while the run still exits 0. So for "scheduled headless
jobs that use claude.ai connectors," there is currently no supported way
to avoid sharing the interactive Keychain credential, and therefore no way to
avoid the refresh race.

Requests

  1. Serialize/harden concurrent OAuth refresh on the macOS Keychain path the

way the file-based path was hardened in v2.1.133/v2.1.136 (e.g., re-read
the Keychain before refreshing, atomic compare-and-swap on write, retry on
invalid_grant after re-read instead of dropping to logged-out).

  1. Clean up / never create duplicate Claude Code-credentials items

(acct="unknown"), and pin reads to the account attribute.

  1. Provide a supported credential-isolation story for headless claude -p

jobs that need claude.ai connector grants (setup-token with connector
scopes, or a per-process credential cache keyed off the main login).

Repro sketch

  1. macOS, subscription OAuth login (Keychain).
  2. Run a long-lived interactive session; leave it idle > token TTL.
  3. Meanwhile run hourly headless claude -p jobs (launchd) on the same login

so the credential rotates several times.

  1. Return to the idle session and issue a prompt — intermittently it

dead-ends logged out (invalid_grant path) instead of picking up the
rotated credential.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗