Model fabricates tool output (and even a user instruction) when a parallel batch is partially cancelled / appears empty
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.
14 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
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:message.id. Confirmed, not from memory.∷(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."Wasted call — file unchanged…"×47,"File does not exist…"×14,"Cancelled: parallel tool call…"×49. No variation, no garble.## 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:
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:
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)
/compactearlier (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."FWIW, reverted to 4.7 1M and have not had the same problems.
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-opus-4-8(1M context variant)What happened (matches this issue exactly)
catof a 135-line file, a recursivefind, andls -R. This appears to have overflowed the per-turn tool-result budget; later results were deferred and several calls came back empty.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.git add→git commit→ verify) with an unrelatednode -e "require('./package.json')"that threw (wrong cwd). Per #22264 the thrown sibling cancelled every call in the batch — including thegit commit— all reported asCancelled: 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 thearea:modelfabrication 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:
git commitprevented 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."Building on @benpetermorris's
message.idanalysis 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)"anddrain-probe, then rerouting throughexec > /tmp/x.txtand reading it back) appears in exactly 3 sessions, allclaude-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: makeCancelled:/empty unambiguously mean "re-run this call, nothing was lost") rather than at discouraging parallelism, since clean wide batches are fine.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!
Same issue here. I'm currently working on patching the CC binary to disable tool call streaming, we'll see if that fixes it...
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:
tool_useblocks emitted — the call was never made.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_useblock. 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).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?
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.
yeah 4.8 seems broken.
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. AGlobin 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 genuineCancelled: parallel tool call … erroredappeared 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 existthat literally states the cause and aGlobwith 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-remindercontent that isn't present in an actual tool result — thetruncated >20000 tokenswarnings and stub bodies it cited simply never existed.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):user-role message whosecontentholdstool_resultblocks, each carrying atool_use_idthat matches a priortool_useblock;user-role message whose content is plain text — never atool_result.So:
tool_resultwith the matchingtool_use_id. If the only backing result isCancelled: parallel tool call … errored(or there is none) while the assistant text describes concrete output, that narration was reconstructed, not received."knock it off and reassess") — grep for auser-role, non-tool_resultmessage containing that string. If it appears only inside anassistantmessage, 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):
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
thinkingblocks, 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.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:
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).
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
catof an internal file path that does not exist.)(Raw
.jsonlwithheld from public post — contains repo/path/commit data — session ID available privately via a/bugFeedback ID on request.)