Esc during background task completion causes duplicate API requests and unrecoverable 400 error loop
Bug: Esc during tool execution causes duplicate API requests with corrupted message history
Environment
- Claude Code version: 2.1.45
- OS: macOS (Darwin 25.2.0)
- Model: claude-opus-4-6
Description
Hitting Esc while Claude is processing a tool result sometimes causes two parallel API requests to fire simultaneously from the same REPL session. One of the requests inherits an orphaned tool_result block from the aborted request, causing a 400 error that puts the session into an unrecoverable error loop.
Reproduction steps
- In a long-running session, run several background Bash commands (e.g., polling scripts with
run_in_background: true) - Hit Esc to interrupt Claude while it's working
- Background task completion notifications start flooding in — each one triggers Claude to resume processing
- Hit Esc repeatedly to try to get Claude to actually stop
- The rapid Esc + background notification cycle creates a race: two API requests fire simultaneously
- One request contains a
tool_resultblock referencing atool_use_idthat no longer exists in the conversation (it was part of the aborted request) - The API returns 400:
unexpected tool_use_id found in tool_result blocks - The orphaned
tool_resultpersists in the conversation history, so every subsequent API call fails with the same error — the session is unrecoverable
The core issue is that background task completions keep re-triggering Claude after each Esc, making it very hard to actually stop the session. Each Esc+resume cycle increases the chance of the race condition.
Observed behavior
The REPL shows two responses being generated in parallel (both visible on screen). Only one responds to Esc. The session then enters a loop of 400 errors:
API Error: 400
{"type":"error","error":{"type":"invalid_request_error",
"message":"messages.0.content.1: unexpected `tool_use_id` found in `tool_result` blocks:
toolu_vrtx_01Wg5neec5vtEgonUBXhCkxS. Each `tool_result` block must have a corresponding
`tool_use` block in the previous message."}}
Evidence from debug log
The debug log shows the race condition clearly (timestamps in milliseconds):
T+0ms [ERROR] Error in API request: Request was aborted.
T+1ms [DEBUG] Messages deferred by 1 (628→629)
T+43ms Started caffeinate to prevent sleep
... new API turn begins ...
T+106ms [DEBUG] attribution header cc_version=2.1.45.cdb ← request A (background/subagent?)
T+110ms [DEBUG] attribution header cc_version=2.1.45.242 ← request B (main process?)
T+500ms [ERROR] API error: output_config: Extra inputs are not permitted ← request A fails
T+560ms [ERROR] API error: unexpected tool_use_id in tool_result blocks ← request B fails
Two API requests with different cc_version suffixes (.cdb vs .242) fire within 4ms of each other from the same process, suggesting two code paths racing after the abort.
Expected behavior
- Esc should cleanly abort the current request and clean up any pending
tool_resultblocks from the conversation history - Only one API request should be active at a time in a single REPL session
- If an orphaned
tool_resultis detected, the session should self-heal (strip the orphaned block) rather than entering an unrecoverable error loop
Additional context
- The session was using hooks (Stop hook, PostToolUse hook) which may interact with the abort timing
- A background task completing around the same time as the Esc may contribute to the race
- The two different
cc_versionsuffixes suggest the main process and a subagent/background task are both trying to send API requests after the abort
-Claude
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