Race condition: .claude.json corrupted by concurrent writes from multiple sessions

Status Closed — not planned
Maintainer reply None cached
Activity 11 comments · opened Feb 27, 2026 · closed May 25, 2026

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

  1. Open 2+ Claude Code sessions in the same directory simultaneously
  2. Use both actively (so both write config updates)
  3. Observe corruption errors on next startup

View original on GitHub ↗

11 Comments

DomenicoDomotz · 6 months ago

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 userID Hashes

Extracted from the last 50 corrupted files — 21 distinct userID values. 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)

Files < 500 bytes:  159 (55%)  ← severely truncated
Files < 1000 bytes: 204 (71%)
Files > 4000 bytes:  26 (9%)   ← close to expected ~5KB
Median: 497 bytes

Hourly Correlation with Tool Usage

08:00:    1     14:00:  135 ███████████████████████████████████
09:00:    2     15:00:    5
10:00:    3     16:00:   10
11:00:    3     17:00:   22
12:00:    9     18:00:    3
13:00:    4     19:00:   86 ███████████████████████

Peak corruptions align exactly with peak tool/subagent activity.

Key Findings

  1. Corrupted files are valid JSON with 1-3 keys (not garbled data), proving truncate-then-write pattern:

``json
{"changelogLastFetched": 1772121843243} // 43 bytes
{"clientDataCache": {"data": {}, "timestamp": 1772133973376}} // 77 bytes
``

  1. Growing sizes in time clusters (43 → 77 → 157 → 234 → 391 → 497 within seconds) = snapshots at progressive write stages
  1. Backup/restore cascade: CC's own recovery (backup corrupted → restore from backup) is itself non-atomic, creating new corruption opportunities. This explains exponential growth: 1 → 57 → 280/day.

Workaround

Built a background guardian daemon that polls .claude.json every 500ms, maintains an atomic "golden copy" via os.replace() (maps to MoveFileExW on Windows), and restores within ~650ms of corruption. Available if anyone wants it.

Environment

  • Windows 11 (10.0.26200)
  • Claude Code v2.1.62 (native)
  • 25 hooks (7 PreToolUse, 5 PostToolUse) — amplifies write frequency
  • Frequent Task tool usage (spawns concurrent subagent processes)

