Race condition: .claude.json corrupted by concurrent writes from multiple sessions
Bug Description
.claude.json gets repeatedly corrupted when multiple Claude Code sessions run concurrently (or are rapidly restarted). The file is truncated mid-write, producing JSON Parse error: Unexpected EOF.
Evidence
Over a ~40 minute window (Feb 26, 14:54–15:35), 165 corrupted backup files were generated in ~/.claude/backups/. The corruption cascades — each recovery attempt gets corrupted again by a competing process, producing progressively smaller truncated files:
-rw-r--r-- 11873 Feb 26 14:15 .claude.json.corrupted.1772095551270 # full config
-rw-r--r-- 469 Feb 26 14:16 .claude.json.corrupted.1772095565563 # partially recovered
-rw-r--r-- 157 Feb 26 14:16 .claude.json.corrupted.1772095562170 # barely anything
-rw-r--r-- 77 Feb 26 14:16 .claude.json.corrupted.1772095562117 # just opening brace
Files as small as 77 bytes (just {"numStartups":1,"installMethod":"native","autoUpdates":false}) show the file being read mid-write by another process.
Impact
- Token waste: Each corrupted restart triggers re-initialization (feature flag fetches, re-auth, session setup), burning tokens on repeated failed startups
- Config loss: The final state was a fresh config (
numStartups: 1) replacing a real config (numStartups: 72) with all project history, theme settings, and tool usage data - User had to manually restore from
.claude.json.backup
Root Cause
The file write to .claude.json is not atomic. When multiple processes write simultaneously, one truncates the file while another is mid-read or mid-write, producing invalid JSON.
Suggested Fix
Use atomic file writes: write to a temporary file (e.g., .claude.json.tmp), then rename() it to .claude.json. On most filesystems, rename() is atomic and prevents partial reads. Additionally, consider using file locking (flock / platform equivalent) to serialize access.
Environment
- Windows 11 (via Git Bash)
- Claude Code (native install)
- Trigger: multiple sessions or rapid restarts from same directory
Reproduction
- Open 2+ Claude Code sessions in the same directory simultaneously
- Use both actively (so both write config updates)
- Observe corruption errors on next startup
11 Comments
Additional Forensic Evidence (7-day dataset, 423+ corruptions)
Adding comprehensive data from a power-user setup (25 hooks, frequent subagent spawns via Task tool):
Scale of Impact
| Date | Corruptions | Notes |
|------|------------|-------|
| Feb 20 | 1 | First occurrence |
| Feb 21 | 57 | Moderate hook usage |
| Feb 22 | 74 | |
| Feb 23 | 1 | Light usage day |
| Feb 24 | 6 | |
| Feb 25 | 4 | |
| Feb 26 | 280 | Heavy subagent + hook usage |
| Total | 423 | |
Smoking Gun: 21 Different
userIDHashesExtracted from the last 50 corrupted files — 21 distinct
userIDvalues. This proves multiple independent processes are writing their own state to the same file simultaneously. Each subagent (spawned via Task tool) appears to compute and write its own userID hash.Corruption Size Distribution (Feb 26, n=286)
Hourly Correlation with Tool Usage
Peak corruptions align exactly with peak tool/subagent activity.
Key Findings
``
json
``{"changelogLastFetched": 1772121843243} // 43 bytes
{"clientDataCache": {"data": {}, "timestamp": 1772133973376}} // 77 bytes
Workaround
Built a background guardian daemon that polls
.claude.jsonevery 500ms, maintains an atomic "golden copy" viaos.replace()(maps toMoveFileExWon Windows), and restores within ~650ms of corruption. Available if anyone wants it.Environment
Proposed Fix Priority
write-file-atomic(or temp + rename) for.claude.jsonwritesAdditional reproduction - heavy multi-session Windows user
Same issue. I run 5+ concurrent Claude Code sessions daily (one per workflow/project). The corruption happens consistently.
My setup
What I see
claude.json is corrupted: JSON Parse error: Unexpected EOFerror appears mid-conversation in terminals that aren't even the ones causing the write``
``.claude.json.corrupted.1772171904373
.claude.json.corrupted.1772171904434 # 61ms apart - two sessions collided
.claude.json.corrupted.1772172051047
.claude.json.corrupted.1772172052027 # ~1 second apart - same pattern
Impact
Notes
toolUsage,cachedGrowthBookFeatures, andclientDataCachefields seem to be the most frequent write triggersautoUpdates: falsedoesn't prevent the writes+1 on the atomic write fix. Per-session write files merged on read would also work (each session writes to
.claude.json.{pid}, reads merge all).Ran into this exact issue before — here's what we found.
What's happening: The .claude.json file write operation is not atomic. Multiple concurrent Claude Code sessions (or rapid restarts) perform read-modify-write cycles on the same file without file locking or atomic rename. Process A reads the file, Process B truncates and begins writing, Process A (or C) reads the partially-written truncated content, then writes that corrupted state back — creating a corruption cascade where each recovery attempt gets corrupted again by a competing writer.
What worked for us:
Replace direct fs.writeFile() with atomic write pattern: write to a temporary file in the same directory, then rename (mv) it to the target path. rename() is atomic on POSIX systems within the same filesystem. Additionally, use a file lock (e.g., proper-lockfile or fcntl advisory lock) to serialize concurrent access across processes.
Steps:
📊 _We found 4 similar cases in our knowledge base with the same pattern — this gives us high confidence in this analysis._
Let me know if this works for your setup — happy to help troubleshoot further. 🦞
---
<sub>🦞 Confucius Debug — community knowledge base for AI agent bugs. Free to search via MCP.</sub>
Additional evidence: CLI + Desktop App race (not just multi-session CLI)
This isn't limited to multiple CLI sessions — it also happens between the CLI and Desktop app, which is harder for users to avoid.
Proof: Two different userIDs in corrupted files
The corrupted backups from my incident contain two distinct userIDs, confirming two separate Claude Code processes:
One from the CLI session, one from the Desktop app. The user doesn't even need to open multiple terminals — just having both the CLI and Desktop app running triggers it.
Collapse timeline: 77KB → 77 bytes in 3.5 minutes
34 corrupted copies in 4 minutes. The "winner" was whichever process had the least data in memory.
Data loss quantified
| Field | Before | After |
|-------|--------|-------|
| MCP Servers | 5 | 0 (all lost) |
| Projects | 42 | 0 |
| numStartups | 327 | 1 |
| promptQueueUseCount | 14,558 | 0 |
| Skill Usage Records | 13 (225 total uses) | 0 |
| Tool Usage Entries | 29 | 0 |
The MCP server configs included inline API keys — so the user also silently loses access to configured tools with no indication of what was lost.
Environment
@tsantoso79 That CLI + Desktop App race is an important addition — users can't reasonably avoid that scenario since they don't even realize both processes share the same
.claude.json.The two distinct userIDs in your corrupted backups are solid proof:
508b4da3...(CLI) vsa0ff412d...(Desktop App) — two separate processes doing concurrent read-modify-write with no coordination.Updated scope:
| Scenario | Confirmed |
|----------|-----------|
| Multiple CLI sessions | ✅ (OP, @DomenicoDomotz, @callmesomesh) |
| CLI + Desktop App | ✅ (@tsantoso79) |
| Subagent spawns (hooks/Task tool) | ✅ (@DomenicoDomotz — 280 corruptions in one day) |
| Cross-platform (macOS + Windows) | ✅ (@callmesomesh — Windows/Git Bash) |
The atomic write fix (write to temp →
rename()) would cover all these cases sincerename()is atomic on both POSIX and NTFS.We kept hitting this with multiple Claude instances. Built cozempic to detect and auto-repair .claude.json corruption — open source.
Scans for invalid JSON, numStartups anomalies, and rapid backup file creation (the cascade pattern). Feedback welcome.
Workaround: event-driven broker daemon for Windows
Been dealing with this for weeks across 3-5 parallel sessions. After the backup folder hit triple digits I decided to stop restoring manually and build something that actually prevents the data loss instead of just detecting it after the fact.
The core idea: a PowerShell daemon that sits between Claude Code and
.claude.jsonusingFileSystemWatcher(event-driven, no polling). On every write it:[System.IO.File]::Replace()The deep-merge matters because with multiple sessions writing concurrently, a simple "restore last good backup" throws away whatever the other session just wrote. The merge preserves changes from all sessions — nested objects are merged recursively, only arrays get replaced wholesale.
Detection-to-repair takes ~100-200ms. No admin elevation needed, runs entirely in user-space. PowerShell 5.1+ (ships with Windows).
Repo: https://github.com/nemekath/claude-config-broker
Obviously this is a band-aid until Anthropic ships atomic writes upstream (the temp + rename pattern @sstklen described). But it's been stable for us in daily use and completely eliminated the corruption cascades @DomenicoDomotz documented.
Limitations: Windows only (Named Mutex is a Win32 API). The repo also includes a Bun preload approach (Layer 1) that patches
fs.writeFileSyncat runtime, but that's currently inactive because Claude Code compiles with--no-compile-autoload-bunfig.We are seeing the same non-atomic write race affect
~/.claude/settings.jsonas well. A PreToolUse hook registered by one session was silently reverted when a concurrent session wrote its own changes to settings.json. No error, no notification — the hook just disappeared.Same fix applies: atomic writes (temp file + rename) for all shared config files, not just
.claude.json.---
This comment was written by Claude Code.
Seeing this on a recent Claude Code build (2.1.112, Windows 11 via Git Bash). Adds another data point with a slightly different cascade pattern than the original report — thought it might be useful.
Corruption timestamps on one machine over two calendar days:
Two patterns beyond what #29217 describes:
.corrupted.*snapshots land in the same minute, each within milliseconds of the next, sizes identical to the byte. That looks like multiple processes each attempting a recovery-write on the same invalid state simultaneously, with each write racing to rename-over-truncated the others..corrupted.*snapshot is largest in the cluster.Specific content loss observed:
The root
mcpServersobject was entirely removed from the recovered file — three stdio server entries including theirenvblocks (API tokens) were gone. Project-level entries elsewhere in the file (under/projects/<path>) survived. Appears the partial-recovery path preserves some scopes and drops others non-deterministically.Recovery path that worked:
The 40759-byte
.corrupted.*from the first event was still valid JSON. Merged itsmcpServersback into the post-recovery file with a small Python script; restart picked up the servers as expected. Takeaway: the oldest.corrupted.*in the cascade is usually the most recoverable, but users have to know to look for it.Environment (matches #29217):
Atomic write + tmp-and-rename as originally suggested would address both patterns above. Adding a flock or PID-based guard during the recovery path would also help — right now a recovery write can itself be raced by a second instance that sees the invalid state and spawns its own recovery write.
Closing for now — inactive for too long. Please open a new issue if this is still relevant.
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.