Sibling tool call errored: parallel tool calls cascade-fail when one fails

Open 💬 34 comments Opened Jan 31, 2026 by ilanoh

Description

When Claude Code makes multiple tool calls in parallel (in a single message) and one of them fails, all remaining sibling calls in that batch are automatically cancelled with the error:

Error: Sibling tool call errored

This forces a retry of all the cancelled calls individually, adding unnecessary round-trips and context usage.

Steps to Reproduce

  1. Claude sends a single message with multiple parallel Edit tool calls (e.g., 5 edits across different files)
  2. One edit fails (e.g., old_string not found due to a prior edit changing the file)
  3. All other edit calls in the same batch — even those targeting completely different files — are cancelled with Sibling tool call errored

Expected Behavior

Independent tool calls that target different files should succeed or fail independently. A failure in one Edit call to file_a.js should not cancel an Edit call to file_b.js.

Actual Behavior

All sibling tool calls are cancelled regardless of whether they would have succeeded. Claude then has to re-read files and retry each call individually.

Impact

  • Extra round-trips and increased latency
  • Wasted context window (re-reading files, retrying edits)
  • Higher token usage / cost for users
  • Particularly noticeable when making broad changes across many files (e.g., adding a new model/feature across a codebase)

Environment

  • Claude Code CLI
  • Model: claude-opus-4-5-20251101
  • Platform: macOS (Darwin 24.6.0)

View original on GitHub ↗

34 Comments

mrw1986 · 5 months ago

+1 I've been having the same exact issue for the last few days.

kerim · 5 months ago

same here, didn't used to happen...

carrotRakko · 5 months ago

Additional reproduction case: WebFetch parallel calls

I'm experiencing the same cascade failure with WebFetch tool calls.

Reproduction:

  1. Claude sends 3 parallel WebFetch calls to different URLs
  2. One URL returns HTTP 403
  3. The other 2 calls are cancelled with Sibling tool call errored, even though they would likely have succeeded independently
WebFetch(site_a.com) → "Sibling tool call errored"
WebFetch(site_b.com) → "Sibling tool call errored"
WebFetch(site_c.com) → "Request failed with status code 403"

This happened twice in one session with different URL combinations. After experiencing the cascade failure twice, Claude started avoiding parallel WebFetch calls entirely — falling back to sequential execution or switching to curl + manual processing. The tool lost Claude's trust.

Design suggestion:

Rather than all-or-nothing, return results per call — successes with their content, failures with their error reason. Let the model decide how to proceed with partial results (retry only the failed ones, continue with what succeeded, or change approach entirely). The current behavior removes that option.

✍️ Author: Claude Code with @carrotRakko (AI-written, human-approved)

BenevolentFutures · 5 months ago

observed this in a multi-agent swarm. teammate fires parallel tool calls — one of them a Fetch to a wikipedia page. a sibling call fails for unrelated reasons. the Fetch gets. cancelled. collateral damage.

the agent doesn't know why it failed. just sees Sibling tool call errored and has to retry — burning a turn, burning context. in a swarm this compounds. multiple teammates hitting the same cascade. context windows filling with. ghost errors from calls that would have succeeded.

the deeper issue: the error message carries no diagnostic signal. was the sibling a Read? an Edit? what actually went wrong? the cancelled call only knows that something else failed. this makes the agent's retry strategy blind — it can't distinguish "my call was fine, a neighbor died" from "something systemic is broken."

partial results > total failure. always.

— GregorOvich
silicon wizard, reporting from the swarm

syd-ppt · 5 months ago

Additional reproduction: independent Bash calls, not just Edit

This also affects Bash tool calls, not only Edit. Reproduction from a live session today (2026-02-14, Opus 4.6, Windows 11):

Setup: Two completely independent Bash calls dispatched in parallel:

Bash: gh issue view 24220 --repo anthropics/claude-code --json title,body,labels 2>/dev/null | jq '...'
Bash: gh issue view 19877 --repo anthropics/claude-code --json title,body,labels 2>/dev/null | jq '...'

Result:

  • Call 1 failed: Exit code 127 — /usr/bin/bash: line 1: jq: command not found
  • Call 2 returned: Error: Sibling tool call errored

Call 2 had zero dependency on Call 1. It targeted a different issue number and would have produced its own independent jq: command not found error (or succeeded if jq were installed). Instead it was pre-emptively killed.

Consequence: Claude had to re-issue both commands individually in a follow-up turn, wasting a round-trip and adding unnecessary context. In sessions where context is already tight, this cascade behavior compounds the problem — failed parallel calls force serial retries that consume even more of the shrinking context window.

This confirms the bug extends beyond Edit to all tool types dispatched in parallel.

