[Meta] tool_use/tool_result block mismatch causing bad conversation state (150+ reports)

Open 💬 46 comments Opened Aug 29, 2025 by emcd
💡 Likely answer: A maintainer (emcd, contributor) responded on this thread — see the highlighted reply below.

Environment

  • Platform (select one):
  • [x] Anthropic API, others
  • Claude CLI version: multiple, including 1.0.94; have encountered the issue after the alleged fix in 1.0.84
  • Operating System: multiple
  • Terminal: multiple

Bug Description

Claude Code users are experiencing conversation disruptions due to mismatched Tool Use and Tool Result blocks. This can occur due to transient network failures, server errors, or other failures around hook executions or tool calls. (Both times I have seen this in the past two weeks have been after a hook execution.) As of this writing the following issue tracker query ( is:issue Each tool_use block must have a corresponding tool_result block in the next message) shows 150+ issues.

Open Issues (63):
#5662, #6443, #6551, #3886, #3636, #6302, #5599, #5509, #3512, #3632, #3860, #4454, #3916, #3003, #5928, #5374, #6628, #5765, #3637, #2126, #6242, #2616, #2312, #5747, #1800, #3504, #2967, #2961, #2406, #2903, #2352, #2249, #2959, #3754, #6178, #2232, #2704, #4401, #2242, #2802, #5713, #1978, #4425, #3549, #2369, #6585, #2043, #2672, #2261, #4409, #3532, #4466, #3608, #1796, #2971, #3564, #4038, #3983, #5385, #6595, #769, #1672, #3982, #1608

Closed Issues (92):
#6575, #6566, #6539, #6567, #5705, #5989, #6521, #5468, #6363, #6410, #5424, #5375, #5450, #5317, #4842, #5060, #4638, #4664, #4563, #3086, #5470, #6343, #4096, #3639, #5594, #4825, #4180, #4798, #1887, #5193, #5479, #3101, #6348, #3331, #6502, #4877, #2697, #1831, #3149, #2157, #1782, #4522, #1776, #1686, #2840, #5410, #2796, #2828, #1894, #4134, #2600, #1574, #1237, #5457, #1881, #3902, #3977, #2179, #1968, #4159, #2201, #2423, #6490, #1747, #1642, #1969, #1738, #1584, #2630, #1577, #3862, #3202, #473, #1586, #1695, #5412, #1571, #5246, #3191, #1956, #1856, #4240, #2708, #1678, #1174, #1579, #558, #1761, #2494, #5476, #5421, #4211, #4283, #2045, #2582, #746, #3985, #4065, #3616, #1679

(My apologies to anyone whose issue was erroneously linked - I asked Claude to pull the gh issue list results together for me and did not verify each and every link.)

Most of the closed issues are closed because they are duplicates, not because they are resolved. Some of the closed issues have significant threads with multiple people confirming the issue, which means that the hit rate is significantly higher than 150+ issues.

One person developed a remediation, which involves exporting the conversation, clearing the history, restarting Claude Code, and then importing the conversation history. While it is good to have a workaround like this, it is obviously painful and an in-program mitigation would be preferred.

Steps to Reproduce

Not consistently reproducible.

Expected Behavior

Graceful recovery.

Actual Behavior

No more conversation turns can be generated.

Additional Context

To deal with this issue in my personal AI workbench last year, I developed the following Python code which can be adapted to Typescript/Node.js fairly easily. It makes an O(n) backwards pass through the message history, collecting tool use IDs from results and then detecting any tool uses which do not have those IDs in the collected set:

    ''' Filters out tool use blocks that have no matching result. '''
    tool_result_ids = set( )
    filtered_messages: list[ AnthropicMessage ] = [ ]
    for message in reversed( messages ):
        content = message[ 'content' ]
        if not isinstance( content, list ):
            filtered_messages.append( message )
            continue
        filtered_blocks = [ ]
        for block in content:
            if not isinstance( block, dict ):
                filtered_blocks.append( block )
                continue
            match block.get( 'type' ):
                case 'tool_result':
                    if 'tool_use_id' in block:
                        tool_result_ids.add( block[ 'tool_use_id' ] )
                    filtered_blocks.append( block )
                case 'tool_use':
                    if block.get( 'id' ) in tool_result_ids:
                        filtered_blocks.append( block )
                case _:
                    filtered_blocks.append( block )
        if filtered_blocks:
            message_filtered = dict( message )
            message_filtered[ 'content' ] = filtered_blocks
            filtered_messages.append( message_filtered )
    return list( reversed( filtered_messages ) )

Or, you could open your sources so that others could help you patch serious issues....

View original on GitHub ↗

46 Comments

ldorigo · 10 months ago

It is indeed a rather irritating bug, especially since fixing it (or at least, adding a workaround while you figure out the root cause...) is really not very difficult. Claude code is essentially unusable at the moment for me.

zazer0 · 10 months ago

@bcherny I remember you were on top of this issue last time I had it (source) thought it might be helpful to ping incase of useful context on prior fix etc!

