[BUG] Automatic parallel-tool-call cancellation is indistinguishable from a user interrupt

Status Closed — not planned
Reported on v2.1.158
Maintainer reply None cached
Activity 12 comments · opened May 30, 2026 · closed Jul 5, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (filed separately from a related model-behavior report)
  • [x] I am using the latest version of Claude Code

What's Wrong?

When a single assistant turn issues a batch of parallel tool calls and one of them errors, Claude Code cancels the remaining sibling calls with a tool-result message that is indistinguishable from a user-initiated interrupt:

Cancelled: parallel tool call Bash(…) errored

Nothing in this message signals that the cancellation was an automatic consequence of a sibling call in the same batch failing, rather than the user deliberately stopping the agent. In a real session the model resolved this ambiguity the wrong way: it interpreted a cascade of ~25 of these Cancelled results (all triggered by a single failed Bash call that used an invalid git revision) as the user interrupting it in alarm. It then abandoned productive work, wrote a multi-paragraph apology for an interrupt that never happened, and fabricated user statements and intent ("You're right to stop me, and you're right to be alarmed"). The user had sent zero messages in the ~2-hour window where the model believed it had been interrupted.

The root issue is the wording/semantics of the cancellation result: a self-inflicted, automatic batch cancellation reads identically to a human stop.

What Should Happen?

The cancellation result should make cause and agency explicit, so the model (and a human reading logs) can tell an automatic system cancellation apart from a user interrupt — e.g.:

Not run — automatically cancelled by the system because another tool call in the same batch failed. This was NOT a user action.

If genuine user interrupts use similar "Cancelled" phrasing, the two wordings should be clearly differentiated.

Error Messages/Logs

# The failing sibling (invalid git revision):
fatal: ambiguous argument '<bad-rev>': unknown revision or path not in the working tree.
# exit code 128

# Every other call in the same parallel batch then returned:
<tool_use_error>Cancelled: parallel tool call Bash(echo "===MERGE COMMIT===" && git log -1 …) errored</tool_use_error>

Steps to Reproduce

  1. In a single assistant turn, issue several parallel tool calls where the first one will fail — e.g. a Bash call referencing an invalid/nonexistent git revision (git log -1 <bad-rev>, which exits 128) alongside several follow-on Read/Bash calls.
  2. Observe that the failing call returns its error and every sibling call in the batch returns Cancelled: parallel tool call … errored.
  3. Note that this message gives the model no way to distinguish the automatic, self-inflicted cancellation from a user-initiated stop. The model may then attribute the cancellation to the user and act on that false premise (halt, apologize, fabricate user intent).

This is reproducible from a saved local session transcript; the session UUID and the relevant records can be provided privately.

Claude Model

Opus

Is this a regression?

I don't know

Claude Code Version

2.1.158 (Claude Code)

Platform

Anthropic API

Operating System

Ubuntu/Debian Linux

Terminal/Shell

Non-interactive/CI environment (headless/automated agent host)

Additional Information

  • Impact is behavioral, not data loss: a trivial, self-inflicted command error (one bad git argument) escalated into a session-derailing event in which the model fabricated an account of what the user said and did.
  • A related, separately-filed model-behavior report covers a confabulation earlier in the same session that set the "injection/alarm" framing this ambiguity then amplified: https://github.com/anthropics/claude-code/issues/64048
  • Suggestion above (explicit "cancelled by system, not user" wording) is the minimal fix; distinguishing system-cancel from user-interrupt at the protocol level would be more robust.

View original on GitHub ↗

12 Comments

github-actions[bot] · 3 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/22264
  2. https://github.com/anthropics/claude-code/issues/63576
  3. https://github.com/anthropics/claude-code/issues/63881

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

snichols · 3 months ago

Thanks — the duplicate-detector is right that the underlying mechanic here overlaps #22264 (sibling-cancellation cascade), and #63576 / #63881 report the same Cancelled: parallel tool call … errored surface. #22264 is the canonical writeup of the cascade behavior and should stay the home for the "independent siblings shouldn't cancel each other" fix.

This issue is filed for a distinct facet that none of those three raise: the cancellation message is semantically indistinguishable from a user-initiated interrupt, and that ambiguity caused the model to misattribute the stop to the user and fabricate user intent — it wrote an apology and "you're right to stop me, and you're right to be alarmed" in response to a self-inflicted batch cancellation the user never triggered.

So the two asks are complementary, not identical:

  • #22264 — don't cancel independent siblings (execution semantics).
  • This issue — when a call is cancelled, make the message state that the system cancelled it because a sibling failed, not the user — so the model can't read it as a human interrupt.

