Skip to content

miteshashar/claude-code-thinking-blocks-fix

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 

Repository files navigation

Fixing Claude Code "thinking blocks cannot be modified" Error

A diagnostic and repair tool for Claude Code sessions that become permanently stuck on the API error thinking or redacted_thinking blocks in the latest assistant message cannot be modified.

Sound familiar?

  • Have you been deep into a long Claude Code session - deeply accumulated context, tools running, real progress - when suddenly every message fails with a cryptic thinking blocks cannot be modified error?
  • Does retrying do nothing, restarting Claude Code does nothing, and the session is effectively bricked with no clear way to recover?
  • Is the error message pointing you at messages.N.content.M but you have no idea what that path means or what you're supposed to do with it?
  • Have you already lost hours worth of session context because you couldn't figure out how to fix the corrupted history and had to start over?
  • Are you staring at a .jsonl file with thousands of lines wondering which ones are broken and how to fix them without destroying the rest of your conversation?

This guide explains how to recover the session first, then explains why it happens.

Table of Contents

Installation

# Clone the repo
git clone https://github.com/user/claude-code-thinking-blocks-fix.git
cd claude-code-thinking-blocks-fix

# Optionally, add it to your PATH
cp claude-session-fix-thinking ~/.local/bin/

Quick Fix (Recommended)

# 1) Diagnose a broken session
claude-session-fix-thinking diagnose SESSION_ID

# 2) Apply targeted fix (auto-creates backup)
claude-session-fix-thinking fix SESSION_ID

# 3) Resume your session
claude --resume SESSION_ID

If Quick Fix Fails

  • Symptom: Same error after fix
    • Run:
claude-session-fix-thinking diagnose SESSION_ID
claude-session-fix-thinking nuke SESSION_ID
claude --resume SESSION_ID
  • Expected: session resumes without the messages.N.content.M thinking-block error.
  • Symptom: SESSION_ID.jsonl not found
    • Run:
ls ~/.claude/projects/*/SESSION_ID.jsonl
  • Expected: you find the exact project path for your session file.
  • Symptom: Debug log file missing
    • Action: continue anyway; diagnose works directly from JSONL.
  • Symptom: Invalid JSON lines / parse errors
    • Action: restore the latest backup file, then rerun fix.
  • Symptom: Still unrecoverable after nuke
    • Last resort: salvage visible conversation into a recovery note, then start a fresh session with that note as seed context.
    • Example salvage command:
grep -E '"type":"(user|assistant)"' SESSION_ID.jsonl > SESSION_ID.salvage.jsonl

Safety

  • fix and nuke automatically create a backup before writing changes.
  • Backup location/pattern: same directory as session JSONL, <session-id>.jsonl.<unix_ts>.backup.
  • If you manually edit session files, create your own backup first:
cp SESSION.jsonl SESSION.jsonl.$(date +%s).backup
  • fix is targeted recovery: it edits detected thinking-block corruption patterns and trims trailing API error/progress/system tail noise.
  • nuke removes all thinking / redacted_thinking blocks from assistant messages but keeps user/assistant text and tool traces.
  • Data loss boundary: missing redacted_thinking payloads cannot be reconstructed.

Compatibility

  • Verified with Claude Code 2.1.42 (at time of writing).
  • Expected to work when session JSONL/debug format matches current Claude Code structure.
  • Known unsupported or partial-support cases:
    • Session files with invalid JSON lines (for example truncation or partial writes).
    • Corruption modes unrelated to thinking-block integrity (for example broken tool-call schema/data).
    • Histories where required redacted_thinking bytes were never persisted and cannot be inferred.
    • Major future Claude Code format changes that alter event/message serialization shape.

The Error

messages.N.content.M: `thinking` or `redacted_thinking` blocks in the latest
assistant message cannot be modified. These blocks must remain as they were
in the original response.

This is an API validation error where the Anthropic API rejects a request because thinking/redacted_thinking content blocks in the conversation history have been corrupted - either modified, reordered, or orphaned from their original response.

Why This Happens (Root Cause)

TL;DR: Interleaved assistant chunks from different messages, combined with repair logic that mutates/reorders thinking blocks and missing persisted redacted_thinking data, causes signature mismatches that Anthropic rejects.

ELI5: We accidentally mixed parts of two answers, changed their order, and lost a few pieces, so the API says, "This is not the exact original answer."

Claude Code stores session history as JSONL files where each streaming chunk is a separate line. Two bugs interact to cause this:

Bug 1: Streaming Interleaving

During long sessions, streaming responses from parallel or rapidly sequential API calls can interleave in the JSONL. Two different msg_ids get their chunks written adjacently:

line 100: assistant id=msg_AAA -> text("\n\n")
line 101: assistant id=msg_AAA -> thinking(765ch, sig=Epg...)    <- host
line 102: assistant id=msg_BBB -> thinking(1371ch, sig=Euo...)   <- INTRUDER
line 103: assistant id=msg_AAA -> text("The answer is...")       <- host resumes
line 104: assistant id=msg_BBB -> text("Let me check...")
line 105: assistant id=msg_BBB -> tool_use(Bash)

When Claude Code reconstructs the API messages array, it merges consecutive assistant lines into one message. The merged message ends up with two thinking blocks from different API responses with different cryptographic signatures.

Bug 2: ensureToolResultPairing Repair

Claude Code's ensureToolResultPairing function detects and repairs missing tool_result messages. During repair, it can:

  • Split merged assistant messages by inserting bare user messages
  • Reconstruct content arrays, mutating redacted_thinking blocks
  • Reorder content blocks within a message

The API validates thinking blocks byte-for-byte against their cryptographic signatures. Any mutation breaks the signature.

Bug 3: JSONL Doesn't Store redacted_thinking

The JSONL writer does not persist redacted_thinking blocks (opaque encrypted blobs). These exist only in memory. When a session crashes or ensureToolResultPairing reconstructs messages, the redacted_thinking blocks are lost or corrupted. The API then rejects the request because the thinking block's signature covers content that's no longer present.

Deep Dive (Optional)

Diagnosis

Step 1: Find the Session Files

# Session JSONL (the conversation history)
ls ~/.claude/projects/*/SESSION_ID.jsonl

