[BUG] macOS: OAuth login revoked ~weekly when long-lived processes on different versions share one Keychain credential (still reproduces 2.1.296)

Status Open
Reported on v2.1.296
Maintainer reply None cached
Activity 0 comments · opened Oct 12, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

What's Wrong?

On macOS (login Keychain), my subscription login gets revoked roughly once a week and every Claude Code process on the machine is forced to /login. It is the same bug class as #91708 (Windows / file store), #24317 (macOS, closed as completed) and #76905 (macOS, auto-closed), so this is a report that it still reproduces on macOS in 2.1.296, with a full daemon log covering four months.

What may be new here is version skew between long-lived processes that share one credential. When the latest failure happened, these were all alive and sharing the same Claude Code-credentials Keychain item:

| Process | Version | Running since |
|---|---|---|
| VS Code extension native binary | 2.1.270 | 2026-09-15 (27 days) |
| claude bg-pty-host / --bg-spare (daemon spare) | 2.1.278 | 2026-09-20 (22 days) |
| CLI session in tmux (--resume) | 2.1.296 (symlink) | 2026-10-08 |
| claude daemon run supervisor | 2.1.296 | 2026-10-10 (self-restarted on upgrade) |
| claude CLI sessions | 2.1.296 | various |

The refresh-coordination code differs across those builds. In the shipped binaries, the counts of refresh_lock strings go 26 → 26 → 31 and tengu_oauth_token_refresh_race goes 3 → 3 → 4 between 2.1.270, 2.1.278 and 2.1.296. That suggests a 2.1.296 process and a weeks-old 2.1.270 process may not honour the same lock or compare-and-swap protocol. The daemon self-restarts on upgrade, but the VS Code extension binary and the bg spares do not, so a machine that is never rebooted keeps several lock implementations racing on one rotating refresh token indefinitely.

I can't prove the skew is the trigger (see "What I couldn't determine"), but it is the one thing in this setup that the earlier reports don't cover.

Evidence: ~/.claude/daemon.log

The daemon does a proactive refresh about every 8 hours (scheduling proactive refresh in 28560s). Over the log's lifetime there were 362 successful refreshes, 53 token still valid (cross-process refresh or not yet due) (another process won, handled correctly), and 10 hard failures:

failure (UTC)               time from "refresh starting" to "failed"
2026-06-20T05:11:05Z            50 ms
2026-06-30T11:39:28Z            52 ms
2026-07-20T14:24:59Z           401 ms
2026-08-06T19:44:26Z         34162 ms   (3 retries, "token still valid": network-shaped)
2026-08-13T08:20:24Z          1652 ms
2026-08-17T10:34:23Z           257 ms
2026-09-14T04:47:06Z            44 ms
2026-09-14T16:07:19Z            51 ms   (5 s after a self-restart for upgrade 2.1.263 → 2.1.270)
2026-10-06T15:56:40Z            40 ms
2026-10-12T21:14:14Z            53 ms

Six of the ten fail in 40–53 ms. Those look like either a fast server-side rejection on a warm connection or a local decision not to call the server at all. Either way, retrying doesn't help and the login is lost.

The most recent one, verbatim:

[2026-10-12T13:19:04.345Z] [supervisor] auth: proactive refresh starting
[2026-10-12T13:19:04.369Z] [supervisor] auth: token still valid (cross-process refresh or not yet due)
[2026-10-12T21:14:14.588Z] [supervisor] auth: proactive refresh starting
[2026-10-12T21:14:14.641Z] [supervisor] auth: proactive refresh failed, signalling re-auth required
[2026-10-12T21:14:14.648Z] [supervisor] auth: headless daemon cannot complete OAuth — run `claude auth login` to refresh
[2026-10-12T21:14:14.649Z] [supervisor] auth: no token found, will re-check keychain every 30s
[2026-10-12T21:43:44.765Z] [supervisor] auth: token found via keychain re-check   ← after manual /login