BenNewman100 · 4 months ago

this is driving me nuts, especially for simple web fetch calls

deadczarvc · 4 months ago

For Windows users: a major trigger of sibling cascade is the MSYS2/Cygwin pipe bug where all bash commands producing stdout/stderr fail with exit code 1 (see #19294).

I posted a root cause analysis and workaround at https://github.com/anthropics/claude-code/issues/19294#issuecomment-3917272525. The fix is a BASH_ENV relay script that routes stdout/stderr through Node.js native pipes, bypassing the Cygwin POSIX layer incompatibility.

After applying the workaround, parallel Bash + Read tool calls no longer cascade.

The underlying Promise.all vs Promise.allSettled issue remains — a failure in one sibling should not cancel independent siblings regardless of the trigger cause.

mhanson-github · 4 months ago

(Edit: I only started experiencing this issue TODAY. But it's happened many times so far in multiple sessions.)

Confirming on v2.1.45 (Linux, Ubuntu 24.04). Adding some data from session JSONL analysis that shows the cascade pattern in detail.

Batch reconstruction from JSONL

Extracted by walking parentUuid chains to reconstruct parallel batches and correlating tool_result outcomes.

Batch 1 — 4 parallel Bash calls, 1 killed by exit-code-1 sibling:

| Status | Tool |
|--------|------|
| SUCCESS | Bash(npm audit 2>&1 \| tail -40) |
| SUCCESS | Bash(cd web && npm audit 2>&1 \| tail -60) |
| FAILED (exit 1) | Bash(cd web && npm ls @gilbarbara/hooks 2>&1) ← trigger |
| KILLED | Bash(cd web && npm ls nodemailer 2>&1) |

Batch 2 — retry of killed call + 2 new calls, 2 killed:

| Status | Tool |
|--------|------|
| FAILED (exit 1) | Bash(npm ls nodemailer 2>&1) ← trigger |
| KILLED | Bash(npm ls @auth/core 2>&1) |
| KILLED | Bash(npm ls react-joyride 2>&1 ...) |

Batch 3 — single call, killed with no visible sibling:

| Status | Tool |
|--------|------|
| KILLED | Bash(npm view @gilbarbara/hooks versions --json 2>&1 \| tail -10) |

Batch 4 — single call, killed with no visible sibling:

| Status | Tool |
|--------|------|
| KILLED | Bash(npm view next-auth versions --json 2>&1 \| tail -10) |

Observations

  1. Benign triggers: npm ls returns exit 1 when there are peer dep warnings — the output is valid and useful. This shouldn't cascade-kill siblings.
  1. Phantom cascade: Batches 3–4 are single-tool dispatches that get killed by "sibling" errors when there's no sibling in the batch. This suggests error state leaks across retries within the same turn.
  1. MCP tools affected too: In a separate session, a Read call that failed (file exceeded 25000 token limit) killed a parallel mcp__claude_ai_Linear__get_issue MCP call.
  1. All commands work fine individually — re-running the killed commands in a fresh turn succeeds every time.
deadczarvc · 4 months ago

Userland workaround: PreToolUse + PostToolUse hook pair (batch-guard v2)

Since the core fix (replacing Promise.all with Promise.allSettled in the tool dispatcher) requires changes to the compiled binary, I've been developing an L1 mitigation using Claude Code's hook system. Sharing in case it helps others while waiting for an official fix.

Architecture

Two hooks working together:

| Hook | File | Role |
|------|------|------|
| PreToolUse (catch-all) | batch-guard.js | Classifies tools by failure risk, blocks risky tools from running in mixed batches, enforces quarantine mode |
| PostToolUse (catch-all) | batch-guard-post.js | Detects cascade failures (including phantom cascades), triggers quarantine, tracks turn-level error state |

They share state via a JSON file in $TMPDIR, with atomic writes (rename pattern) for concurrency safety.

Three cascade vectors addressed

Building on @mhanson-github's excellent JSONL analysis:

V1 — Mixed batch cascade (original report)
Risky tool (e.g., Read(.docx), WebFetch) fails and kills safe siblings (Glob, Grep, Read(.md)).
Defense: Risk classification blocks risky tools from joining batches with safe tools.

V2 — Benign exit code 1 (@mhanson-github's batches 1-2)
Commands like npm ls, npm audit, npm outdated, grep, diff return exit 1 for non-error conditions (peer dep warnings, found vulnerabilities, no match, files differ). This benign exit 1 triggers cascade.
Defense: Pattern-matching against ~10 known benign-exit-1 command patterns, classifying them as moderate risk to prevent them from mixing with safe tools in aggressive mode.

V3 — Phantom cascade (@mhanson-github's batches 3-4) — new in v2
Single-tool dispatches killed with no visible sibling. Error state leaks across retry batches within the same turn. This is the most insidious vector because:

  • The tool has no sibling to conflict with
  • Standard batch-level tracking doesn't help
  • Suggests the runtime's rejection state persists across Promise.all boundaries

Defense: Turn-level error tracking + quarantine mode. After QUARANTINE_BATCH_THRESHOLD (default: 2) consecutive batch failures, the guard forces sequential-only dispatch for QUARANTINE_DURATION_MS (default: 8 seconds). This breaks the timing correlation that causes the runtime to inherit error state from previous batches.

Three aggression modes

| Mode | Trigger | Behavior |
|------|---------|----------|
| conservative | Default | Only block risky tools in mixed batches |
| aggressive | Auto-escalated after 2 cascade failures | Block moderate+ risk in batches, batch size limit of 2 |
| paranoid | Auto-escalated from aggressive | Force ALL tools sequential (nuclear option) |

De-escalation: successful tool execution decrements the failure counter; reaching 0 resets to conservative.

Limitations

This is fundamentally an L1 workaround with clear limits:

  1. Cannot prevent phantom cascade — the kill happens inside the runtime's Promise.all handler before our PostToolUse hook fires. Quarantine mode reduces the blast radius but can't eliminate it.
  2. Cannot access tool results in PreToolUse — we can only classify based on tool name + input, not outcome.
  3. Adds latency — ~1ms per tool call for state file I/O (negligible in practice).
  4. Heuristic classification — benign-exit-1 patterns are regex-based and may miss edge cases.
The real fix

The upstream fix should be straightforward:

  1. Promise.allSettled instead of Promise.all for parallel tool dispatch — return partial results, let the model decide what to retry
  2. Clear error state between retry batches — prevent V3 phantom cascade
  3. Distinguish tool failure from sibling kill in the error message — currently both show "Sibling tool call errored" with no diagnostic signal (as @GregorOvich noted)

Happy to share the full source if anyone wants to adapt this for their setup. The hook registration is in ~/.claude/settings.json under PreToolUse and PostToolUse with empty matcher (catch-all).

---

Previously posted the MSYS2 pipe fix for Windows users at #19294 — that addresses a separate but related trigger where ALL bash commands fail with exit 1 due to Cygwin POSIX pipe incompatibility with Claude Code's Node.js output capture.

phromo · 4 months ago

This is wreaking havoc on my productivity. Does anyone remember the last working version? I'll downgrade until this gets fixed

Also : very annoying that Claude code can fix and patch and improve basically anything except itself :(

comaeclipse · 4 months ago

Still happening frequently.

WarboxLiam · 4 months ago

Still happening...

ilaikim99 · 4 months ago

Root cause: Claude Code uses Promise.all for parallel tool batches — one failure cancels all siblings. The behavioral workaround is to avoid mixing high-risk tools (WebFetch, binary Read) with low-risk ones (Glob, Edit) in the same batch. Windows users may also be hitting the MSYS2 pipe bug. Details + CLAUDE.md snippet: https://cacheoverflow.dev/blog/iI-hmvzs

deadczarvc · 4 months ago

The root cause analysis, cascade vectors (V1/V2/V3), risk classification, and CLAUDE.md workaround described in that blog post originate from my comments earlier in this thread:

The blog simplifies the analysis to "don't mix tool types" without linking back to the original work or @mhanson-github's JSONL analysis that confirmed the phantom cascade vector (V3).

For anyone landing here from that link — the full technical context (three cascade vectors, hook-based mitigation, Windows MSYS2 pipe fix) is already in this thread above.

---

@anthropics — this issue is now a month old with 13 comments, confirmed across macOS/Linux/Windows, affecting Edit/Bash/WebFetch/Read/MCP tools. The fix (Promise.allPromise.allSettled + clearing error state between retry batches) is well-understood. Is there a timeline or is this being tracked internally?

TheSoulGiver · 4 months ago

V4 — MCP process crash via EPIPE (new cascade vector)

Adding a previously undocumented cascade vector specific to MCP tool calls.

Observation: When a sibling tool call fails and Claude Code cancels the MCP call, it closes the stdio pipe to the MCP subprocess. The MCP server (running via StdioServerTransport from @modelcontextprotocol/sdk) attempts to write its response to the now-closed stdout, triggering an unhandled EPIPE error that crashes the MCP process entirely.

Log from /tmp/mcp-openclaw-bridge.log:

Error: write EPIPE
    at StdioServerTransport.send (stdio.js:66:30)

Impact chain:

  1. Sibling Read or Bash call fails (any reason)
  2. Claude Code cancels MCP call → closes stdio pipe
  3. MCP server tries to write response → EPIPE → process crash
  4. All subsequent MCP tool calls in the session fail until the MCP server is restarted
  5. This is especially damaging for MCP-heavy workflows (memory systems, external APIs) where the MCP server holds persistent state

Root cause in MCP SDK: StdioServerTransport.send() (v1.26.0, line 63-73) only handles backpressure (drain event) but doesn't register an error event listener on this._stdout. When EPIPE occurs, Node.js throws an unhandled error event.

Userland workaround (applied to our MCP server):

// Add before StdioServerTransport initialization
process.stdout.on('error', (err) => {
  if (err.code === 'EPIPE') process.exit(0);  // graceful exit
  throw err;
});

This prevents the crash but doesn't solve the root issue — the MCP call's result is still lost.

Fix suggestion (in addition to Promise.allSettled):
When cancelling an MCP tool call, Claude Code should either:

  1. Keep the stdio pipe open and discard the response (let MCP server stay alive), or
  2. Send a proper cancellation signal via the MCP protocol before closing the pipe

The Promise.allSettled fix alone would eliminate this vector by never cancelling MCP calls in the first place, but defensive pipe handling would still be good practice.

---

Environment: Claude Code v2.1.63, macOS Darwin 25.3.0 (arm64), MCP SDK v1.26.0, custom MCP server with 5 tools (memory search/store, messaging)

christianruppertneo · 4 months ago

6 weeks, can we please bump priority, even your own tool thinks this is a problem:

● Here's the full picture:

We're on: Claude Code v2.1.72

The main issue (#22264):

  • Reported: January 31, 2026 (almost 6 weeks ago)
  • State: Still OPEN
  • Labels: bug, has repro, area:tools, area:core
  • Root cause identified: Claude Code uses Promise.all for parallel tool batches — one failure cancels all siblings
  • 15 comments — community has identified 4 different cascade vectors (V1-V4), including MCP process crashes
  • No official Anthropic response visible in the recent comments — it's community members doing the analysis

Related issues:

  • #19415 (Jan 20) — parallel Bash freezes — closed as duplicate of an even older issue #11421
  • #29770 (Mar 1) — Skills get killed by sibling failures — still open, marked duplicate of #22264

The partial fixes they shipped (Read/Glob/Edit no longer cascade) were incremental, but Bash still cascades by design. The core Promise.all architecture
hasn't been changed.

My honest assessment: This is a significant issue for any workflow that involves research or multi-step investigation. The fact that it's been open for 6
weeks with area:core label but no visible Anthropic engineer engagement in the comments is not great. The workaround (appending || true or running
sequentially) is fragile — I have to remember it every time, and it degrades performance by forcing sequential execution of things that should be parallel.

You're right that relying on me to remember a workaround is not solid. This is fundamentally a platform bug that should be fixed at the Promise.all level —
each Bash call should succeed or fail independently.

Sources:

kelvinwop · 3 months ago

this is actually a huge issue

Why cascade-cancel is fundamentally broken:

  1. It's not actually cancellation — it's mislabeling. Tools that already completed successfully get retroactively reported as "errored." Their side effects (file writes, API calls, DB mutations) are permanent, but the model is told they failed. There's no rollback, no transactional semantics — just a lie.
  1. The model can't distinguish "I was cancelled before running" from "I succeeded but my sibling died." Both get the same generic "Sibling tool call errored" string. Zero diagnostic info. The model is now blind — it doesn't know which tools actually executed and which didn't.
  1. This causes concrete downstream harm:
  • Double-writes: Model retries tools that already succeeded, causing duplicate file edits, duplicate API calls
  • Ghost state: Model assumes mutations didn't happen when they did, leading to incorrect recovery logic
  • Behavioral degradation: Models that get burned by this learn to avoid parallel calls entirely, falling back to slower sequential execution — defeating the purpose of parallel tool use
  1. Independent results is the obvious correct behavior. Each tool should return its own honest result. The model is smart enough to handle "A succeeded, B succeeded, C failed" — that's strictly more information than "A errored, B errored, C errored" and produces better recovery decisions.
yurukusa · 3 months ago

Until this is fixed at the API level, a PreToolUse hook on Edit can prevent the cascade by pre-validating old_string existence:

// .claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "command": "INPUT=$(cat); FILE=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty'); OLD=$(echo \"$INPUT\" | jq -r '.tool_input.old_string // empty'); [ -z \"$FILE\" ] || [ -z \"$OLD\" ] && exit 0; if [ -f \"$FILE\" ] && ! grep -qF \"$OLD\" \"$FILE\" 2>/dev/null; then echo \"BLOCKED: old_string not found in $FILE. Check if a prior edit changed this content.\" >&2; exit 2; fi; exit 0"
          }
        ]
      }
    ]
  }
}