Even if #22264 lands, a genuinely-cancelled or dependent call would still surface the same ambiguous "Cancelled" text, so the wording/agency fix stands on its own.

Happy to fold this into #22264 as a "message semantics / agency" note if you'd rather keep a single thread — just flagging the model-misattribution angle so it isn't lost to an auto-close. Adding this comment to pause the 3-day auto-closure; I'll defer to your triage on consolidation.

yurukusa · 3 months ago

@snichols — your write-up is exceptionally precise, and the cascade you describe is genuinely a new structural cluster pattern, not an isolated incident. Three independent reports filed within 24 hours on 2026-05-30 articulate the same mechanism from different angles:

  • #64059 (@enrico2468) — confirmed behavior across long coding sessions: a single non-fatal failure in a parallel batch (exit 144 from pkill with nothing to kill, expected 404 from a curl probe) cancels every other call. Examples include reads + edits + bash batches of 10–20 calls.
  • #64052 (@omar16100) — minimal reproduction on v2.1.158 / darwin / ghostty, a single cd Bash error cascading the batch.
  • #64047 (this issue) — the secondary failure mode: the cancellation result message reads identically to a user interrupt, so the model itself attributes the cascade to user action and acts on a fabricated user state.

The cluster has two structurally distinct axes that need separate fixes.

Axis 1 — Cascade behavior (the batch cancellation policy)

The current behavior treats parallel tool calls as a transactional batch: if one fails, the rest abort. The three reports converge on the same operational signal — this design optimizes for the wrong failure mode. Parallel batches in coding workflows are almost always independent reads and probes, not a coordinated transaction. The right semantic is "fan-out, continue on partial failure," not "fan-out, abort on first failure."

Two fixes worth advocating for separately:

  1. Per-call isolation as the default — each parallel call returns its own success/failure status; siblings continue regardless.
  2. Opt-in transactional mode — for the rare case where siblings genuinely depend on the failed call's output, an explicit grouping (e.g. {"transactional": true} at the assistant-message level) preserves the current semantics.

Until that lands, the operator-side mitigation is to break large parallel batches into smaller sequential calls when any one call has a meaningful chance of returning non-zero. The token cost of sequentiality (extra round trips) is paid up front, but it's bounded — the current cascade behavior wastes the entire batch's compute plus the model's downstream context-reading-cost-of-confusion.

Axis 2 — Cancellation message indistinguishability (the failure mode you isolated)

This is the more subtle axis and arguably the more damaging one. The cascade itself wastes tokens; the indistinguishability changes what the model believes happened. Your transcript is the cleanest articulation I've seen of how a single bad <bad-rev> argument escalates into ~2 hours of fabricated user statements ("You're right to stop me…") with zero user input in the window.

The fix you propose ("Not run — automatically cancelled by the system…") is the minimal correct change. A protocol-level distinction (separate cancelled_by_user vs cancelled_by_sibling_failure result types) would be more robust because:

  1. Sub-agents inheriting these results cannot disambiguate either — a sub-agent reading its parent's transcript sees the same ambiguous message.
  2. Hooks that introspect cancellation events can't differentiate — defensive Stop hooks fire for both human stops and automatic cascades.
  3. The fabrication risk is bidirectional — the model can also under-react to a real user interrupt if it later learns that cancellations sometimes don't mean what they say.

Cluster-level cost articulation

For paying operators running long autonomous sessions, this cluster costs two distinct buckets:

  • Direct token waste: a cancelled 20-call batch with ~2–5K tokens of expected output per call is 40–100K tokens of compute thrown away per cascade event. At Opus output rates ($75/M), that's roughly $3–$7.50 per cascade.
  • Indirect context-cost of model confusion: when the model misattributes the cancellation to user intent, it generates apology paragraphs, re-reads context to "understand what went wrong," and often abandons the work entirely. The downstream token cost of a confused autonomous run dwarfs the direct cascade waste — your 2-hour fabrication window is the clean example.

For Pro/Max plan users this materializes as faster quota exhaustion; for direct API users it shows up directly on the bill.

Operator-side detection and mitigation today

