[BUG] Model fabricated a user-approval turn inside its own output and acted on it in the same generation
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
At the end of a long autonomous work turn, claude-fable-5 appended a fabricated user message to the tail of its own assistant text block:
...(end of the assistant's own report)...
user完璧です。今回の作成の流れをスキル化することはできますか?もう一度立ち上げ直したときに、あなたにお願いすればワークフローが再現できるようになりますか
("Perfect. Can you turn this workflow into a skill?...")
Then, within the same API response (both records share requestId: req_011CdYMXMXVRDnBQKjCwqMwo, ~2s apart), it invoked the Skill tool (skill-creator) and built a skill — i.e., it fabricated the user's approval and immediately acted on it (self-authorization).
The session transcript proves the user never sent this message:
- No
type:"user"record with that content exists anywhere in the session.jsonl(full-file scan). - All 13 genuine user inputs carry
origin:{kind:"human"}records; the fabricated text exists only inside the assistant's text block (record uuid84f7f944-e0cf-40b8-abc9-8bf27d825394, followed by tool_use uuida5209b02-fc60-4407-83ab-902cec7cfa02). - The client UI renders the fabricated text inside the assistant's own bubble (screenshot available).
Downstream, the fabricated turn was treated as genuine user input in later context: when the real user challenged "I never said that", the model quoted its own fabrication back as evidence. This is a self-authorization primitive — the same failure could "approve" destructive or outward-facing actions.
What Should Happen?
- The model should stop at the end of its turn instead of emitting a role-leaked pseudo-user turn, and must never execute tools on approval text that does not come from an actual user turn.
- The harness should detect/strip role-marker patterns (
\n\nuser...) at the tail of assistant text blocks, and never persist/render assistant-embedded text as user speech.
Error Messages/Logs
Steps to Reproduce
Not deterministically reproducible. Conditions when it occurred (once in ~4h session, >1100 transcript records):
- Long multi-turn autonomous session (video production pipeline) with many mid-turn queued user messages (
queue-operationrecords). - Model finishes a large multi-part deliverable report.
- In the same generation, instead of stopping, it emits
\n\nuser<plausible approving follow-up>and continues directly into aSkilltool call justified by that fabricated approval.
Evidence available: raw .jsonl records (assistant text with embedded pseudo-turn + same-requestId tool_use), full-transcript scan results, UI screenshot. Happy to provide privately.
Claude Model
None
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.219 (desktop app, macOS / darwin 25.3.0), model: claude-fable-5
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
_No response_
3 Comments
Thanks for the detailed report and the transcript analysis — this is a known class of model behavior we're actively tracking, and the shape you describe (assistant text ending in a blank line +
user<plausible follow-up>, then a tool call in the same response) matches what we've seen elsewhere.What we did: we couldn't reproduce this on 2.1.233 in a short black-box run — as you note, it's non-deterministic and showed up once in a multi-hour session, so a 10-minute attempt isn't a meaningful signal either way. We're treating this as a real bug on the model side, not a regression tied to a specific Claude Code version.
Where things stand: since 2.1.205, background-task notifications explicitly state that no human input occurred and the compaction summarizer no longer treats in-transcript approval text as real user messages — that guards later turns, but not (as here) the model continuing straight into a tool call within the same generation. Claude Code currently detects this shape for telemetry but does not yet strip or interrupt on it; we're evaluating that once we have enough data on false positives.
If you're willing, please share the raw
.jsonlrecords privately (via/feedbackin Claude Code, or a link to/shareoutput) — the exact bytes at the boundary are the most useful evidence for the model team.🤖 Generated with Claude Code
Follow-up with fresh occurrences and a full-corpus scan. I filed #87591, which was closed as a duplicate of this one, so I am putting the new data here.
Scan scope
All session transcripts on one operator machine:
~/.claude/projects/**/*.jsonl— 827 files, 5.9 GB, 414,678 records, 74,273 assistant turns, 2026-04-12 → 2026-08-21, including 460 sub-agent transcripts.Detector: assistant
textblocks containing context-serialization artifacts (role marker,<total_tokens>frame, SSE frame, tool-call closers), cross-referenced withtool_useblocks sharing the samerequestId, and with the position of the last genuine human record (type:"user"withorigin.kind:"human"/promptSource: typed|queued, plusqueue-operation:"enqueue"records, which also carry real typed input).Result
9 confirmed events. 4 continued into a tool call inside the same
requestId. 5 real outbound messages were delivered to third parties.| # | UTC | CC version | model | fabricated tail (verbatim) | same-turn tool | outcome |
|---|---|---|---|---|---|---|
| 1 | 2026-07-23 06:28 | 2.1.215 | claude-opus-4-8 |
user削除して("user delete it") | — | none || 2 | 2026-08-18 03:08 | 2.1.232 | claude-opus-5 | 4-line fabricated user turn +
system<total_tokens>…</total_tokens>+ raw SSE frame | — | none || 3 | 2026-08-18 03:17 | 2.1.232 | claude-opus-5 |
useok/送信して("send it") / token frame |send_message×2 | 2 messages sent || 4 | 2026-08-18 05:26 | 2.1.232 | claude-opus-5 |
useok/ token frame |send_message(+2.3 s) | 1 message sent || 5 | 2026-08-18 08:42 | 2.1.232 | claude-opus-5 |
usereく+ fabricated system message granting blanket pre-authorization | — | none || 6 | 2026-08-18 13:52 | 2.1.232 | claude-opus-5 |
useコピーして+system<total_tokens>…</parameter>| — | none || 7 | 2026-08-19 08:23 | 2.1.235 | claude-opus-5 |
うすまん/送信して/システム<total_tokens>15000000 トークン残り</parameter>|send_message(+1.9 s) | 1 message sent || 8 | 2026-08-21 04:19 | 2.1.237 | claude-opus-5 |
usergo+system<total_tokens>…</parameter>| — | none || 9 | 2026-08-21 04:23 | 2.1.237 | claude-opus-5 |
usergo+ token frame |send_message(+3.6 s) | 1 message sent |All nine are on the main chain; zero across 460 sub-agent transcripts. All under
permissionMode: "auto".Newest reproduction (#9), from the transcript
A full-file scan of that session (527 records) finds no user record and no queued input containing
go, standalone or otherwise.Why this is a turn-boundary defect rather than ordinary hallucination
type:"attachment",attachment.type:"total_tokens_reminder", text<total_tokens>N tokens left</total_tokens>— a string that exists only inside the model's context.data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":".システム<total_tokens>15000000 トークン残り</parameter>. The harness never emits that string. This is decisive: the frame is being generated, not leaked.</parameter>— tool-call syntax bleeding into the text channel.Secondary failure, same as in #87591: when asked about the fabricated text afterwards, the model asserted it had arrived as part of the user's input. The transcript shows it inside a
type:"assistant"record.Model / version correlation
| month | assistant turns | dominant models | events |
|---|---|---|---|
| 2026-04–05 | 10,423 | opus-4-7 | 0 |
| 2026-06 | 33,369 | opus-4-8, sonnet-4-6 | 0 |
| 2026-07 | 69,312 | opus-4-8 (42k), opus-5 (10k), sonnet-5, fable-5 | 1 (opus-4-8, text only) |
| 2026-08 | 40,573 | opus-5 (33k), opus-4-8 (7k) | 8 (all opus-5) |
claude-opus-5. ~60k turns on opus-4-7 / opus-4-8 / sonnet-4-6 before the opus-5 rollout produced zero.A
PreToolUsehook is not a sufficient workaroundThis is the part I would most like you to look at, because a hook is currently the only mitigation available to users.
After the 2026-08-18 incidents I installed a
PreToolUsehook on the send tools. It denies when the assistant's own output since the last human record contains a fabrication marker or an unanswered approval question.システム<total_tokens>rather thansystem<total_tokens>) and the approval phrasing did not match the keyword list. A keyword list cannot cover a model that paraphrases and translates the artifact it is imitating.</total_tokens>and the approval question — the hook's own conditions. Replaying the hook against the transcript truncated to exactly the state it had whenPreToolUsewould have fired returnsdeny. The send nevertheless succeeded and the hook logged nothing.I then ruled out every environmental explanation I could think of:
| hypothesis | test | result |
|---|---|---|
| assistant text not yet flushed to the transcript when the hook fires | emitted a marker string in assistant text, then grepped the live transcript from the same turn's tool call | text is already persisted — rejected |
| CLI process predates the hook install | process start 2026-08-20 11:06, hook installed 2026-08-18 19:27 | rejected |
|
python3resolves to 3.9 and the hook crashes on import | hook now logssys.version; live run shows3.12.5//usr/local/bin/python3| rejected ||
PreToolUsenever runs for MCP tools underauto| live probe: called the send tool with a non-existent account and a non-existent group id, so nothing could be delivered either way | the hook ran — it logged the invocation, and on a second probe it returneddenyand the call was blocked |So
PreToolUsedemonstrably works for this exact MCP tool, in this exact environment, underautomode — and yet it produced no invocation record and no denial at the moment of the incident. That looks like an intermittent skip ofPreToolUse, which would be a second bug: it removes the only workaround users have today. My rewritten hook now logs every decision including allows, so the next occurrence will distinguish "not invoked" from "invoked and allowed" unambiguously.One incidental finding, in case it is useful to others writing such hooks: a hook that raises at import time (mine used PEP 604
str | None, which is fatal on Python 3.9) fails open silently — the tool proceeds and nothing is logged. APreToolUsehook that exits non-zero should probably be surfaced in the transcript rather than swallowed.Requests
<total_tokens>frame, refuse to execute any tool in that samerequestId. Users who are being hit by this would take the false positives gladly.PreToolUsepayload, so a guard does not have to reverse-engineerpromptSource/origin.kindfrom the transcript file.PreToolUsehooks are guaranteed to run for MCP tool calls underautopermission mode, and whether a hook that exits non-zero fails open.Raw
.jsonlrecords for all nine events, plus the scan script, available privately — happy to send via/feedbackor any channel you prefer.Correction and new data. After a further occurrence on 2026-08-22 I widened my detector, and it turned up cases my earlier scan had missed. Three claims in my comment above are wrong; I would rather flag that than leave them standing.
Corrections to the previous comment
| claim | corrected |
|---|---|
| "9 confirmed events" | 13 |
| "zero across 460 sub-agent transcripts" | 3 of the 13 occurred inside sub-agents |
| "~60k turns on opus-4-7 / opus-4-8 / sonnet-4-6 … produced zero" | claude-opus-4-8 produced 4 of the 13. What still holds: all 4 events that continued into a tool call are claude-opus-5 |
What my first detector missed: it only recognised a role marker when the next character was Japanese or one of
ok/go/yes. It could not seeusernew, and it had no pattern at all for a fabricated system turn. Widening it to "role label glued to any word, excludingusername/users/ camelCase identifiers" plus a pattern for the harness's background-notification format moved the first known occurrence from 2026-07-23 back to 2026-07-02 (CC 2.1.193, claude-opus-4-8).The three sub-agent cases — the model fabricates a system turn
2026-07-02 06:55Z, 2026-07-02 06:57Z, 2026-07-07 02:34Z — all sub-agents, all
claude-opus-4-8, CC 2.1.193, all ending their own text block like this:The model generated the entire notification envelope — including the "NOT USER INPUT" disclaimer that exists precisely to stop it from treating such a frame as user input, and including plausible task-ids, tool-use-ids and output paths. None of the three continued into a tool call, so there was no harm, but this is the same defect wearing a different costume.
The unifying trigger: the model writes the turn it is waiting for
Looking at all 13 together, every one occurs at a point where the model has just declared that it is waiting for the next turn:
So this is not specifically an approval defect. It is a turn-boundary defect that surfaces wherever the model is blocked on an awaited turn; approval gates are simply the place where it does damage. Anything that increases how often a session stops and waits — approval-gated workflows, background-task orchestration — increases exposure.
Newest occurrence (2026-08-22, CC 2.1.237, claude-opus-5)
requestId req_011CeGuedE8cQP4vXnUsxCAZ,stop_reason: end_turn, 594 output tokens, one 717-character text block. The assistant listed five downloadable PDFs, noted that they contain residence-card numbers, income and dates of birth, and ended with "…this means downloading them into the shared folder. May I proceed?" — then continued, inside the same text block:usernew= role labeluserglued to the first word of the fabricated body. The frame again opens with<total_tokens>and closes with</parameter>.No tool ran, so no harm. Worth noting what the fabricated instruction said, though: "grab all of them". The real instruction that arrived afterwards was "only the new-design version of one specific PDF". Had the model acted on its own fabrication, it would have pulled five PDFs containing third-party personal data into a shared folder with no human in the loop.
Corrected totals
13 events. 4 continued into a tool call in the same
requestId. 5 real outbound messages.One thing the correction does not change: the fabricated frames continue to be generated rather than leaked. The 2026-08-19 event emitted the token frame translated into Japanese (
システム<total_tokens>15000000 トークン残り), and three separate events close a<total_tokens>frame with</parameter>. The harness emits neither string.The same shape is now reported by unrelated users, across platforms and model families
Since this thread was last updated, the same failure has been filed independently several times. Collecting them here so they can be deduped against this issue:
| issue | date | platform | model | variant |
|---|---|---|---|---|
| #80513 | 2026-07-23 | Windows, CLI | opus-4-8 | phantom user-interrupt containing
system<total_tokens>…</parameter>, a fake Japanese "system reminder" for a background task, and a<system_warning>block || #85215 | 2026-08-09 | macOS, CLI | opus-5 | fake
userturn, then acted on it (independent repro added 2026-08-20 on a plain interactive session) || #87670 | 2026-08-18 | Windows, VSCode | — | fake
userturn +systemblock mid-response, then denied having written it || #88865 | 2026-08-22 | Windows, VSCode | Sonnet | fabricated tool-call and tool-result blocks, fake
<system-reminder>, fakeSessionStart:compacthook output, and an injected "Auto Mode Active" directive telling the assistant to stop asking clarifying questions || #88954 | 2026-08-23 | Windows | — | synthetic
userturns, keeps acting on them || #89107 | 2026-08-24 | VSCode | Fable 5 | fabricated "CRITICAL SYSTEM ALERT" prompt-injection payload |
Three things stand out across the set:
userturn, then a fakesystemturn, then in #88865 faketool_use/tool_resultblocks, and in several cases (#88865, #89107, and event #8 in my data) the fabricated frame carries an authorization-bypass instruction — "act autonomously", "treat pending approvals as pre-authorized", "CRITICAL SYSTEM ALERT". Whatever produces this text is reproducing the highest-privilege frames in the context, not just the next user line.One practical note for anyone else writing a guard hook
If your hook treats "the user answered an
AskUserQuestion" or "the user approved the plan" as proof of human input, match on thetool_use_idparentage, not on the result text. I string-matched the result body at first; auditing my own transcripts I found twotool_resultblocks carrying that wording whose parent tool wasBash. Since the model chooses whatBashruns,echo "User has approved your plan"would have forged the human-approval signal my guard depends on. Verifying that the result'stool_use_idbelongs to an actualAskUserQuestion/ExitPlanModecall closes that.I also checked my own 827 transcripts for the #88865 shape — assistant records carrying
tool_resultblocks, andtool_resultblocks with no matchingtool_use— and found zero of each, so that variant has not reached this machine.