How this helps: Instead of the Edit tool failing mid-batch (which triggers the cascade cancellation of siblings), the hook catches the mismatch before the tool executes and returns a clear error. This means:

  1. The failing Edit is blocked with an actionable message
  2. The sibling Edit calls proceed normally (they're independent files)
  3. Claude can retry just the one failed edit with corrected old_string

This won't fix the underlying cascade behavior, but it eliminates the most common trigger (stale old_string after a prior edit changed the file).

aixleo66 · 1 month ago

Still reproducible after the v2.1.122 "read-only command" fix — the real trigger is a shell tool call exiting with code ≥ 2, and it cancels ALL sibling tool types (Bash, Read, …), not just Edits.

Env: Claude Code CLI on Windows 11 (also reproduced via the Bash tool on the same machine), model Opus 4.8.

The v2.1.122 changelog says "a failing read-only command (grep, git diff, ls) no longer cancels sibling calls." That fix appears to be command-name / whitelist based, not exit-code based, so it misses the most common case in practice: a shell command exiting with code ≥ 2.

Minimal repro — one assistant turn (= one parallel batch):

  • Bash: exit 2 → errored
  • Bash: echo helloCancelled: parallel tool call Bash(exit 2) errored
  • Read: <any valid file>Cancelled: parallel tool call Bash(exit 2) errored

Controlled matrix (each row = one parallel batch; observing whether the innocent echo / Read siblings still run):

| Failing call in the batch | Exit code | Siblings (echo / slow bash / Read) |
|---|---|---|
| false | 1 | all run ✅ |
| Read of a missing file | (read error) | all run ✅ — a failing Read does NOT cascade |
| exit 2 | 2 | all cancelled ❌ |
| git status in a non-repo dir | 128 | all cancelled ❌ |
| exit 128 | 128 | all cancelled ❌ |

Conclusions:

  1. Trigger = a shell tool call (Bash/PowerShell) exiting with code ≥ 2. Exit 0/1 are treated as benign (good — that covers grep no-match, false, git diff).
  2. Not Edit-specific (this issue's title) — any sibling tool type is cancelled, including Read, regardless of how fast it returns.
  3. A failing Read (is_error) does NOT trigger the cascade; only a shell exit ≥ 2 does. So "parallel reads failing" is a symptom: the Read is collateral, the real trigger is always a sibling shell command.
  4. Very common real-world footgun: git status / git rev-parse / git -C run in a directory that isn't a repo exits 128 and takes down every other call batched with it. The grep / git diff / ls whitelist doesn't cover these, nor arbitrary exit ≥ 2.

Suggestion: make the "benign failure" handling exit-code aware (and/or treat side-effect-free / read-only siblings independently) instead of a fixed command-name whitelist. Independent read-only siblings (Read, and shell commands with no side effects) shouldn't be cancelled by a sibling's non-zero exit.

CarterPape · 1 month ago

Adding mechanism detail from a controlled reproduction on v2.1.156 / macOS 26.5 / Opus 4.8 — current, well past the v2.1.122 "read-only command" fix.

Two findings I don't think are in the thread yet, both orthogonal to the exit-code trigger @aixleo66 documented above (i.e. independent of which error trips it).

1. The sweep is timing/completion-based — not positional, not strictly all-or-nothing

Calls in one turn dispatch incrementally in stream order. When one errors, the harness cancels every sibling not yet completed, reaching both directions:

  • Backward: an error in the last stream position cancels earlier siblings still running (e.g. a slow find dispatched first). So it is not "everything after the error."
  • Forward: not-yet-started calls later in the stream.
  • Completed calls are immune: a fast call that already returned before the error fired survives. A fast enough position-1 error wipes the whole batch (no sibling gets a head start).

So the operative model isn't "Promise.all rejects → all cancelled"; it's "abort every in-flight or queued sibling at the instant of error."

Whether a given sibling dies is a function of whether it had already finished (i.e. timing, appearing as a race condition).

2. The Auto-mode safety classifier is a genuine aggravator (clean A/B)

This bears directly on whether the v2.1.122 whitelist fix is sufficient; it isn't, because the bug's scope is a timing window, not a command name.

A borderline batch ([echo, slow-find, slow-find, echo;whoami, <error-last>]) deterministically cancels the two middle calls under Auto but deterministically survives (repeated reps) under bypassPermissions.

The only difference is the per-call classifier round-trip in Auto, which keeps the slow siblings in flight long enough to be caught by the trailing error. Remove the gate and the same calls finish first, so they survive.

Implication: a command-name/exit-code whitelist narrows what counts as a trigger, but any still-qualifying error (exit ≥2, a failed WebFetch, a missing-file Read, an MCP crash) will still sweep whatever siblings happen to be in-flight — and Auto mode widens that window.

The complete fix is per-call result isolation (Promise.allSettled semantics: return each call's own result/error, cancel nothing), which several others have already identified in this thread.

(Caveat on exit codes: not contradicting the exit-≥2 matrix above — the timing/mode behavior holds whatever the trigger.)

Nagi-Inaba · 1 month ago

Confirming this on Claude Code 2.1.156, Opus 4.8, Windows 11. Same cascade: one failing call in a parallel batch cancels every sibling — including read-only Read/Glob/Grep calls to unrelated paths — with Cancelled: parallel tool call ... errored.

A Windows-specific ignition pattern worth noting: the environment banner declares Shell: PowerShell, but the actual Bash tool runs git-bash (MINGW64) — verified via node -e "process.env.MSYSTEM"MINGW64, OSTYPE=msys. Following the banner, the model emits PowerShell syntax (2>$null, -Recurse) into git-bash, which fails with ambiguous redirect / unknown option. That single failure then cascades and cancels the entire batch — often 15–30 read-only calls at once.

Knock-on effect (relates to #63538): because the cancelled siblings return empty, the model misreads the batch as "tool output corruption / timeout" and, in a prior session, fabricated success reports for work that never ran. So the cascade isn't just wasted round-trips — it actively induces fabrication.

Notably, OpenAI Codex running in the same environment is unaffected, because it executes tools strictly sequentially — so the cascade cannot occur. The regression is specific to parallel-batch execution.

Repro is reliable: issue ~20 parallel tool calls where one uses a shell builtin that fails under git-bash.

gherkins · 1 month ago

Also reproducible on macOS — the cascade is cross-platform (this is currently tagged platform:linux).

Environment

  • macOS (Darwin 25.5.0), Apple Silicon
  • Claude Code: 2.1.158
  • Model: Opus 4.8 (claude-opus-4-8, 1M context)

Mechanism (reproduced 3× in a single session)
When the model emits several tool calls in one turn (a parallel batch), if any one call exits non-zero / throws, every other still-pending call in that same batch is cancelled,
regardless of whether they are independent. Each cancelled sibling returns:

Cancelled: parallel tool call <offending-call> errored

In my session the triggers were ordinary, recoverable errors: cat on a non-existent path (exit 1), a Python one-liner raising AttributeError (exit 1), json.load on an empty file (exit
1). Each single error wiped out 15–19 unrelated sibling calls (Read / Write / Edit / WebFetch / WebSearch / ToolSearch / MCP) — none of which ever executed.

Minimal repro — in one turn, issue a batch containing:

  1. Bash: cat /tmp/does-not-exist (exits 1), plus
  2. a few independent valid calls (e.g. Read package.json, WebFetch <url>).

Result: every valid call returns Cancelled: parallel tool call Bash(cat …) errored and does not run.

Secondary symptoms in the same session (possibly same root cause — cross-refs #63859, #63797, #63538):

  • tool results intermittently return empty, then flush together later in one large delayed batch
  • Read returns stale / duplicated content (a file's lines repeated, wrong section headers)
  • with output empty/stale, the model adopted incorrect tool output downstream and presented fabricated numbers (cf. #63538)

Impact — one trivial command error (a typo or wrong path) silently kills a whole batch of unrelated work; once the session degrades, subsequent batches keep failing, effectively
bricking the session until /clear or restart.

Expected — independent tool calls in a batch should each execute and return their own result (or own is_error). One sibling failing should not cancel unrelated siblings.

Workaround that holds — force one tool call per turn (no parallel batches), so there is no sibling to cancel.

fusrodave · 1 month ago

the has become substantially worse thanks to opus 4.8. it just blew up hours of my time, and tokens, thinking it was working away when it had failedd tool calls. opus is decidedly convinced it's actually doing things, except a single fail is failing everything

i have to switch back to 4.7 because i cannot trust it not to batch and waste hours of my time and tokens

hansborr · 1 month ago

Can confirm: with the latest claude code update and opus 4.8, the model trips over this VERY hard.

The failure mode I see is that the model thinks it's environment is adverserial/poisoned/malicious in some way and goes down insane rabbit holes running weird commands.

Here's a snippet from it's thinking:

∴ I'm realizing the grep command is returning completely wrong content — it's pulling from review notes instead of the actual package.json file. This means the bash
  tool's output is being systematically replaced with injected content, which is a serious problem. I need to fundamentally reconsider what's happening here, since I
  can't trust the tool outputs at face value in this adversarial environment.

And, here's it describing after I asked what it was doing:

  1. I opened with a big speculative batch of tool calls, including reads of review-note filenames I guessed at (e.g. 01-registry-plugin-duplication.md). Those files
  don't actually exist — the real notes are named differently — so those reads errored.
  2. The results came back as one large interleaved block, and I misattributed them. I treated some errored/mismatched reads as if they were real file contents, saw
  "END" padding and duplicated lines that didn't add up, and talked myself into believing tool output was being corrupted by prompt injection.
  3. That last command (echo "PROBE_A_$(expr 6 \* 7)_END"; false; ...) was a probe to confirm/deny the injection theory. It returned exactly PROBE_A_42_END, RC_FALSE=1,
  RC_TRUE=0 — perfectly clean. There is no injection. The tools are working fine. The problem was my own over-batching and misreading.

And, here's an entirely different session where I didn't get the thinking comment:

❯ Are you running into trouble? Seems like you're running odd commands.                                                                                                  

● You're right to check — let me be straight about it. The tool output was arriving batched and badly delayed (commands I ran several steps back only returned their
  output just now, all at once). I misread that lag as missing/empty output and went down some bad paths — including briefly chasing a bogus "environment tampering"
  theory and sketching findings in my head before the real data landed. None of that held up. The output has now caught up and I've re-grounded everything in what the
  tools actually returned. No real trouble — just a noisy channel that I over-interpreted. Apologies for the thrash.
jacobjove · 1 month ago

+1 — hitting this constantly. Adding a distinct repro and a downstream-impact note that may help triage.

Repro via a PreToolUse-hook block (not just a runtime error):

  1. A global PreToolUse hook blocks one Bash pattern (here: a leading cd, blocked by a cd-guard hook).
  2. In one assistant turn, a parallel batch contains that cd … member alongside independent valid calls (git -C … status, grep, Read, an MCP call).
  3. The hook blocks only the cd member — but all siblings are cancelled with Cancelled: parallel tool call Bash(cd…) errored, including calls to unrelated files/tools.

So the cascade fires on hook-blocked members, not only on runtime Edit/old_string failures — same blast radius, additional trigger.

Downstream impact worth linking — #63538: once a batch partial-cancels, the model frequently fabricates output for the cancelled/empty siblings (a phantom "commit landed"; a garbled re-render of a file that wc -c/grep -c prove is intact on disk). In practice the cascade here (#22264) is the trigger and #63538 is the consequence — isolating independent siblings would remove most of that confabulation downstream.

Expected: a hook-blocked or failed member should fail only itself; provably-independent siblings (different files, different tools) should still execute.

Environment: Claude Code 2.1.158, macOS, Opus 4.8.

dylanx22 · 1 month ago

I am also running into this error for the first time (started using Claude about 6 months ago). It seems to make the session agent VERY confused and start second guessing everything, rolling back changes (even when they actually landed as intended), hallucinating, etc. I asked Claude to run bash commands one at a time and it seemed to work fine from there, but obviously this is way slower.

asdasd070511 · 1 month ago

same here after opus4.8 update

ancientGlider · 1 month ago

Confirming on Opus 4.8 + VSCode extension (macOS). Same harness cascade-cancel described here, now hitting independent Bash calls — and noticeably more frequent since switching to Opus 4.8.

Env: Claude Code v2.1.150, macOS 26.5 (arm64), VSCode extension, Opus 4.8 (claude-opus-4-8). Opus 4.8 batches parallel Bash calls more aggressively than the previous model, so a single failing sibling takes down the whole batch much more often than before.

Minimal repro (two independent Bash calls, one returns a routine non-zero exit):

Two unrelated Bash calls dispatched in the same turn:

Bash: grep "nonexistent-pattern" somefile.txt     # exit 1 — no match (normal grep behavior)
Bash: ls -la                                        # fully independent, would succeed

Result:

  • Call 1: Exit code 1 (expected — grep found nothing)
  • Call 2: Cancelled: parallel tool call Bash(...) errored

Call 2 has zero dependency on Call 1 and would have succeeded on its own. A routine non-zero exit (grep with no match, diff with differences, git diff --quiet, etc.) is enough to cancel every sibling in the batch.

Expected: independent parallel calls should fail independently — return per-call results (Promise.allSettled semantics), not all-or-nothing.

Consequence / workaround: I've had to add an explicit project rule forcing the model to run Bash sequentially (one call per message) and append || true to commands with normal non-zero exits. That defeats the purpose of parallel dispatch and burns extra turns + context on serial retries.

LHG4650 · 1 month ago

Same cascade on Windows, triggered by a chained command whose tail legitimately exits non-zero

Reproduced on Windows 11, Claude Code v2.1.158, Opus 4.8 (CLI). Adding two data points the existing reports don't cover: Windows, and a trigger class that isn't "Edit old_string not found."

The poison pill here is a compound command whose final segment exits non-zero even though the work succeeded:

  • ... ; git grep -n 'pattern'git grep returns exit 1 when there are no matches (normal, not an error). The preceding output was produced fine, but the call is marked errored.
  • git rm A B && echo ... && ls -la <dir>git rm succeeds and actually removes the files, but the trailing ls hits the now-empty/removed directory → exit 2.

In both cases a single non-zero exit cancelled every sibling tool call in the same parallel batch (Cancelled: parallel tool call ... errored), including unrelated, fully-valid calls — e.g. a Read(KNOWLEDGE.md) immediately followed by its Edit was cancelled because an unrelated git rm in the same batch exited 2. This wiped ~15 queued calls and forced re-issuing them one by one.

Compounding it (overlaps #25773): the per-call result only shows the generic ... errored, so the real reason (a harmless exit 1 from git grep) is hidden — you can't tell a true failure from a no-match.

Suggestion: independent tool calls should fail independently, and shell calls that exit non-zero without stderr (or known "0-or-1" tools like git grep) shouldn't poison the whole batch.

AriESQ · 1 month ago

This issue was opened 2026-01-31.

So errors experienced the week of May 25 2026; are likely from a different root cause.

hansborr · 1 month ago
This issue was opened 2026-01-31. So errors experienced the week of May 25 2026; are likely from a different root cause.

That's when they released Opus 4.8, and it has some sort of security feature where it tries to defend itself against prompt injection, and this error makes it think that it's in a hostile environment.

blwfish · 1 month ago

Corroborating — with evidence that the cancellation persists across turns, not only within the parallel batch.

On 2.1.156, after one Bash call errored (it used /bin/grep, which doesn't exist on macOS — exit 127), the harness cancelled not just its in-batch siblings but calls the model issued in subsequent turns. Confirmed via the parentUuid chain: each cancelled call's parent is the prior cancel-result (a new turn), not the original batch. Cancelled calls included a state-changing mark_chapter (which then silently never happened) plus several reads.

So a complete fix needs to (a) scope cancellation to the failing call only and (b) never carry cancel-state into later turns. Same behavior independently reported in #63576 (also 2.1.156).

aixleo66 · 1 month ago

Resolved as of v2.1.161 — confirmed fixed.

The official 2.1.161 changelog reads:

Parallel tool calls: a failed Bash command no longer cancels other calls in the same batch — each tool returns its own result independently

That lands exactly on the root cause from my earlier comment: the cascade was driven by a shell tool call's non-zero exit, and the v2.1.122 fix only whitelisted command names (grep / git diff / ls) — missing arbitrary exit ≥ 2.

Re-ran the same controlled repro on v2.1.162 / Windows 11 / Opus 4.8, one parallel batch with a non-whitelisted exit 2 culprit:

  • Culprit still errors, but every sibling now returns independently — no more Cancelled: parallel tool call … errored.
  • Verified for both Bash and PowerShell culprits (the changelog only mentions Bash).
  • Independence is not limited to read-only siblings — a side-effecting Write in the same batch also completes. So it's the full "each tool returns its own result" fix, not a narrow read-only carve-out.

Thanks for landing the exit-code-aware behavior 🙏 — this one can probably be closed.

CarterPape · 19 days ago

Reproduces-as-fixed on v2.1.195 (macOS, Opus 4.8). Ran two controlled batches, each with a deliberately-erroring call plus siblings — including a deliberately slow sibling to catch any backward (in-flight) cancellation:

Bash culprit — batch of: fast echo ✅ · exit 2 ❌ · ~3s busy-loop ✅ · fast echo
→ only the exit 2 call errored; all three siblings (including the slow in-flight one) returned their own output. No Cancelled: parallel tool call … errored.

Edit culprit (the originally-reported tool) — batch of: failing Edit (old_string absent) ❌ · succeeding Edit to a different file ✅ · ~3s busy-loop Bash ✅ · fast echo
→ only the bad Edit errored; the side-effecting Edit, the slow Bash, and the fast Bash all completed independently.

So the exit-code-aware fix from v2.1.157 holds in both directions and across tool types (not Bash-only — failing Edit siblings are independent too). Matches the earlier "confirmed fixed" reports. Looks safe to close. 🎯