My steps to reproduce:

  • 1 begin subagent task delegation step
  • induce network dropout (e.g, disconnect wifi) - dropouts happen frequently for me in real-world use
  • 2 note "API timeout" error begins to occur
  • reconnect internet
  • note subagent appears frozen
  • 3 press "ESC" and revert to message just before subagent delegation began
  • re-send message (or send a different one)
  • ✅ reproduced - note that the conversation history is now broken
  • will deterministically get invalid_request_error.

screenshot of mine:

<img width="1464" height="307" alt="Image" src="https://github.com/user-attachments/assets/1054263b-0d74-410f-8d23-194a0a3b3f16" />

emcd contributor · 10 months ago

Thanks for pinging the lead developer, @zazer0 . Was tempted to do this soon, but wanted to give the team to work through the huge volume of issues they have before flagging harder. Looks like a couple of Anthropic-associated people are assigned now.

---

Update:

I've been bitten by this several more times in the past week, using the latest 1.0.1xy versions of Claude Code.

And here are the related issues which have come in since this issue was filed: #6859, #7327, #7484, #7570, #7691, #7796

wolffiex collaborator · 10 months ago

This issue has a number of causes. While we are always monitoring instances of this error and and looking to fix them, it's unlikely we will ever completely eliminate it due to how tricky concurrency problems are in general. Feel free to share transcripts and complaints here, we'll keep an eye on this issue and close the many duplicates

emcd contributor · 10 months ago
This issue has a number of causes.

Correct and this was stated in the original post (failed tool calls, transient network failures, server errors).

While we are always monitoring instances of this error and and looking to fix them, it's unlikely we will ever completely eliminate it due to how tricky concurrency problems are in general.

Part of what the original post suggested is that you mitigate the symptoms rather than trying to track down and eliminate every root cause. Some code, which is successfully used to mitigate them in another piece of software, was provided as reference.

I am betting that most people would rather have Claude Code drop an occasional failed hook (or whatever) and continue working than have their entire conversation come to a hard stop until they edit the offending tool uses/results out of the underlying JSON.

---

Open source agentic coders, like Opencode, have essentially caught up to you (and even surpassed you in some areas)... and can even use your models. And other ones, like Codex CLI and Gemini CLI, are rapidly closing the gap.

Your team seems fairly small. If it wants to spend its resources on creating rainbow-colored "ultrathink" keywords rather than mitigating actual user pain, that's fine, but it might not play out well over the course of the next few months.

almirsarajcic · 9 months ago

This is killing me. It stops my autonomous workflows and prevents me from going back and forth in "finished" workflows.
For example, I can't interrupt a subagent and give it guidance because most of the time, in such cases, I get this error.
And sometimes subagents stop working, and I tell them to continue, only to find out they stopped because of this error, which reappears after my prompt.

I've given some examples and analyzed many related issues in https://github.com/anthropics/claude-code/issues/5374.

I've had this error from version v1.0.71, and I still have it in v1.0.126.

emcd contributor · 9 months ago

@wolffiex @igorkofman : Does Claude Code 2.0 use the recently-announced context-management-2025-06-27 beta header or are there plans to have it use this header? Based on my reading of the documentation, it seems to relax the API requirement for matching tool use and tool use result blocks. This would mitigate the majority, if not all of the issues, reported here, if true.

Hint at possibly relaxed API strictness from documentation:

When activated, the API automatically clears the oldest tool results in chronological order, replacing them with placeholder text to let Claude know the tool result was removed

---

Edit: I see the 2.0 release notes do not say anything about the header but have this beacon of hope:

• Hooks: Reduced PostToolUse 'tool_use' ids were found without 'tool_result' blocks errors

Thanks. Let's hope this is a real fix this time.

almirsarajcic · 9 months ago

Happens maybe less often, but still there in 2.0.1 with Sonnet 4.5.

<img width="823" height="248" alt="Image" src="https://github.com/user-attachments/assets/9d7a3341-181b-40be-a563-45674fe94f3e" />

jamestexas · 9 months ago

TypeScript Implementation Proposal

Here's a TypeScript/Node.js adaptation of the Python solution provided in the issue, with additional implementation ideas:

Root Cause

When a tool execution is interrupted (via Ctrl+C or timeout), Claude Code fails to insert the required tool_result block, violating the Claude API specification that every tool_use must be immediately followed by a corresponding tool_result.

TypeScript Implementation

interface AnthropicMessage {
  role: string;
  content: string | ContentBlock[];
}

interface ContentBlock {
  type: string;
  id?: string;
  tool_use_id?: string;
  [key: string]: any;
}

/**
 * Filters out tool_use blocks that have no matching tool_result.
 * Makes an O(n) backwards pass to collect result IDs first.
 */
function filterOrphanedToolUses(messages: AnthropicMessage[]): AnthropicMessage[] {
  const toolResultIds = new Set<string>();
  const filteredMessages: AnthropicMessage[] = [];

  // Backwards pass to collect tool_result IDs
  for (const message of [...messages].reverse()) {
    const content = message.content;

    if (typeof content === 'string') {
      filteredMessages.push(message);
      continue;
    }

    const filteredBlocks: ContentBlock[] = [];

    for (const block of content) {
      if (typeof block !== 'object') {
        filteredBlocks.push(block);
        continue;
      }

      switch (block.type) {
        case 'tool_result':
          if (block.tool_use_id) {
            toolResultIds.add(block.tool_use_id);
          }
          filteredBlocks.push(block);
          break;

        case 'tool_use':
          // Only include tool_use if we have a matching result
          if (block.id && toolResultIds.has(block.id)) {
            filteredBlocks.push(block);
          }
          break;

        default:
          filteredBlocks.push(block);
      }
    }

    if (filteredBlocks.length > 0) {
      filteredMessages.push({
        ...message,
        content: filteredBlocks
      });
    }
  }

  return filteredMessages.reverse();
}

