Session JSONL corruption from non-atomic writes during concurrent tool execution

Resolved 💬 3 comments Opened Apr 8, 2026 by ppiankov Closed Apr 12, 2026

Bug Description

Claude Code sessions become permanently unresumable with API Error: 400 due to tool use concurrency issues when the JSONL session file is partially written during concurrent tool execution.

Steps to Reproduce

  1. Start a session with multiple concurrent tool calls (3+ parallel MCP tool calls or subagent dispatches)
  2. While tools are executing, trigger a state change (enter plan mode via Shift+Tab, or send a new message)
  3. The session hits 400 and cannot continue. /rewind sometimes helps but often the corruption persists.

Root Cause (Inference)

The session file appears to be written non-atomically. When multiple tool results are being written concurrently, one entry can be dropped — leaving a tool_use in the assistant message with no matching tool_result in the user message. Every subsequent API call fails validation.

Evidence from affected sessions:

  • Assistant message contains 3 tool_use blocks
  • Following user message contains only 2 tool_result blocks
  • The missing tool_result was never written to the JSONL
  • The orphaned tool_use ID has no matching result anywhere in the file

Environment

  • Claude Code CLI: 2.1.85 (also reproduced on 2.1.89-2.1.91)
  • macOS 15.x (Darwin 25.4.0)
  • Reproduction rate: approximately 1 in 10 sessions with heavy tool use
  • Higher rate when using MCP tools (external services, database connectors)

Suggested Fix

Use atomic write pattern for session persistence:

1. Write session data to a temporary file (same directory)
2. fsync the temporary file
3. Rename temporary file to the session file (atomic on POSIX)

This is a standard pattern for crash-safe file updates. The current approach of writing directly to the session file creates a window where interruption leaves the file in an inconsistent state.

Workaround

Manually repair the JSONL by:

  1. Finding the orphaned tool_result (parent UUID points to missing entry)
  2. Injecting a synthetic tool_result for the orphaned tool_use
  3. Resuming the session

This is fragile and requires understanding the JSONL format.

Related Issues

  • #31328 (session becomes unresumable after JSONL writer drops assistant entry — closed as stale)
  • #21321 (tool-use concurrency 400 mid-session)

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