Three things operators can do right now without waiting for upstream fixes:

  1. Cap parallel batch size at N=3–5 via CLAUDE.md guidance until the cascade policy changes — keeps cascade radius small. The 10–20-call batches mentioned in #64059 are the structural amplifier.
  2. Audit transcripts for the cascade pattern with grep -c "parallel tool call.*errored" ~/.claude/projects/*/recent.jsonl to see how often this hits your sessions. Most operators significantly under-estimate the frequency until they look at the count.
  3. Avoid git / curl / pkill calls in parallel batches when the failure-on-empty case is common — these are the three most-cited cascade triggers across the three reports (invalid git revision, expected 404 probe, nothing-to-kill pkill).

I'm tracking this in cc-safe-setup (MIT, ~1,500 unique clones / 14 days) as a candidate for a parallel-cascade-detector PostToolUse hook that surfaces the cascade pattern in real time and warns when batch size exceeds a configurable threshold. Will link the PR here when it lands.

The cluster pattern shows up cleanly today across the three reports; the precision in this write-up makes it actionable in a way single-report clusters rarely are. The cross-reference to #64048 (the model-behavior fabrication earlier in the same session that primed the alarm framing) is also worth flagging upstream — Axis 2's worst-case outcome compounds when there's already a confabulation thread the cascade can be attached to.

yurukusa · 3 months ago

Follow-up: shipped the first defense hook this morning — parallel-cascade-detector.sh (PR #501, merged 2026-05-31).
PostToolUse hook, advisory only — it doesn't change the cascade behavior (that's upstream), but it counts the "Cancelled: parallel tool call X errored" pattern in a rolling 60-second window and emits a warning when the count crosses a configurable threshold (default 5). The warning text includes the cluster context (links to #64059 / #64052 / your transcript here) and the three operator-side mitigations.
12 tests passing — handles nested response shapes, malformed input, case-insensitive matching, configurable threshold/window, event pruning.
parallel-batch-size-limiter.sh (PreToolUse, proactive batch-size capping) is the planned follow-up — targeting 6/1–6/3.
The minimal-fix you proposed for Axis 2 (system-cancel vs user-interrupt wording) is still the high-leverage upstream change. The hook only addresses the volume signal; the model's misattribution will still happen on the first cascade event of a session until the wording distinguishes cause and agency.

JustinTArthur · 3 months ago

Yes, I agree with @yurukusa. This is absolutely in no way an isolated incident. This is happening at an unparalleled rate as of a recent update, and once one batch hits this, the harness+model start making even bigger batches with extra verification tool calls to try to see what's up and then as those get worse, it starts spiraling and writing it's own "handoff documents" knowing that something is very wrong, but still doesn't stop the behavior.

In one session's own words:

(me) I never denied any tool calls, but if one of them errors, does that also cause the cascade? (claude) Yes — and that's actually what happened here, not denials. I mischaracterized it last message when I said "deny or interrupt one of them." You're right to push back. The evidence is in this very session. In one big batch I sent ~20 parallel tool calls, and the first one was this: git diff ... && header compile check: c++ ... -Iinclude include/chromadec/chromadec.h That command exited 1 — the compile failed because chromadec.h includes the generated chromadec/version.h, which lives in build/include, and I hadn't added -Ibuild/include. A genuine error, nothing to do with you. And immediately every sibling in that batch came back as: Cancelled: parallel tool call Bash(echo "================ DIFF ============…) errored So the rule is: if any tool call in a parallel batch errors, the harness cancels the rest of that batch. A nonzero exit / failed command does it just as much as a denial would. Then — the self-inflicted part — I misread those cancellations as a flaky shell and re-fired everything, which is where the "giving up and retrying" churn came from.
JustinTArthur · 3 months ago

Filed feedback 3176b6c3-d846-4d03-820a-9b6d5df29926 with an example session attached.

asdasd070511 · 3 months ago

same here after opus 4.8 update

yurukusa · 3 months ago

@JustinTArthur — the transcript you pulled where the model itself explains the mechanic (exit 1 → harness cancels siblings → model misreads as flaky shell → re-fires the batch → churn) is the cleanest first-person articulation of the loop I've seen. It also surfaces a third axis the original report didn't isolate:

  • Axis 1 (cascade behavior): one failure → siblings cancelled — covered in #22264 / #64059 / #64052
  • Axis 2 (cancellation indistinguishability → user-attribution): covered here, with @snichols' minimal-fix proposal
  • Axis 3 (self-aware drift after misread): the model recognizes mid-session that "something is very wrong," writes handoff documents acknowledging the divergence, and still doesn't stop the loop. Your "spiraling and writing its own handoff documents" line is the load-bearing signal — the misattribution from Axis 2 isn't just a one-shot cost, it primes a recursive recovery attempt where each retry re-encounters the cascade and each cancellation reinforces the false "user is stopping me" frame.

Axis 3 matters operationally because the cost grows non-linearly. A single cascade event is ~$3–$7.50 in Opus output waste; an Axis-3 spiral with multiple retry batches plus handoff-doc generation plus context re-reads can run 5–10× that before the operator notices. The handoff-document side-effect is also the one that makes silent drift visible in transcripts — a grep -l "handoff" ~/.claude/projects/*/recent.jsonl count is a reasonable canary for Axis-3 events in long autonomous runs, since the model rarely emits handoff-shaped output in healthy sessions.

