Credential writes replace symlinks and clobber shared state: concurrent instances sharing .credentials.json cascade each other (and the user) to logged-out

Open 💬 0 comments Opened Jul 11, 2026 by SevenX77

What happened

We run a multi-agent fleet where several Claude Code instances (4 seats) shared one ~/.claude/.credentials.json via symlinks (each instance's $HOME/.claude/.credentials.json → one shared file). Within hours of normal operation, every instance and the human user's interactive session cascaded to logged-out, repeatedly, several times a day. Filesystem forensics isolated three CLI behaviors that combine into this failure:

1. Credential writes replace symlinks with private regular files.
Claude Code persists credentials via temp-file + atomic rename. rename() replaces the directory entry itself — it never follows a symlink — so the instance's first successful token refresh silently detaches it from the shared file. Observed on native ext4 inside WSL2 (not a Windows-filesystem artifact):

# before: sandbox .claude/.credentials.json -> /root/.claude/.credentials.json (symlink)
# after one successful refresh:
REGULAR  mtime=17:58  expiresAt=1783760307680 (future)  size=471   <-- detached private file, holds the rotated chain

2. Refresh-token rotation then invalidates every other sharer.
The refresh that just succeeded rotates the refresh token; the shared source file still holds the now-consumed token. Every other instance's next refresh gets 401 Invalid authentication credentials.

3. On refresh failure, the CLI overwrites the credentials file with a logged-out stub.
The 401 losers write {"claudeAiOauth": {..., "expiresAt": 0}} (~243-281 bytes) over the credentials path — destroying whatever was there. When the loser is still symlink-attached, this write can clobber the shared source itself, logging out even the human's interactive session:

REGULAR  mtime=18:00  expiresAt=0  size=281   <-- freshly-spawned instance, 401 -> stub

Net effect with N>1 instances on one chain: first refresh event forks the chain, everyone else dies within their next refresh cycle. Access tokens expire on the order of hours → "must re-login several times a day".

Expected / suggestions (any subset helps)

  1. Write-through for symlinked credential paths (resolve the symlink and rename within the target's directory), or document that symlinked .credentials.json is unsupported.
  2. Don't destroy state on refresh failure: keep the failed credentials (or move them aside) instead of overwriting with a logged-out stub — the stub turns a maybe-transient 401 into a hard logout and, via symlinks, propagates it to other consumers.
  3. Tolerate concurrent sharers: re-read the credentials file immediately before refreshing (another process may have already rotated and persisted a newer chain), and/or take a file lock around refresh+persist. Either would make shared-file setups converge instead of cascade.
  4. Document the supported multi-instance pattern explicitly (we've since migrated to claude setup-token + CLAUDE_CODE_OAUTH_TOKEN, which works well — a doc pointer in the auth guide would save others this forensic hunt).

Environment

  • Claude Code CLI on Linux (WSL2 Ubuntu 24.04, root user, ext4 paths — symlink replacement happens on native Linux fs; macOS Keychain storage is presumably unaffected)
  • 4 concurrent instances (multi-agent orchestration) + 1 interactive Windows session sharing one OAuth chain
  • Reproduced across two independent incidents in one day; forensic file listings captured for both

View original on GitHub ↗