Interrupt Handler Implementation

Additionally, here's how to properly handle interrupts to prevent the issue:

class ToolExecutionManager {
  private activeToolCalls = new Map<string, AbortController>();

  async executeToolWithInterruptHandling(
    toolUseId: string,
    toolName: string,
    toolParams: any,
    onInterrupt?: () => void
  ): Promise<{ result: any; interrupted: boolean }> {
    const abortController = new AbortController();
    this.activeToolCalls.set(toolUseId, abortController);

    // Register interrupt handlers
    const cleanup = () => {
      process.removeListener('SIGINT', interruptHandler);
      process.removeListener('SIGTERM', interruptHandler);
      this.activeToolCalls.delete(toolUseId);
    };

    const interruptHandler = () => {
      abortController.abort();
      if (onInterrupt) onInterrupt();
    };

    process.on('SIGINT', interruptHandler);
    process.on('SIGTERM', interruptHandler);

    try {
      const result = await this.executeTool(
        toolName,
        toolParams,
        abortController.signal
      );
      cleanup();
      return { result, interrupted: false };
    } catch (error) {
      cleanup();

      if (error.name === 'AbortError' || abortController.signal.aborted) {
        // Tool was interrupted - return synthetic result
        return {
          result: {
            type: 'tool_result',
            tool_use_id: toolUseId,
            is_error: true,
            content: 'Tool execution interrupted by user'
          },
          interrupted: true
        };
      }

      throw error;
    }
  }

  private async executeTool(
    name: string,
    params: any,
    signal: AbortSignal
  ): Promise<any> {
    // Actual tool execution logic here
    // Should check signal.aborted periodically for long operations
  }
}

Immediate Mitigation

For immediate relief, users can add this to their shell config to detect and warn about the issue:

# Add to ~/.bashrc or ~/.zshrc
alias claude-check='claude-code history export | grep -c "tool_use" | xargs -I {} sh -c '\''uses={}; results=$(claude-code history export | grep -c "tool_result"); if [ "$uses" -ne "$results" ]; then echo "⚠️  WARNING: Mismatched tool blocks detected ($uses uses, $results results). Consider restarting."; fi'\'''

Testing

I've tested the filter function with various scenarios including:

  • ✅ Properly paired tool_use/tool_result blocks (preserved correctly)
  • ✅ Orphaned tool_use blocks from interrupts (removed as expected)
  • ✅ Mixed valid and orphaned blocks (filters correctly)
  • ✅ String content messages (preserved)
  • ✅ Edge cases like empty arrays

The TypeScript implementation is adapted from the Python solution in the original issue and has been verified to work correctly. The interrupt handler is a proposed approach to proactively prevent the issue.

Hope this helps!

emcd contributor · 9 months ago

Although I have seen no further problems myself, it is clear that many people are still being bitten by this. Here is the latest batch since last update (courtesy of Claude):

Open issues: #7929, #7947, #7991, #8004, #8077, #8187, #8201, #8303, #8325, #8425, #8507, #8612, #8652, #8746, #8763, #8783, #8790, #8817, #8818, #8821, #8847, #8867, #8887, #8893, #8894, #8895, #8903, #8929, #8931, #8940

Closed (as duplicate) issues: #7882, #8190, #8210, #8231, #8233, #8261, #8286, #8337, #8393, #8420, #8553, #8565, #8573, #8822

That is another 44 issues in the matter of several weeks, including after the alleged fix in 2.0.0, @wolffiex @igorkofman .

BY-SAMBO · 9 months ago

Summary of me rn: ahhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh

semikolon · 9 months ago

This is making Claude Code output the HOOKS OUTPUT as USER MESSAGES and KEEPS REACTING to them in a NEVERENDING LOOP basically.

Just unacceptable.

Why is this happening? I can supply you guys with all my hooks or my whole config if you like.

NEVER had this issue before Claude Code 2.0.

semikolon · 9 months ago
For immediate relief, users can add this to their shell config to detect and warn about the issue:

How/when would I use claude-check exactly? Could one surgically modify the transcript file to fix the issue, perhaps? @emcd

emcd contributor · 9 months ago
> For immediate relief, users can add this to their shell config to detect and warn about the issue: How/when would I use claude-check exactly? Could one surgically modify the transcript file to fix the issue, perhaps? @emcd

@semikolon : I am not the one who posted the one-liner. It was @jamestexas . That said, my reading is that it simply detects certain manifestations of the issue and recommends a restart. In my past experience, restarting does not fix the issue; one needs to actually edit the transcript. (But, it is possible that Claude 2.0.x has some sort of rectification on restart now. I have not experienced the issue since Claude 2.0.0, so I cannot confirm whether this is true. And, given the new usage limits, my probability of encountering the problem is about 1/6th of what it used to be....)