The Keychain item's mdat was 2026-10-12T13:18:14Z, so a different process rotated the token 50 s before the daemon's 13:19 check, which correctly deferred. Nothing wrote the item again before the 21:14 failure.

The Keychain was unlocked at the moment of failure. securityd logs KCdb … is unlocked at 21:14:14.625Z, 16 ms before the daemon reported failure, and on every 30 s re-check afterwards. So this is not "Keychain locked". The Keychain is configured no-timeout with no lock-on-sleep.

Upgrades are a red herring (but they feel like the cause)

As a user it feels like "every update logs me out", but the log says otherwise: about 30 self-restarts for upgrade since July and only one failure adjacent to an upgrade (2026-09-14). The failures land on the routine 8-hour refresh. An update is simply when you restart or open a new session and notice. Worth knowing when triaging similar "logged out after update" reports.

Secondary UX bug: misleading error on re-login

After the revocation, /login failed with:

OAuth error: Couldn't save your login. If your Mac's keychain is locked, unlock it and log in again.

This sent me down a "keychain is locked" path that had nothing to do with the root cause. (security unlock-keychain + /login did work, but the revocation itself happened with the keychain unlocked.) If the save failure has a specific OSStatus, surfacing it would help.

What Should Happen?

  • A refresh won by one process must never get the token family revoked for the others, including across versions. If the lock or CAS protocol changes, older live processes must still be excluded. One option is a versioned lock that old binaries also respect. Another is for new binaries to treat an unknown-protocol peer as "don't refresh; re-read the Keychain".
  • Long-lived non-daemon processes (VS Code extension binaries, bg-spare / bg-pty-host) should not run for weeks on an old auth stack after an upgrade, or should defer refresh to the daemon.
  • The daemon log should record why a refresh failed: HTTP status / error code (invalid_grant?) or "no refresh token present". As it stands I can't tell a server revocation from a local short-circuit.

Steps to Reproduce

Not deterministic for me (about once a week). The setup that produces it:

  1. macOS, subscription login (Keychain storage). Native installer CLI with auto-update, the background daemon running, and the VS Code extension open.
  2. Keep VS Code and some claude sessions open for weeks without rebooting while the CLI auto-updates several times. You end up with processes on 3+ different builds sharing one Keychain credential (table above).
  3. Wait. About every 1–3 weeks, a proactive refresh fails in ~50 ms and every process needs /login.

A tighter repro for someone with access to the token endpoint logs: start a 2.1.270 process and a 2.1.296 process on the same account, then force both near access-token expiry (e.g. by editing expiresAt in the stored credential) and check whether the old build presents an already-rotated refresh token.

What I couldn't determine

  • Whether the 40–50 ms failures are a server invalid_grant (token-family revocation) or a local short-circuit. The daemon log doesn't say.
  • Which process consumed the refresh token at 13:18:14Z. The Claude Desktop app's embedded Claude Code (2.1.295) appears to use its own token cache ([oauth-v2] using cached token in ~/Library/Logs/Claude/main*.log), so I don't believe it participates.

Workarounds tried

  • claude setup-token / CLAUDE_CODE_OAUTH_TOKEN: not viable. As #91708 notes, it is inference-only, and I rely on Remote Control from my phone.
  • Restarting all long-lived processes after each upgrade: about to try this; I'll report back whether the failures stop.

Is this a regression?

Unknown. The failures go back to at least 2026-06-20 (start of the daemon log's auth history).

Claude Code Version

2.1.296 (CLI and daemon); also live at the time: 2.1.270 (VS Code extension native binary), 2.1.278 (bg spare), 2.1.295 (Claude Desktop embedded).

Platform

Anthropic API (Claude subscription / OAuth)

Operating System

macOS 26 (Darwin 25.5.0), Apple Silicon

Terminal/Shell

zsh; sessions in tmux, VS Code, Claude Desktop

---
Report drafted with Claude Code from the local daemon log, securityd unified log and security CLI output. No token material included.

View original on GitHub ↗