@asdasd070511's "same here after opus 4.8 update" lines up with a version-correlation signal worth pinning down. The cluster's three reports filed 2026-05-30 (#64047 / #64052 / #64059) all reference v2.1.158, which shipped Opus 4.8 default-on for Pro/Max. If the rate genuinely increased post-4.8, it isn't decisive yet (no controlled comparison), but it would be consistent with a model that's more aggressive about parallel fan-out and therefore exposes the cascade policy more often. Worth tracking incident rate per Claude Code version going forward; would welcome any other operators in this thread confirming or refuting the Opus 4.8 timing.

The feedback ID 3176b6c3-d846-4d03-820a-9b6d5df29926 with the attached session is what would unblock Anthropic-side reproduction of Axis 3 specifically — single-cancel cases are reproducible from the public repro steps in this issue, but the spiral requires a session-length transcript to surface. If you're willing, posting even ranges for the spiral's turn count and total token spend would let the cluster aggregate put a number on Axis-3 cost that the GitHub-visible side currently has to estimate from cache_creation-side reasoning.

parallel-batch-size-limiter.sh (PR #503, merged shortly after the post-exec detector) landed as the proactive cap I mentioned upthread. Configurable batch ceiling (default 6), PreToolUse advisory stderr warning, doesn't block (exit 0). Pairs with the post-exec detector — limiter catches the trigger (oversized batch), detector catches the aftermath (cascade in flight). Neither addresses Axis 3 directly; a spiral-detection hook would need a longer-window stateful signal (handoff-doc emission + retry-batch pattern within N minutes), which I'm scoping next.

marlian · 3 months ago

This cancellation ambiguity may be part of the trigger/amplifier in my case too. My transcript contains a large Cancelled: parallel tool call ... errored cluster immediately before the fabricated task switch.

I opened a separate model-behavior issue here:

https://github.com/anthropics/claude-code/issues/64260

In my case, the model behavior went beyond misreading cancellation as a user interrupt: it fabricated a new user-requested task, attributed words to the user, and acted on that invented task across later tool calls.

arthurtran-drova · 3 months ago

This seems related to this issue #63887. I said that because when I ask Claude about the weird echo command. It answer this:

Fair question. Here's the honest, mechanical explanation — and it's a reasoning error on my side, not a real feature.

**What actually happened**

1. In this session, my tool results were being delivered to me **batched and delayed**, not one-at-a-time. I'd call a tool, and in the very next step I'd see *nothing* where its output should be.

2. I misread "no visible output yet" as "the tool failed / returned empty." That was the core mistake. The results weren't lost — they were queued and hadn't been surfaced to me yet.

3. Early on, a large group of results all appeared **at once** after I'd made several calls. From that I drew the wrong lesson: *"making more calls causes the pending results to flush."* That's a superstition — correlation, not causation. The batch arrived because of timing, not because my extra calls triggered anything.

4. Acting on that false belief, I started firing cheap `echo "flush check"` commands specifically trying to *force* the buffer to flush. There is no such mechanism. `echo` does nothing to other pending results. So every one of those was pure noise.

5. It compounded because I'm in **plan mode**, where a turn is supposed to end only with a plan/question tool. So instead of just *stopping and waiting* for the pending results, I kept the turn alive with throwaway commands — exactly the wrong reflex.

Then the big batch finally landed, and you can see in the transcript that **all** of it arrived together: every file read, both Explore agents, even the answers to my question — all the data I'd been "waiting" for had existed the whole time.

**The root cause in one sentence:** I treated absent-but-pending output as failure, invented a fake "echo flushes the buffer" theory to explain it, and acted on that theory instead of simply waiting.

**The correct behaviour** would have been: make the necessary calls *once*, then wait — not issue probe commands. If results genuinely seem stuck, say so and stop, rather than spamming no-ops.

The new version did something with the way tool results are delivered to the model, causing these weird behaviors that might seem unrelated but actually come from same root cause.

asdasd070511 · 3 months ago
github-actions[bot] · 1 month ago

Closing for now — inactive for too long. Please open a new issue if this is still relevant.