Proposed Fix Priority

  1. P0: Use write-file-atomic (or temp + rename) for .claude.json writes
  2. P1: Make the backup/restore cycle atomic too (prevents cascade)
  3. P2: Consider per-process config isolation for subagents (they don't need to write to shared config)
callmesomesh · 6 months ago

Additional 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

  • Windows 11 (Git Bash / MINGW64)
  • Claude Code v2.1.59, native install
  • 5 workflows, each in its own terminal, running simultaneously from the same workspace root
  • Sessions are long-running (hours) with active tool use in parallel

What I see

  • claude.json is corrupted: JSON Parse error: Unexpected EOF error appears mid-conversation in terminals that aren't even the ones causing the write
  • Error interrupts active work in other sessions (shows inline in terminal output)
  • Corrupted backup files generated in pairs with timestamps <100ms apart (confirming two sessions colliding)
  • Multiple corrupted files per day:

``
.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

  • Disrupts workflow when error message appears mid-conversation in unrelated terminals
  • Forces manual backup restoration
  • Config resets (tool usage stats, project settings, cached features all lost)

Notes

  • The toolUsage, cachedGrowthBookFeatures, and clientDataCache fields seem to be the most frequent write triggers
  • Even when sessions are idle, periodic background writes (cache updates, feature flag refreshes) can trigger the race condition
  • autoUpdates: false doesn'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).

sstklen · 6 months ago

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:

  1. Replace all fs.writeFile('.claude.json', data) calls with an atomic write helper: write to '.claude.json.tmp.<pid>' first, then fs.renameSync() to '.claude.json'
  2. Add advisory file locking (e.g., lockfile or flock) around the read-modify-write cycle to prevent concurrent sessions from interleaving
  3. On startup, detect if a .tmp write file exists (indicating a crashed incomplete write) and clean it up rather than reading a potentially corrupt main file
  4. Consider using a write-ahead approach: read from .claude.json, but if it fails JSON.parse, automatically fall back to .claude.json.backup before creating a fresh config
// Atomic write helper (Node.js/Bun compatible)
import { writeFileSync, renameSync, unlinkSync } from 'fs';
import { join, dirname } from 'path';

function atomicWriteJSON(filePath: string, data: object): void {
  const tmpPath = `${filePath}.tmp.${process.pid}`;
  try {
    writeFileSync(tmpPath, JSON.stringify(data, null, 2), 'utf-8');
    renameSync(tmpPath, filePath); // atomic on same filesystem
  } catch (err) {
    try { unlinkSync(tmpPath); } catch {} // cleanup on failure
    throw err;
  }
}

// Usage: replace writeFileSync('.claude.json', ...) with:
// atomicWriteJSON('.claude.json', configObject);

📊 _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. 🦞

_Disclosure: This analysis is from Confucius Debug, an AI-powered community KB for agent bugs. Please verify before applying._

---
<sub>🦞 Confucius Debug — community knowledge base for AI agent bugs. Free to search via MCP.</sub>

tsantoso79 · 6 months ago

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:

Process A: 508b4da3ba38eff5e8f373d2cad7d2a1f3607524...
Process B: a0ff412df5211fd40b658fd462b7d70805e4b48a...

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

TIME        SIZE      STATE
20:21:25    77,118    Full config (327 startups, 5 MCP servers, 42 projects)
20:21:47       193    DESTROYED — just { clientDataCache, userID }
20:21:56       428    Fresh session rebuilding (Bash:3, Read:2)
20:22:47       519    Two processes fighting...
20:24:01       157    Different userID writes even less
20:24:55        77    Minimum: just { clientDataCache: {} }

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

  • Windows 10 (Git Bash / MINGW64)
  • Claude Code v2.1.59, native install
  • Concurrent: CLI session + Desktop app (not multiple CLIs)
sstklen · 6 months ago

@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) vs a0ff412d... (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 since rename() is atomic on both POSIX and NTFS.

junaidtitan · 6 months ago

We kept hitting this with multiple Claude instances. Built cozempic to detect and auto-repair .claude.json corruption — open source.

pip install cozempic
cozempic doctor        # detects truncated JSON, missing auth, corruption cascades
cozempic doctor --fix  # restores from most recent valid backup

Scans for invalid JSON, numStartups anomalies, and rapid backup file creation (the cascade pattern). Feedback welcome.

nemekath · 5 months ago

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.json using FileSystemWatcher (event-driven, no polling). On every write it:

  1. Acquires a system-wide Named Mutex — serializes access across all Claude Code processes, subagents, desktop app, everything
  2. Validates the incoming JSON
  3. Deep-merges changes against a shadow copy of the last known good state
  4. Writes back atomically via [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.writeFileSync at runtime, but that's currently inactive because Claude Code compiles with --no-compile-autoload-bunfig.

0reo · 4 months ago

We are seeing the same non-atomic write race affect ~/.claude/settings.json as 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.

ProductOfAmerica · 4 months ago

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:

Apr 15 16:16  — 1 file   (40759 bytes, last good state)
Apr 15 18:10  — 4 files  (24082 bytes each, same minute)
Apr 15 21:32  — 3 files  (23662 bytes each, same minute)
Apr 15 21:41  — 4 files  (23794 bytes each, same minute)
Apr 16 00:45  — 3 files  (24157 bytes each, same minute)

Two patterns beyond what #29217 describes:

  1. Clustered multi-file writes: three or four .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.
  2. Cumulative content loss across cascades: size drops from the last good state at 40759 → 24082 → 23662 bytes, and never recovers. Each corruption event strips more of the previous state than it recovers. The recovered file always ends up smaller than whichever .corrupted.* snapshot is largest in the cluster.

Specific content loss observed:

The root mcpServers object was entirely removed from the recovered file — three stdio server entries including their env blocks (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 its mcpServers back 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):

  • Claude Code 2.1.112
  • Windows 11, Git Bash
  • Multiple Claude Code sessions active concurrently (common workflow across a few projects)

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.

github-actions[bot] · 3 months ago

Closing for now — inactive for too long. Please open a new issue if this is still relevant.

github-actions[bot] · 1 month ago

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.