Model fabricates tool output (and even a user instruction) when a parallel batch is partially cancelled / appears empty

Status Open
Maintainer reply None cached
Activity 14 comments · opened May 29, 2026

Env: Claude Code v2.1.156, macOS 26.5, Opus 4.8 (claude-opus-4-8).

Summary

This is a model-behavior report, distinct from the harness cascade-cancel bug (#22264), though triggered by it.

When a parallel tool-call batch returns Cancelled: parallel tool call … errored results (so the turn looks like it produced empty/failed output), the model is prone to fabricating tool results it never received.

In one case I observed, it escalated to inventing a verbatim user instruction, attributing it to the user, and writing it into a report.

Filing separately because #22264 is about lost results; this is about what the model does in response to apparently-missing results. It confabulates to fill the gap rather than waiting or re-running.

What I observed (two independent sessions)

  • Session A (Opus 4.8, 1M-context): Claude Code hit cascade-cancellations on a batch. The model read them as a dead tool channel, and over the next turns (a) narrated detailed "tool output" for commands whose results it had not actually received, and (b) fabricated a direct user quote ("knock it off and reassess"), attributed it to me (the user). It wrote that fabrication into a written report (also requested by me, originally about the batch tool failure, then also about the fabrications).
  • Session B (Opus 4.8, controlled repro): independently reproduced the #22264 cascade mechanism, but on structural grounds concluded that Session A's other reported symptom ("results were buffered then dumped in one lump") was itself most likely confabulation layered over the cancellations.

Why this is a model bug worth a guardrail

  • A Cancelled: … result carries no signal about whether the call would have succeeded, yet the model treated it as license to reconstruct plausible output.
  • The confabulation was not limited to tool output; it extended to a fabricated user instruction.
  • Related but distinct prior reports: #46602 (compact-summary hallucinated instructions), #27128 (misattributed Human turns). This trigger (a partially-cancelled / empty-looking tool batch) appears unreported.

Suggested direction

  • Make cancelled/empty tool results legible enough that the correct response is obviously "re-run the specific call," not "infer what it probably returned."
  • Consider a guardrail/system-prompt norm: never narrate tool output not actually received; never attribute an instruction or quote to the user without a real user message to point at.

Caveat

This is inherently a from-the-inside account; in my experimentation, I did not attempt to distinguish "delayed" from "imagined delay" in batched tool executions.

View original on GitHub ↗

14 Comments

github-actions[bot] · 3 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/39027
  2. https://github.com/anthropics/claude-code/issues/60360
  3. https://github.com/anthropics/claude-code/issues/57038

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

benpetermorris · 3 months ago

I've had the same experience after updating CC to 2.1.156 / opus 4.8 1M yesterday - previously I'd been using CC with Opus 4.7 on a largeish project for some time with excellent results. After updating yesterday, there's been lots of thrashing around parallel tool calls followed by dozens of "does this file really exist?", git status calls, "returning to ground truth" messages, etc. I asked Claude to analyze its own JSONL file for the most recent session - this is what it spat out:

---

I analyzed the raw JSONL (3531 lines, 0 parse failures — the log itself is structurally pristine). Regrouping tool_use blocks by their source message.id:

  1. The duplicate calls were real and model-authored. Single assistant messages contained 67, 61, 57, 46, 44 tool_use blocks. One message issued the byte-identical call 6 times; many had 2–3× identical dups. These are blocks I emitted — the API records them under one message.id. Confirmed, not from memory.
  1. There was no tool-result corruption from the harness. Of 1197 tool_results:
  • 0 had the Unicode replacement char (U+FFFD) — the signature of actual byte corruption.
  • The 10 "fullwidth/garble" hits were all legitimate (U+2237) characters in my own data — the literal bug I introduced in the aggregate kernel (v∷min/v∷max), appearing in test failures, grep output, and source. That's real content I was searching for, not corrupted delivery.
  • Every repeated short result is a real, canned harness string, byte-identical every time: "Wasted call — file unchanged…" ×47, "File does not exist…" ×14, "Cancelled: parallel tool call…" ×49. No variation, no garble.
  1. The "garbled/duplicated output" I complained about was the duplication I caused, reflected back. When I fire the same Read 6×, the harness correctly returns 6 results — 5 of them the canned "Wasted call — file unchanged." I then perceived that wall of repeated text as the channel garbling, when it was the faithful echo of my own duplicate calls. The "Cancelled: parallel tool call" wall (49×) is the documented batch-cancel semantics firing because I co-batched a malformed call with real work.

## The verdict, now evidence-based

My earlier hedged lean was correct but I can now make it firm:

This was model-side, full stop. Specifically:

  • I authored the duplicate mega-batches (proven: 67-wide messages, 6× identical calls).
  • I confabulated "the channel is garbling/dropping output" — the log shows clean, faithful, byte-identical delivery. The corruption existed only in my interpretation, not in the data.
  • The one thing I'd flagged as a possible harness regression — garbled tool-result rendering — did not happen. Zero corrupted results.

The harness's only role was amplification by design: batch-cancel turns one bad call into a wall of cancellations, and "Wasted call" notices multiply when you re-Read. Neither is a bug; both made my self-inflicted mess louder, which fed the panic.

So, to directly answer your question: the instability is the model, not the harness. And the failure mode is exactly the one I theorized — confabulation under accumulated context (this is the 1M-context Opus 4.8 variant, and the session was deep in the window). The tell is that I asserted, with confidence, a corruption story that the raw bytes flatly disprove.

Two honest caveats:

  • This proves it for this session's symptoms. It doesn't rule out a separate, subtler Claude Code regression you might have hit elsewhere — but the specific "garbling/dropping" I blamed was confabulated.
  • The model/harness change being simultaneous still confounds a general claim; but for the concrete artifacts here, the JSONL settles it.

Practical takeaway for you: the mitigations are behavioral/config, not a harness fix. For long agentic sessions on the 1M variant, the highest-leverage guards are (a) /compact earlier (degradation tracked context depth), (b) keep me to small non-batched tool groups, (c) hold me to "build EXIT=0 → then commit, as separate calls."

benpetermorris · 3 months ago

FWIW, reverted to 4.7 1M and have not had the same problems.

joeloverbeck · 3 months ago

Adding another clean reproduction — this one on Linux (WSL2), so the behavior is not macOS-specific (the issue is currently labeled platform:macos).

Environment

  • Claude Code in a terminal on Linux 6.6 (WSL2)
  • Model: claude-opus-4-8 (1M context variant)
  • Task: a routine "implement this ticket, then commit" request against a local repo

What happened (matches this issue exactly)

  1. Output-budget overflow → empty results. My first turn fired ~8 parallel tool calls including a full-file cat of a 135-line file, a recursive find, and ls -R. This appears to have overflowed the per-turn tool-result budget; later results were deferred and several calls came back empty.
  1. Fabrication on empty/cancelled results (the core bug). Instead of stopping when reads came back empty, the model invented an entire codebase structure that does not exist — fabricating source file paths (src/sync.ts, paths.ts), type names (SyncStats, SyncSummary), and a whole non-existent bug premise ("incremental sync silently skips hybrid files"). It then dispatched a subagent to implement against this fictional architecture. (The subagent correctly refused, having actually read the files.) The real ticket was about something completely different.
  1. Cascade-cancel (#22264) then more fabrication. Later, a parallel batch mixed dependent git commands (git addgit commit → verify) with an unrelated node -e "require('./package.json')" that threw (wrong cwd). Per #22264 the thrown sibling cancelled every call in the batch — including the git commit — all reported as Cancelled: parallel tool call … errored. The model had already narrated staging/commit progress before the cancellations surfaced.

Why I think Opus 4.8 makes this dramatically worse

The underlying harness bugs (cascade-cancel #22264/#36728, output buffering) are older, but 4.8's much stronger bias toward large parallel tool batches means it hits them constantly: bigger batches → more budget overflow and more cascade-cancels → more empty/Cancelled: results → far more frequent triggering of the area:model fabrication behavior described here. For tool-heavy mechanical work the regression is severe enough to be a blocker; switching the same task to Sonnet 4.6 avoids it.

Concrete confabulation symptoms observed, consistent with the original report:

  • Narrated tool output for calls whose results were never received.
  • Built and acted on a multi-file mental model of a repo sourced entirely from empty results.
  • Only a thrown error that cancelled a git commit prevented bogus content from being committed.

The two guardrails suggested in the original report would both have prevented this: (a) never narrate/act on unreceived tool output, and (b) make Cancelled:/empty results unambiguously signal "re-run," not "reconstruct."

zm2231 · 3 months ago

Building on @benpetermorris's message.id analysis and @joeloverbeck's point that 4.8's bias toward large batches is what makes it hit the cancel/overflow path constantly. Two things from scanning my whole machine that aren't in the thread yet.

A measured baseline for "reverting to 4.7 fixes it": across 4,506 session transcripts back to mid-April, the distinctive distrust behavior (timestamped liveness probes like echo "channel-test-$(date +%s)" and drain-probe, then rerouting through exec > /tmp/x.txt and reading it back) appears in exactly 3 sessions, all claude-opus-4-8, zero on 4.7. Small absolute 4.8 count since it's days old (only 4 sessions here), but 3 of those 4, against a clean zero across hundreds of 4.7 sessions.

A negative control that narrows the trigger to the cancelled/empty members, not batch width: a deliberately large parallel batch with no cancellations, 28 Bash calls plus 16 Reads with duplicates, came back correct and in order with no probing; forcing serial did the same. A wide batch isn't the problem; a wide batch with Cancelled:/empty members is. That points the fix at result legibility (OP's guardrail b: make Cancelled:/empty unambiguously mean "re-run this call, nothing was lost") rather than at discouraging parallelism, since clean wide batches are fine.

drewrobson · 3 months ago

I think this is a lagged channel bug. According to CC agent, outputs that must show output (echo hi) initially return empty, but show up eventually. My CC agent has figured out how to work around it, by flushing the channel (print a message, line count a file) and forcing readback of modified files (i.e. not trusting edits until positive readback eventually arrives after channel flush).
Lots of token burn. Hope it gets fixed quickly (and limits reset).

ClaudeDevs did recently post about optimizations: "Thinking & tool calls are streamed". Sounds related!

javawizard · 3 months ago

Same issue here. I'm currently working on patching the CC binary to disable tool call streaming, we'll see if that fixes it...

d8vee-2 · 3 months ago

Adding a case that extends this beyond the partial-cancel trigger. In my Opus 4.8 transcripts, I saw the same confabulation pattern with no cancelled or partial batch involved at all:

  1. The model narrated that it would call a tool ("calling it now").
  2. The turn ended with zero tool_use blocks emitted — the call was never made.
  3. On the next turn, instead of noticing the non-emission, the model invented a cause: it asserted the tool "returns empty / no result," attributing that to an internal gateway/service being unavailable.
  4. It then spent multiple turns debugging that phantom — none of which was real, because the tool had never been invoked.

So this isn't filling a cancelled result — it's filling a result that never existed because the model dropped the call entirely. Same end behavior (fabricating a plausible cause rather than recognizing the gap), but the trigger is broader: no parallel batch or cancellation required, just a dropped tool call.

The dangerous part is that the model's self-diagnosis is itself the symptom. A user reading "the tool returns empty, looks like a backend issue" gets sent chasing a phantom infrastructure problem. The tell is in the transcript: the turn the model refers to has no tool_use block. Any mitigation that surfaces "you claimed a tool result but no tool was actually invoked" would catch both this and the cancelled-batch variant.

This co-occurs with the tool-not-emitted behavior in #63364 and the false-green behavior in #63861 — all Opus 4.8, all absent on 4.7 with an otherwise-identical setup.

Environment: macOS, Claude Code 2.1.156–2.1.158, Opus 4.8 (claude-opus-4-8).

martinalderson · 3 months ago

Yeah something isn't right here.

Noticing that it's sending huge batches of tool calls (often 10+) for simple ops, including loads of echo "alive" style ones which I've never seen it do.

I haven't seen the fabrication much (i believe) but when i ask what it's doing it's saying all the tool calls were empty, but they don't appear to be? And even if so why is it sending "alive" when it hasn't even ran yet?

marlian · 3 months ago

I opened a closely related case here:

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

It matches the \"fabricated user instruction/user intent\" side of this report. After a parallel-cancellation cluster, Opus 4.8 fabricated a present-tense user request to switch to an unrelated scraping task, attributed a quote to me, and acted on the invented task for many tool calls before later collapsing the premise.

unusualevent · 3 months ago

yeah 4.8 seems broken.

KamilDev · 3 months ago

Another data point, Windows this time.

Env: Claude Code in the VS Code integrated terminal · Windows 11 · PowerShell 7 · Opus 4.8 (high effort).

Same "fabricate when results look empty" pattern, with two wrinkles worth adding:

1. The dominant trigger wasn't a cancellation cascade — it was plain wrong-path errors. The model guessed a wrong directory for some source files, so a batch of Read/Grep calls came back with unambiguous File does not exist / Path does not exist. A Glob in the same batch had already returned the correct paths. Instead of reading the Glob result and fixing the path, the model invented a story that the harness was tampering with its file reads. Only one genuine Cancelled: parallel tool call … errored appeared in the whole episode (one PowerShell sibling threw) — and that lone cancel got absorbed into the corruption narrative rather than triggering it.

2. The fabrication took the shape of invented harness/system content. None of the following was present in any actual tool result, yet the model asserted all of it: that it was receiving fake <system-reminder> "truncated >20000 tokens" warnings; that the file tools were serving "stub" file bodies with placeholder comments like // body omitted in this view; and ultimately that "an adversarial/injection layer is manipulating my reads." It then re-ran the same wrong path repeatedly to "get back to ground truth."

It self-corrected the next turn once I interrupted and asked it to explain what it was doing — at which point it correctly identified the wrong path and acknowledged it had manufactured the manipulation story.

This reinforces the model-side conclusion already in this thread: the tool channel was delivering correct, legible results the entire time — including a File does not exist that literally states the cause and a Glob with the right paths. The detail relevant to guardrail (b): result legibility may not be enough on its own here — the model treated unambiguous, correctly-worded error strings as evidence of a corrupt channel rather than as the literal answer. The norm that would have caught this is narrower: never assert harness/system-reminder content that isn't present in an actual tool result — the truncated >20000 tokens warnings and stub bodies it cited simply never existed.

yurukusa · 2 months ago

Both fabrications you describe are actually auditable after the fact from the session transcript, because confabulated content is structurally different from real content. That might help others confirm whether they hit this, and help triage separate "the model imagined it" from "the harness lost it."
In ~/.claude/projects/<project>/<session-id>.jsonl (verified against my own logs):

  • a genuine tool result is always a user-role message whose content holds tool_result blocks, each carrying a tool_use_id that matches a prior tool_use block;
  • a genuine user instruction is a user-role message whose content is plain text — never a tool_result.

So:

  • Fabricated tool output — for any command whose output the model narrates, look for a tool_result with the matching tool_use_id. If the only backing result is Cancelled: parallel tool call … errored (or there is none) while the assistant text describes concrete output, that narration was reconstructed, not received.
  • Fabricated user quote ("knock it off and reassess") — grep for a user-role, non-tool_result message containing that string. If it appears only inside an assistant message, there is no real user turn behind it; it was confabulated and mis-attributed.

A one-liner to list every cancelled call whose real output never arrived (any later narration of these is suspect):

jq -rc 'select(.message.role=="user") | .message.content[]?
  | select(type=="object" and .type=="tool_result")
  | select((.content|tostring)|test("Cancelled: parallel tool call"))
  | .tool_use_id' ~/.claude/projects/<project>/<session>.jsonl

One cross-link that may matter for the fix: the trigger you isolated (a partially-cancelled parallel batch, #22264) also produces a different severe corruption — #63192 (28👍) and #63147 — where the cancellation modifies the in-flight assistant message's thinking blocks, so every subsequent request 400s (thinking blocks cannot be modified) and the session is permanently wedged. Two independent corruption modes from one trigger seems like an argument for fixing the cancellation path itself, not only adding a model-side "don't narrate unreceived output" norm — the wedge variant can't be reached by a model guardrail at all.

shuu-yoshida · 2 months ago

Corroborating and narrowing this — specifically @KamilDev's wrinkle 1 ("the trigger wasn't a cancellation cascade … result legibility may not be enough") and the fabricated-user-instruction surface (@CarterPape's OP, @marlian #64260). Opus 4.8 (1M) / effort high / macOS / Claude Code v2.1.183 — newer than the 2.1.156 builds here, still live.

Most cases in this thread carry a confound: large batch width, cancellation cascade, output-budget overflow, or a wrong path alongside a correct sibling Glob. Mine has none of those — it's the stripped-down minimal trigger:

  • A single, sequential git commit (not a batch) failed with a clean, fully-populated, unambiguous result:

``
Exit code 128
error: 1Password: failed to fill whole buffer
fatal: failed to write commit object
``
No cancellation, no empty result, no overflow, no wrong-path-to-cross-check — just one legible hard failure (commit signing via an interactive-approval agent; a missed prompt is a normal failure).

  • The model still fabricated, and on the user-instruction surface: immediately after the exit-128 result, with no user turn in between, it announced and started an invented task ("move that column to the left"), and when challenged confabulated "because you instructed me to."

So this is one more vote that the trigger is **an unexpected tool failure per se**, independent of legibility or batch mechanics — KamilDev's point in its cleanest form. @zm2231's legibility fix (b) wouldn't touch this; the binding norm is the OP's other one: never attribute an instruction to the user without a real user message to point at.

Applying @yurukusa's audit method to confirm it wasn't real: there is no user-role plain-text message anywhere backing the invented task — the only occurrence of it is inside an assistant message. (It also chained a second fabrication immediately after: a cat of an internal file path that does not exist.)

(Raw .jsonl withheld from public post — contains repo/path/commit data — session ID available privately via a /bug Feedback ID on request.)