Session resume silently truncates model context at null-byte padding from interrupted append
Summary
Session resume loads only the portion of a session JSONL that precedes the first non-JSON line. If the file contains a block of null bytes inserted by an interrupted append, the UI picker still renders the correct last-message preview from the tail of the file, but the active model context is truncated at the null-byte line. The user sees the picker showing the correct latest turn, starts typing, and the model continues from a much earlier point with no warning.
Environment
- Claude Code 2.1.111
- WSL2, Linux 6.6.x microsoft-standard
- ext4 with default mount options
Session file path shape: $CLAUDE_CONFIG_DIR/projects/<encoded-cwd>/<session-uuid>.jsonl
Corruption pattern
A mid-file line in the JSONL is a run of null bytes (length varies, observed cases 200 to 700 bytes). The lines before and after the null-byte block are valid JSON events with correct consecutive timestamps. The file continues to receive valid appends after the corrupted line. Line count and byte size are consistent with the session having continued to run past the corruption event.
This is the well-known ext4 signature of a crashed append with buffered I/O and delayed allocation. Metadata journaling extends the file size and allocates the extent; the unflushed data block reads as null bytes on recovery. Later appends write valid content past the hole. The same signature has been reported in other CLI tools running on WSL2 (see github/copilot-cli#2217 for an identical null-byte trace in an unrelated codebase).
Reproduction
The underlying crash cannot be scheduled, but the corrupted file can be reproduced synthetically.
- Take any valid session JSONL from
$CLAUDE_CONFIG_DIR/projects/<encoded-cwd>/. - Insert one line of N null bytes between two existing valid lines, where N is at least a few hundred bytes.
- Append further valid JSON lines after the null-byte line.
- Run
claude --resume <session-uuid>.
Observed
- Session picker shows the correct latest-message preview (parsed from the file tail).
- After selecting resume, the model context contains only events from before the null-byte line.
- All valid JSON events written after the null-byte line are silently excluded from the replay.
- No warning, no error, no log entry.
Expected
Either of the following is acceptable:
- Resume parser skips non-JSON lines with a warning to stderr and continues loading subsequent valid events.
- Resume fails loudly with a message naming the line number of the first invalid line, so the file can be repaired before resuming.
The current behavior, in which the UI preview and the replayed model context disagree about the contents of the same file, is the worst case. A user cannot detect the truncation until the model starts behaving as if recent turns did not happen, at which point they may have written new messages on top of the silently truncated state.
Repair for the affected file
When the corrupted line is entirely null bytes, is not valid JSON, and has no uuid or parentUuid referenced by any other event:
sed -i '<line>d' FILEto delete the bad line.- Per-line JSON validate to confirm every remaining line parses.
- Resume.
No official repair flow exists in the CLI.
Scale
A scan of one environment with tr -cd '\0' | wc -c over all session JSONL files under $CLAUDE_CONFIG_DIR/projects/ found multiple corrupted files. Two patterns appeared:
- Trailing null-byte block at end of file, no valid data after. This is likely loaded correctly on resume because the parser stops at EOF at the hole.
- Null-byte block mid-file with valid JSON before and after. This triggers the silent-truncation failure described above.
Requested change
Pick any of:
- Use a line-tolerant JSONL parser in the resume path. Skip lines that fail to parse, log the skip count and line numbers to stderr or to a session log, continue loading subsequent valid lines.
- Fail loudly on the first invalid line in the resume path. Print the file path and line number. Do not silently truncate.
- Call
fsync(2)on the session file after each event append to close the crash window that produces the null-byte hole in the first place. Cost is one syscall per append; benefit is no more null-byte corruption under abnormal shutdown.
Any of the above removes the worst part of the current behavior, which is the disagreement between the UI preview and the replayed context.
Related issues
- #20992 concurrent-write corruption with truncated mid-line JSON
- #22526 phantom parentUuid chains stop resume traversal early
- #24304 conversation history missing on resume except last message
- #36583 messageId and uuid collision breaks chain traversal
- #45076 silent session data loss between sessions
- github/copilot-cli#2217 identical null-byte trace on WSL2 in an unrelated CLI
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