Registry file written non-atomically: Claude Desktop crash mid-write produces all-null `local_*.json`, session vanishes from picker (Windows 1.7196.0.0)
TL;DR
Claude Desktop writes the per-session registry file (AppData/Roaming/Claude/claude-code-sessions/<userId>/<orgId>/local_<sessionId>.json) non-atomically. If the process dies during a flush, the OS leaves a file of the correct allocated size but filled entirely with null bytes (0x00). The session vanishes from every UI surface (/resume, session picker, CCD list_sessions MCP tool) — even though the underlying .jsonl transcript is fully intact.
This is the same bug class as #24958 / #24009 / #23421, but with concrete forensic evidence of the corruption pattern and a successful manual recovery procedure. Issue #24958 was closed as stale; the bug is still present in v1.7196.0.0.
Concrete incident (2026-05-17 ~02:45 ICT)
| Artifact | State |
|---|---|
| local_<sid>.json registry entry | Corrupted — 144,258 bytes, all 0x00 |
| <cliSessionId>.jsonl conversation transcript | Intact — 38,518,504 bytes, valid JSONL |
| git-worktrees.json lease pointing at local_<sid> | Intact — points to orphaned (now-unreadable) session |
| Session in list_sessions (incl. include_archived: true) | Missing |
| Session in CLI /resume | Missing |
User had this session pinned with a custom title. Both pin state and title were lost. The 38 MB of conversation history was unreachable until manual repair.
Timeline from AppData/Roaming/Claude/logs/main.log
2026-05-17 02:42:19 Last clean activity (different session, local_68bdc412...)
2026-05-17 02:45 local_<sid>.json mtime — file truncated to all-null
2026-05-17 02:46:14 [updater] Checking for updates ← Claude Desktop relaunched
2026-05-17 02:46:14 [account] Account details not yet available, waiting for IPC...
2026-05-17 02:46:15 [MSIX] Filesystem virtualization active
2026-05-17 02:46:15 [growthbook] loaded 157 features (157 changed)
2026-05-17 02:46:15 [outbound-ccr] started
No explicit crash log line was written (consistent with a hard kill / process exit during fsync rather than an exception).
Forensic dump of the corrupted file
$b = [IO.File]::ReadAllBytes('local_<sid>.json')
$b.Length # 144258
$first = -1
for ($i=0; $i -lt $b.Length; $i++) {
if ($b[$i] -ne 0) { $first = $i; break }
}
$first # -1 (no non-zero byte in entire file)
This is the classic Windows footprint of a non-atomic in-place write that lost its data page after the file was extended: the metadata (length) commits, but the data pages never reach disk before the process dies. Other writers using O_TRUNC + write + close produce the same artifact.
Why it breaks downstream
The session manager iterates claude-code-sessions/<userId>/<orgId>/local_*.json and tries JSON.parse on each file. A file of all NULs fails parse and is silently skipped — there's no warning, no telemetry event, no "would you like to recover?" prompt. The session is just gone from every UI.
The .jsonl transcript file is still on disk and valid — but with no registry entry, neither the picker nor claude --resume <uuid> finds it.
Requested fix
- Atomic write:
fs.writeFile('local_<sid>.json.tmp', body)→fs.rename('local_<sid>.json.tmp', 'local_<sid>.json'). On Windows, useMoveFileEx(src, dst, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)for atomicity across the metadata commit. Node'sfs.promises.renamedoes this on NTFS. - Crash recovery: on startup, if a registry file fails to parse, derive a stub entry from the matching
<cliSessionId>.jsonltranscript (the first line of each transcript containscustomTitle,aiTitle, andcwdevents — enough to rebuild a basic registry entry). At minimum, show the user a recovery prompt instead of silently dropping the session. - Surface the failure: log a warning when a
local_*.jsonfails to parse, so the user can spot the problem before assuming the session is permanently lost.
Anyone hitting this today has to do what I had to do manually: copy the entire JSON schema from another working session, hand-patch sessionId/cliSessionId/cwd/title/worktreePath/timestamps, and overwrite the null file. Far beyond what a normal user should be expected to do to recover their own conversation.
Manual recovery procedure that worked
For anyone hitting this before the fix lands:
import json
from pathlib import Path
REG = Path(r"<...>\claude-code-sessions\<userId>\<orgId>")
TEMPLATE = REG / "local_<known-good-session>.json"
TARGET = REG / "local_<corrupted-session>.json"
j = json.loads(TEMPLATE.read_text(encoding="utf-8"))
j["sessionId"] = "local_<corrupted-session>"
j["cliSessionId"] = "<uuid from corresponding .jsonl filename>"
j["cwd"] = r"<worktree path>"
j["originCwd"] = r"<repo root>"
j["worktreePath"] = r"<worktree path>"
j["worktreeName"] = "<worktree dir name>"
j["branch"] = "claude/<worktree dir name>"
j["sourceBranch"] = "main"
j["title"] = "<custom title — recover from `customTitle` event in jsonl>"
j["titleSource"] = "user"
j["isArchived"] = False
j["createdAt"] = <epoch ms>
j["lastActivityAt"] = <epoch ms>
TARGET.rename(TARGET.with_suffix(".json.corrupted-bak")) # keep backup
TARGET.write_text(json.dumps(j, ensure_ascii=False, separators=(",", ":")), encoding="utf-8")
# then restart Claude Desktop (in-memory cache only refreshes at startup)
Pin state and exact completedTurns cannot be recovered.
Environment
- OS: Windows 11
- Claude Desktop:
1.7196.0.0(MSIX install,Claude_pzs8sxrjxfjjc) - Filesystem: NTFS, OneDrive-synced parent (
C:\Users\<USER>\OneDrive\Desktop\MycOS) — note: corrupted file is inAppData\Local\Packages\...\LocalCache\Roaming\Claude\, not under OneDrive, so OneDrive isn't a factor - Affected session: 38.5 MB
.jsonl, ~6 months active
Related issues (closed as stale, not resolved)
- #24958 — OOM crash corrupts session data: custom names lost and sessions missing from resume
- #24009 — Crash corrupts session data, resume picker fails
- #23421 —
sessions-index.jsonstops updating, new sessions don't appear
Locking #24958 after 7 days of inactivity made it impossible to reply with new evidence to the same bug. Filing this as a fresh issue per the auto-close instruction.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