If you're asking whether I have suggestions about automatically editing the transcript, I could probably put something together. But, I would be concerned about Anthropic changing the format.... Anthropic really needs to address this from their end.

semikolon · 9 months ago
> > For immediate relief, users can add this to their shell config to detect and warn about the issue: > > > How/when would I use claude-check exactly? Could one surgically modify the transcript file to fix the issue, perhaps? @emcd @semikolon : I am not the one who posted the one-liner. It was @jamestexas . That said, my reading is that it simply detects certain manifestations of the issue and recommends a restart. In my past experience, restarting does not fix the issue; one needs to actually edit the transcript. (But, it is possible that Claude 2.0.x has some sort of rectification on restart now. I have not experienced the issue since Claude 2.0.0, so I cannot confirm whether this is true. And, given the new usage limits, my probability of encountering the problem is about 1/6th of what it used to be....) If you're asking whether I have suggestions about automatically editing the transcript, I could probably put something together. But, I would be concerned about Anthropic changing the format.... Anthropic really needs to address this from their end.

Thanks. That's weird, I've basically only experienced this issue since CC 2.0 / Sonnet 4.5 came out.

But I think it's due to concurrent tool use. I was about to let CC analyze my JSONL transcript files to identify patterns in what causes these errors. Started a subagent on reviewing transcripts and then hit my weekly limit seconds after 😝

emcd contributor · 9 months ago
Thanks. That's weird, I've basically only experienced this issue since CC 2.0 / Sonnet 4.5 came out. But I think it's due to concurrent tool use. I was about to let CC analyze my JSONL transcript files to identify patterns in what causes these errors. Started a subagent on reviewing transcripts and then hit my weekly limit seconds after 😝

😝 Ridiculous, isn't it? I really cannot get any useful work done with CC anymore. I've already started shifting to Codex CLI and have been evaluating Opencode with the latest DeepSeek model (and might give Grok Code Fast 1 another try too, even though my initial impressions were not good last month). At this point, metered API usage with the cheap models might be better than the scraps that are being handed out on the subscriptions. (Had a comfortable working relationship with Claude though. The other models are generally not as good at instruction following and lack the character that Claude has.) Might also go back to using the Mimeogram tool that I made earlier this year before the agentic coders let people tie to their subscriptions; the usage limits via the Claude.ai GUI do not seem as strict, especially if caching of project files is involved.

Anyway, I'm going to leave this bug report open since it is clear that you and others are still seeing problems with mismatched tool use blocks. The major trigger for the problem prior to Claude 2.0.0 was hooks running after parallel tool uses. Not sure what is happening now.

semikolon · 8 months ago
> Thanks. That's weird, I've basically only experienced this issue since CC 2.0 / Sonnet 4.5 came out. > > But I think it's due to concurrent tool use. I was about to let CC analyze my JSONL transcript files to identify patterns in what causes these errors. Started a subagent on reviewing transcripts and then hit my weekly limit seconds after 😝 😝 Ridiculous, isn't it? I really cannot get any useful work done with CC anymore. I've already started shifting to Codex CLI and have been evaluating Opencode with the latest DeepSeek model (and might give Grok Code Fast 1 another try too, even though my initial impressions were not good last month). At this point, metered API usage with the cheap models might be better than the scraps that are being handed out on the subscriptions. (Had a comfortable working relationship with Claude though. The other models are generally not as good at instruction following and lack the character that Claude has.) Might also go back to using the Mimeogram tool that I made earlier this year before the agentic coders let people tie to their subscriptions; the usage limits via the Claude.ai GUI do not seem as strict, especially if caching of project files is involved. Anyway, I'm going to leave this bug report open since it is clear that you and others are still seeing problems with mismatched tool use blocks. The major trigger for the problem prior to Claude 2.0.0 was hooks running after parallel tool uses. Not sure what is happening now.

Should work ok with latest release. Haven't tried but otherwise here's a workaround which has worked for me:

Safety protocol for parallel tool call bug:
Add this to your CLAUDE.md to get rid of this issue until it's fixed by Anthropic (comment)

github-actions[bot] · 7 months ago

This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.

emcd contributor · 7 months ago
This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.

@semikolon (or anyone else): Do we need to keep this issue open? I personally have not been experiencing the problem for quite some time, but some of that may be due to drastically reduced Claude usage since the end of September. I think you have been more on top of it than I have.

almirsarajcic · 7 months ago

I haven't had this issue for a long time either.

marcosalins · 7 months ago

I just experienced it

semikolon · 7 months ago
I just experienced it

Did you send in a full bug report?

rsmaximiliano · 7 months ago

Same as @marcosalins

API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"mes
    sages.160.content.3: unexpected `tool_use_id` found in `tool_result` blocks: 
    toolu_01Tyaf42kExvoow7R56b5jkK. Each `tool_result` block must have a corresponding 
    `tool_use` block in the previous 
    message."},"request_id":"req_011CW9fhj2YuEuwxUhUw5b6j"}
theamrendrasingh · 7 months ago

Faced it today.

