Frequent forced re-login when many concurrent sessions share ~/.claude (refresh-token rotation race?)

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

Summary

With many concurrent Claude Code CLI sessions sharing one ~/.claude/.credentials.json, the OAuth login is force-expired roughly weekly (sometimes more often), requiring /login again. Single- or low-concurrency machines on the same account are affected far less. We suspect a refresh-token rotation race between concurrent processes.

Environment

  • Claude Code 2.1.228–2.1.240 (multiple long-running sessions across versions)
  • Linux VPS (Ubuntu), 10–24 concurrent interactive CLI sessions in tmux under one OS user, shared ~/.claude
  • Second machine: macOS, normal usage, same subscription (Max)
  • Auth: claude.ai subscription OAuth via /login

Observed

  • ~/.claude/.credentials.json access token expiresAt is ~8h; the file is refreshed continuously by whichever process needs it.
  • At unpredictable intervals (≤1 week) all sessions on the machine drop to Login expired · Please run /login; freshly spawned sessions start logged out.
  • The in-terminal 3-day expiry warning is easy to miss in headless/tmux operation.

Hypothesis

Multiple concurrent processes read the same credentials file and independently attempt token refresh. If refresh tokens are rotated server-side, two near-simultaneous refreshes can invalidate the chain (the loser presents an already-rotated refresh token), killing the login for the whole machine. There appears to be no cross-process lock/serialization around the refresh in the CLI (not documented, at least).

Ask

  1. Serialize token refresh across processes (file lock around read-refresh-write), or tolerate a concurrent-refresh grace window.
  2. Document the behavior/limits for multi-session setups.
  3. A machine-readable way to query login health/expiry (for external monitoring) would help headless setups a lot.

Happy to provide timestamps/logs on request (credentials contents obviously excluded).

View original on GitHub ↗