# Debug log (detailed error traces)
ls ~/.claude/debug/SESSION_ID.txt

Step 2: Confirm the Error

# Check debug log for the specific error
grep "thinking.*blocks.*cannot be modified" ~/.claude/debug/SESSION_ID.txt
# Note the message index: "messages.N.content.M"

# Check for ensureToolResultPairing repairs
grep "ensureToolResultPairing" ~/.claude/debug/SESSION_ID.txt

Step 3: Find Corruption Points

The key pattern: consecutive assistant JSONL lines with different msg_ids where at least one has a thinking block.

# Use the automated script
claude-session-fix-thinking diagnose SESSION_ID

Optional (advanced): inspect with Python:

import json

with open("SESSION_ID.jsonl") as f:
    lines = f.readlines()

prev_type, prev_id, prev_line, prev_thinking = None, None, None, False
for i, line in enumerate(lines):
    obj = json.loads(line)
    t = obj.get('type', '')
    if t != 'assistant':
        prev_type = t
        continue
    msg = obj.get('message', {})
    msg_id = msg.get('id', '')
    content = msg.get('content', [])
    has_thinking = any(
        isinstance(b, dict) and b.get('type') in ('thinking', 'redacted_thinking')
        for b in (content if isinstance(content, list) else [])
    )
    if prev_type == 'assistant' and msg_id != prev_id and (has_thinking or prev_thinking):
        print(f"CORRUPTION: lines {prev_line}-{i}, ids {prev_id[:20]}->{msg_id[:20]}")
    prev_type, prev_id, prev_line, prev_thinking = t, msg_id, i, has_thinking

Fix Procedure Internals

fix auto-creates a backup before writing; see Safety for details.

Pattern A: Intruder thinking block between host's blocks

line N:   assistant id=AAA -> thinking(...)     <- HOST
line N+1: assistant id=BBB -> thinking(...)     <- INTRUDER
line N+2: assistant id=AAA -> text(...)         <- HOST continues

Fix: Merge intruder's thinking text into host's thinking block, keep host's signature, delete the intruder line.

host = json.loads(lines[N])
intruder = json.loads(lines[N+1])

# Merge thinking text
host_thinking = host['message']['content'][0]['thinking']
intruder_thinking = intruder['message']['content'][0]['thinking']
host['message']['content'][0]['thinking'] = host_thinking + "\n\n" + intruder_thinking
# Signature stays as-is from host

lines[N] = json.dumps(host, ensure_ascii=False) + "\n"
del lines[N+1]

Pattern B: Orphaned thinking block after different msg's text

line N:   assistant id=AAA -> text(...)
line N+1: assistant id=BBB -> thinking(...)     <- ORPHANED
line N+2: assistant id=BBB -> text(...)

Fix: Remove the thinking-only line. The text blocks carry the conversation; the thinking block is just internal reasoning.

del lines[N+1]  # Remove the orphaned thinking line

Also: Strip Trailing API Error Messages

When the session is stuck on this error, the tail of the JSONL accumulates API error entries and retry noise. Strip them:

# Walk backwards from end, remove error/progress/system lines
# Stop at the last real user or assistant message

Nuclear Option: Strip All Thinking Blocks

If the targeted fix doesn't work (e.g., the API still rejects due to signature mismatches), strip ALL thinking blocks from the JSONL:

claude-session-fix-thinking nuke SESSION_ID

This removes every thinking content block from every assistant message. The conversation history is fully preserved (text, tool_use, tool_result all remain). You lose the model's internal reasoning traces but gain a working session.

Prevention

  • Avoid very long sessions. The longer a session runs, the more likely streaming interleaving and message array degradation become.
  • Watch for streaming fallback warnings in the debug log: "Stream completed with message_start but no content blocks completed".
  • If you see ensureToolResultPairing: repaired in the debug log, the session is already degraded. Consider starting fresh or applying the fix proactively.

About

Fix Claude Code 'thinking blocks cannot be modified' API errors caused by streaming interleaving corruption in session JSONL files

Topics

Resources

Stars

Watchers

Forks

Contributors

Languages