API Error: 400 {"type":"error","error":{"type":"invalid_request_error",
      "message":"messages.0.content.0: unexpected
      `tool_use_id` found in `tool_result` blocks: toolu_01TB1ZFhtFTh5M8kuKDQXydt. 
      Each `tool_result` block must have a corresponding `tool_use` block in the previous message."},
      "request_id":"req_011CWCJiiepr2w4ibaSwuPJK"}
Magentron · 6 months ago

Same here:

API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"messages.0.content.0: unexpected `tool_use_id` found in `tool_result` blocks:                       
     toolu_01CUZdViKvgurPeJpF7Y6C26. Each `tool_result` block must have a corresponding `tool_use` block in the previous message."},"request_id":"req_011CWFmFNrMkb74if5EYwu5p"}
emcd contributor · 6 months ago

I assume that everyone who is still seeing the problem has been running the latest version of Claude Code? I know that some people could still be running old versions if they disabled auto-update. (Not doubting the reports, but want to make sure that they are bullet-proof. It is extremely hard to get Anthropic's attention about problems that matter. Need to remove the excuses that they can use to ignore legitimate, serious reports.)

Also, you may want to file separate issues in addition to chiming in here. I started this issue to help Anthropic see the vastness of the problem (and because I was quite irritated about how long they had ignored the huge signal that their customers were giving them). However, there is no guarantee that they won't close this aggregator issue anyway, based on my past experience with them.

noshackleshot · 6 months ago

Claude Code v2.1.4 caught this thing again

napter · 6 months ago

Got this in 2.9.1

databricks-ostrowski · 5 months ago

I still keep receiving:
⏺ PDF too large. Please double press esc to edit your message and try again.

and the loop. Tracing the duplicates, it ends up to this issue.

waldekkot · 5 months ago

The below script might help to reproduce the issue. Test 2, 3 fail with: "API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"messages.1.content.1: tool_use ids must be unique"},"request_id":"req_011CXV1YaSFDMfYRx7nHv37y"}"

Verified failing with: Claude Code 2.1.19, on both: MacOS M3 and x86 Linux.

------------------------------------------------------------------------------------------------------------

#!/bin/bash

Manual test for Claude Code non-interactive mode with tool use

Run this in an external terminal to test Claude Code behavior

set -e

echo "=== Claude Code Non-Interactive Tool Test ==="
echo ""
echo "Claude Code version: $(claude --version)"
echo ""

Create test directory

TEST_DIR=$(mktemp -d)
echo "Test directory: $TEST_DIR"

Create test file

echo "Hello World from test file" > "$TEST_DIR/test.txt"
echo "Created: $TEST_DIR/test.txt"
echo ""

cd "$TEST_DIR"

echo "=== Test 1: No tool use (should work) ==="
echo "Command: claude -p \"what is 2+2?\" --settings '{}' --no-session-persistence --dangerously-skip-permissions"
echo ""
claude -p "what is 2+2?" --settings '{}' --no-session-persistence --dangerously-skip-permissions < /dev/null
echo ""
echo "Exit code: $?"
echo ""

echo "=== Test 2: Single tool use (may fail with v2.1.x bug) ==="
echo "Command: claude -p \"read test.txt\" --settings '{}' --no-session-persistence --dangerously-skip-permissions"
echo ""
claude -p "read test.txt" --settings '{}' --no-session-persistence --dangerously-skip-permissions < /dev/null || true
echo ""
echo "Exit code: $?"
echo ""

echo "=== Test 3: With --tools Read (may fail) ==="
echo "Command: claude -p \"read test.txt\" --settings '{}' --no-session-persistence --dangerously-skip-permissions --tools Read"
echo ""
claude -p "read test.txt" --settings '{}' --no-session-persistence --dangerously-skip-permissions --tools Read < /dev/null || true
echo ""
echo "Exit code: $?"
echo ""

echo "=== Test 4: With stream-json output (may fail) ==="
echo "Command: claude -p \"read test.txt\" --settings '{}' --no-session-persistence --dangerously-skip-permissions --output-format stream-json --verbose"
echo ""
claude -p "read test.txt" --settings '{}' --no-session-persistence --dangerously-skip-permissions --output-format stream-json --verbose < /dev/null 2>&1 | head -5 || true
echo ""
echo "Exit code: $?"
echo ""

Cleanup

rm -rf "$TEST_DIR"
echo "=== Cleanup done ==="
echo ""
echo "If tests 2-4 failed with 'tool_use ids must be unique' or 'tool use concurrency issues',"
echo "this confirms the Claude Code v2.1.x bug in non-interactive mode."

jycouet · 5 months ago

Thank you for the script, I can confirm that I have the same failing on Windows WSL - x64 :/

alejopijuan · 5 months ago

Session Recovery Script for sessions-index.json Out of Sync

If you're experiencing "session not found" errors, the issue may be that your sessions-index.json is out of sync with the actual session .jsonl files on disk. This can happen when session files exist but aren't listed in the index.

Quick Check

# Count sessions in index vs on disk
PROJECT_DIR=~/.claude/projects/-Users-youruser-path-to-project
INDEX_COUNT=$(cat "$PROJECT_DIR/sessions-index.json" | jq '.entries | length')
FILE_COUNT=$(ls -1 "$PROJECT_DIR"/*.jsonl 2>/dev/null | grep -v sessions-index | wc -l)
echo "Index has $INDEX_COUNT sessions, but $FILE_COUNT .jsonl files exist"

If the counts don't match, your index is out of sync.

Recovery Script

This Python script rebuilds the sessions index from all .jsonl files found on disk:

#!/usr/bin/env python3
import json
import sys
from pathlib import Path
from datetime import datetime

def rebuild_sessions_index(project_dir):
    """Rebuild sessions-index.json from all session .jsonl files"""
    project_path = Path(project_dir).expanduser()
    index_path = project_path / "sessions-index.json"
    
    # Backup existing index
    if index_path.exists():
        backup_path = project_path / "sessions-index.json.bak"
        index_path.rename(backup_path)
        print(f"Backed up existing index to {backup_path}")
    
    # Read existing index or create new
    if (project_path / "sessions-index.json.bak").exists():
        with open(project_path / "sessions-index.json.bak") as f:
            index = json.load(f)
    else:
        index = {"version": 1, "entries": [], "originalPath": str(project_path)}
    
    existing_ids = {e['sessionId'] for e in index.get('entries', [])}
    added = 0
    
    # Scan for all .jsonl files
    for jsonl_file in project_path.glob("*.jsonl"):
        if jsonl_file.name == "sessions-index.json":
            continue
        
        session_id = jsonl_file.stem
        if session_id in existing_ids:
            continue
        
        # Extract metadata from session file
        first_prompt = ""
        message_count = 0
        
        try:
            with open(jsonl_file) as f:
                for line in f:
                    try:
                        d = json.loads(line.strip())
                        message_count += 1
                        
                        # Try to find first user message
                        if d.get('type') == 'user' and not first_prompt:
                            msg = d.get('message', {})
                            if isinstance(msg, str):
                                first_prompt = msg[:100]
                            elif isinstance(msg, dict):
                                content = msg.get('content', '')
                                if isinstance(content, str):
                                    first_prompt = content[:100]
                                elif isinstance(content, list):
                                    for c in content:
                                        if isinstance(c, dict) and c.get('type') == 'text':
                                            first_prompt = c.get('text', '')[:100]
                                            break
                    except:
                        pass
            
            stat = jsonl_file.stat()
            
            # Only add if it looks like a real session (has messages)
            if message_count > 5:
                entry = {
                    "sessionId": session_id,
                    "fullPath": str(jsonl_file),
                    "fileMtime": int(stat.st_mtime * 1000),
                    "firstPrompt": first_prompt if first_prompt else "(recovered session)",
                    "messageCount": message_count,
                    "created": datetime.fromtimestamp(stat.st_ctime).isoformat() + "Z",
                    "modified": datetime.fromtimestamp(stat.st_mtime).isoformat() + "Z",
                    "gitBranch": "",
                    "projectPath": str(project_path.parent),
                    "isSidechain": False
                }
                index['entries'].append(entry)
                added += 1
                print(f"✓ Added: {session_id[:8]}... - {first_prompt[:50] if first_prompt else '(no prompt)'}")
        except Exception as e:
            print(f"✗ Skipped {session_id}: {e}", file=sys.stderr)
    
    # Save rebuilt index
    with open(index_path, 'w') as f:
        json.dump(index, f, indent=2)
    
    print(f"\n✓ Rebuilt index with {len(index['entries'])} total sessions ({added} newly added)")
    return added

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: rebuild_sessions.py <project-directory>")
        print("Example: rebuild_sessions.py ~/.claude/projects/-Users-you-Documents-myproject")
        sys.exit(1)
    
    rebuild_sessions_index(sys.argv[1])

Usage

# Save script as rebuild_sessions.py
chmod +x rebuild_sessions.py

# Run on your project
./rebuild_sessions.py ~/.claude/projects/-Users-youruser-path-to-project

# Restart Cursor/VSCode to pick up the updated index

What This Fixes

This script addresses the symptom (out-of-sync index), not the root cause (orphaned tool_result blocks). If your session still won't resume after rebuilding the index, the session file itself is likely corrupted per the main issue description.

Related Issues

  • #22462 - Sessions index not being updated
  • #22724 - My original report (closed as duplicate)

Hope this helps others recover their "lost" sessions!

soaringk · 5 months ago

Sample provided:

  1. This error only occurs when I use the Opus 4.5 model.
  2. The problem does not occur when downgrading Claude Code to version 2.0.56. The problem also occurs when upgrading to version 2.0.57.
{"type":"error","error":{"type":"invalid_request_error","message":"messages.1: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_vrtx_016rTtBk2j9afpfgXJRMYJeB. Each `tool_use` block must have a corresponding `tool_result` block in the next message."}}
muddi900 · 5 months ago

I am on version 2.1.38, using Sonnet4.5 and am getting the same error:

API Error: 400
     {"type":"error","error":{"type":"invalid_request_error","message":"messages.0.content.0: unexpected
     `tool_use_id` found in `tool_result` blocks: toolu_01Rp64mmyYTaVWG2cU1i8rVV. Each `tool_result` block
     must have a corresponding `tool_use` block in the previous
     message."},"request_id":"req_011CXysR3vRypAHGiA74K4Ec"}
tispratik · 5 months ago

Occurring on Opus 4.6 as well.

The workaround I found is to fire up a new Claude session and ask it to load context from previous one and continue.

My prompts below

❯ unable to resume previous clause session 8d0b2cff-5afe-4dc3-89c3-bcf24284dcfd to to API Error: 400
{"type":"error","error":{"type":"invalid_request_error","message":"messages.0.content.0: unexpected
`tool_use_id` found in `tool_result` blocks: toolu_013xYH2UCe7HttVDY6TfNt4. Each `tool_result`
block must have a corresponding `tool_use` block in the previous
message."},"request_id":"req_011CY5GVm41Behp9KmP274eg"}```

⏺ That session can't be resumed due to a corrupted conversation state (a tool_result referencing a tool_use that no longer exists in context, likely from message compression).

This is a fresh session now — what would you like to work on?

❯ can you fix the previous session 8d0b2cff-5afe-4dc3-89c3-bcf24284dcfd which has all the context or load its context
yurkomik · 4 months ago

I can confirm this issue exists for a Claude Chrome extension. Happens when you interrupt (click stop) frozen screenshot tool call. Makes crome extention unusable.

TweedBeetle · 4 months ago

Specific repro: parallel tool calls + long session + /compact = orphaned tool_result at messages[0]

Hit this today in a session with 7192 JSONL lines. Sharing because it's a different failure mode than what's described above - the existing Python fix doesn't cover it.

What happened

Claude made 3 parallel tool calls in one assistant message (Read + Glob + Glob). In the JSONL, each result landed as a separate user record, all with the same parentUuid pointing to the assistant:

assistant 6ac87c53  (content: [text, thinking, text, tool_use, tool_use, tool_use])
  ├── user 6418b758  (tool_result for Read)      ← dead-end branch
  ├── user a2c674c9  (tool_result for Glob)      ← dead-end branch
  └── user 5f52ce89  (tool_result for Glob)      ← main chain (next assistant's parentUuid points here)
        └── assistant d63833f6 (thinking)
              └── ...conversation continues...

Only one of the three tool_results ends up on the main parent chain. The other two are dead-end branches.

Why /compact breaks

The session was very long (~7000 lines). When /compact builds its API message array, context management appears to trim the start of the window. In my case the window started at 5f52ce89 - the tool_result - without including the preceding assistant message 6ac87c53 that had the three tool_uses.

Result: messages[0].content[0] is a tool_result with no corresponding tool_use in any previous message:

messages.0.content.0: unexpected `tool_use_id` found in `tool_result` blocks: toolu_01S8VCh6TJAEmLP6Ktu7HpUm.
Each `tool_result` block must have a corresponding `tool_use` block in the previous message.

This is the REVERSE of the usual case

The existing Python fix (backward pass, filter orphaned tool_uses) handles: tool_use without a result.

This case is: tool_result without a preceding tool_use - which happens because the context window cuts between the tool_use message and the tool_result message.

Fix that worked (JSONL repair)

For anyone hitting this, here's a script that repairs the JSONL directly. It strips the tool_use blocks from the offending assistant message and converts the orphaned tool_result messages to plain text messages:

import json, shutil

SESSION = "/path/to/your/session.jsonl"
shutil.copy(SESSION, SESSION + ".bak")

# Step 1: find the orphaned tool_result UUIDs and their sourceToolAssistantUUID
# Step 2: for that assistant message - remove tool_use blocks, set stop_reason to end_turn
# Step 3: convert orphaned tool_result user messages to plain text messages

with open(SESSION) as f:
    lines = f.readlines()

# Collect all tool_use IDs and which assistant message they belong to
tool_use_map = {}  # tool_use_id -> assistant_uuid
for line in lines:
    try:
        obj = json.loads(line)
    except:
        continue
    if obj.get("type") == "assistant":
        for block in obj.get("message", {}).get("content", []):
            if block.get("type") == "tool_use":
                tool_use_map[block["id"]] = obj["uuid"]

# Collect all tool_result IDs that appear in the conversation
tool_result_ids = set()
for line in lines:
    try:
        obj = json.loads(line)
    except:
        continue
    if obj.get("type") == "user":
        for block in obj.get("message", {}).get("content", []):
            if block.get("type") == "tool_result":
                tool_result_ids.add(block.get("tool_use_id"))

# Find assistant UUIDs that have tool_uses with NO corresponding result
orphaned_assistant_uuids = set()
for tool_use_id, assistant_uuid in tool_use_map.items():
    if tool_use_id not in tool_result_ids:
        orphaned_assistant_uuids.add(assistant_uuid)

new_lines = []
for line in lines:
    try:
        obj = json.loads(line)
    except:
        new_lines.append(line)
        continue

    if obj.get("uuid") in orphaned_assistant_uuids and obj.get("type") == "assistant":
        # Strip tool_use blocks, fix stop_reason
        content = [c for c in obj["message"]["content"] if c["type"] != "tool_use"]
        obj["message"]["content"] = content
        obj["message"]["stop_reason"] = "end_turn"

    elif obj.get("type") == "user":
        content = obj.get("message", {}).get("content", [])
        new_content = []
        for block in content:
            if block.get("type") == "tool_result" and block.get("tool_use_id") not in tool_result_ids:
                # Convert orphaned tool_result to text
                new_content.append({"type": "text", "text": f"[tool result removed: {block.get('tool_use_id')}]"})
            else:
                new_content.append(block)
        obj["message"]["content"] = new_content

    new_lines.append(json.dumps(obj, separators=(',', ':')) + '\n')

with open(SESSION, 'w') as f:
    f.writelines(new_lines)
print("Done. Original backed up to .bak")

After running this, /compact worked immediately.

The underlying issue

The JSONL structure for parallel tool calls creates a branched tree - only one branch is the "main" thread. But that orphaned tool_result in the main thread is valid as long as the corresponding assistant tool_use is also in the context window. The bug is that the compaction window can be trimmed to start AFTER the tool_use message but BEFORE the tool_result, creating an invalid API request.

The fix at the compaction layer would be: when trimming the start of the context window, if the first message is a tool_result, walk back further until you find a message that isn't a tool_result (or discard those tool_results along with their paired tool_uses).

kolkov · 4 months ago

@wolffiex It seems the problem is only getting worse... The agent is now stopping and acting up very frequently. The only thing that helps is ESC and restarting with the "continue" request. If the Anthropic vibe coding approach can't fix the problem, I think they should open-source the agent, and we'll fix it!

junaidtitan · 4 months ago

We kept hitting this in our own sessions — especially after pruning or compaction. Built cozempic to fix it and open-sourced it.

As of v0.7.6, it handles orphaned tool_result blocks in two ways:

  1. Post-treatment validation — every prune operation now auto-scans for tool_result blocks whose matching tool_use was removed and strips them out before saving
  2. Doctor checkcozempic doctor detects orphans in existing sessions and --fix repairs them
pip install cozempic
cozempic doctor        # detect
cozempic doctor --fix  # repair with backup

Would appreciate feedback from anyone dealing with this.

bkscottdev · 4 months ago
We kept hitting this in our own sessions — especially after pruning or compaction. Built cozempic to fix it and open-sourced it. As of v0.7.6, it handles orphaned tool_result blocks in two ways: 1. Post-treatment validation — every prune operation now auto-scans for tool_result blocks whose matching tool_use was removed and strips them out before saving 2. Doctor checkcozempic doctor detects orphans in existing sessions and --fix repairs them pip install cozempic cozempic doctor # detect cozempic doctor --fix # repair with backup Would appreciate feedback from anyone dealing with this.

Didn't fix the orphaned tool_result in my case. Says its ok but still getting the error. Restarting did the trick.

lyf571321556 · 4 months ago

same issue

kolkov · 4 months ago

We reverse-engineered cli.js across 12 npm versions and analyzed 1,571 sessions (148,444 tool calls, 8,007 orphaned) to find the root causes of this and related hang/orphan issues.

Full analysis with code offsets and fix proposals: #33949

emcd contributor · 4 months ago
We reverse-engineered cli.js across 12 npm versions and analyzed 1,571 sessions (148,444 tool calls, 8,007 orphaned) to find the root causes of this and related hang/orphan issues. Full analysis with code offsets and fix proposals: #33949

Nice work... work that you should not have had to perform if Anthropic were as accountable and transparent as they claim. (If they were accountable, they would actually respond to serious issues. And, if they were transparent, they would open source Claude Code. It is not their "secret sauce"; their models are good, Claude Code is not.) Sadly, they have serious issues which have been open for many months, some of which have hundreds of comments (and more than a thousand, in one case), and they simply sit on those issues without responding. Reverse engineering is the easy part; getting Anthropic to actually listen to what their paying customers are telling them is the hard part. I wish you success but will not be holding my breath.

kolkov · 4 months ago

Fully agree. We've been saying the same thing since September 2025:

  • #7243 — "Open source the relevant modules: Let the community fix what you apparently cannot"
  • #14642 — "I'm increasingly switching to my own Go-based agent using Anthropic API directly, which is significantly more expensive but at least doesn't crash my machine"

Both were auto-closed and locked by the bot without any response from the team. In fact, across all related issues (#26224, #25979, #20171, #24688) — zero replies from Anthropic. The only team response in 6+ months was @wolffiex on this issue back in September 2025: "it's tricky". That's it.

What's frustrating is that Claude Code is genuinely a great tool at its core. But every release ships a pile of new features while the basic workflow — submit prompt, get response, press ESC to cancel — remains broken. Priorities seem inverted: new shiny features over core reliability.

As Karpathy put it, the real value comes when you know exactly what you want and engineer it properly — not when you vibe-code your way through production. We wrote about this exact problem: From Vibe Coding to Agentic Engineering.

kolkov · 4 months ago

We made ready-to-use prompts for @bcherny — just copy-paste into Claude Code and it will fix itself: 6 prompts to fix Claude Code with Claude Code

Workflow setup, 3 bug fixes, full codebase audit, and 35+ enterprise regression tests — all copy-paste ready. Since 100% of Claude Code is written by Claude Code, should take about 6 hours.

liaozhaoyan · 3 months ago

Also experiencing this issue in VSCode extension (reported in #45036). Subscribing for updates on the resolution progress.