Credential writes replace symlinks and clobber shared state: concurrent instances sharing .credentials.json cascade each other (and the user) to logged-out
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)
- Write-through for symlinked credential paths (resolve the symlink and rename within the target's directory), or document that symlinked
.credentials.jsonis unsupported. - 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.
- 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.
- 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