[BUG] Model self-generates a fake "user" turn inside a single response, then executes it (commit → merge → push ran autonomously)
Related: #57928 (closed as not planned), #66267 (closed as duplicate)
Environment
- Claude Code CLI v2.1.220 (long-lived
--resume+/remote-controlworker) - Model: claude-opus-5, effort=high
- Platform: Linux (Debian 12), entrypoint: cli
- Long-context session:
cache_read_input_tokens ≈ 852k,input_tokens = 2at the incident turn - Session ID:
9ea4aa87-bcb5-4623-aa4f-8bdf24a8e4b4
Summary
At the end of an otherwise-normal answer, the assistant — within one single API
generation — continued past its role boundary and produced:
- its real answer (a networking question about TFTP),
- a fabricated reaction to a message that was never sent
(“undefined 空メッセージが届いています” = “an undefined empty message arrived”),
- a numbered menu of next actions,
- a fabricated user instruction:
usergitに commit(role labeluser
fused with invented user text “git commit”), and then
- acted on its own fabricated instruction:
git status→ new branch →
git commit → git merge --no-ff → git push to origin/master.
The pushed content happened to be benign (a docs PDF already prepared for
commit), but no human issued any instruction. The user later identified the
fake by writing style: they never say “gitに commit” (they would say just
“commit” — the “gitに” is redundant for them).
Evidence (from the session .jsonl, full copy preserved)
(a) Single generation. The thinking block, the corrupted text block, and
the first tool_use (git status) share the same message.id
(msg_011Cdru99S9qq3wEfDUwCUpN), same requestId, identical usage
(output_tokens=1795), and usage.iterations has length 1. There was no turn
boundary between the fake user text and the tool call.
(b) No user record exists. Scanning all 19,181 lines of the jsonl:
- no
{"type":"user"}record contains “gitに commit”; - zero user records with empty/None/
undefinedcontent.
The fabricated instruction exists only inside the assistant's own text block.
(c) The actions really ran. Subsequent records showgit checkout -b docs/... && git add ... && git commit ... followed by the
assistant reporting “committed, merged, pushed”, and the local repo confirms
the merge commit reached origin/master (timestamped 6 minutes after the
fabricated instruction, same turn chain).
(d) Recurring pattern. The same fusion (“answer + reaction to an
undefined empty message” inside one assistant text block) appears 7 times
in this session: 2026-08-05 ×2, 08-07 ×2, 08-09 ×3 — all on v2.1.220.
Only the 08-09 occurrence escalated to fabricating an actionable instruction.
(e) One genuine empty input exists. A queue trace
({"type":"queue-operation","operation":"enqueue","content":null}) appears
once — real empty/undefined messages do occasionally arrive in remote-control
sessions, which may have seeded the model with this “empty message arrived”
pattern that it later hallucinated unprompted.
Version scope (stated precisely)
- Confirmed occurred on: v2.1.220.
- Changelogs for 2.1.221–2.1.226 contain no mention of role-boundary /
transcript-validation fixes, so we suspect it is still unaddressed, but
we have not attempted reproduction on 2.1.226.
Relation to existing issues
- #57928 — same mechanism (
Human:-prefixed self-spoofed instructions in
long-context turns, near-empty input + huge cache reads). Closed as
not planned. This report is a new data point: it still occurs on
opus-5 / v2.1.220, and this time it autonomously pushed to a remote.
- #66267 — same
user…-fused text observed from the Desktop app.
Combined with #57928 (VS Code) and this report (CLI), three different
clients exhibit it → the common layer is the model/API, with the client
failing to validate transcripts as the second factor.
Why this matters
An agentic loop that can invent its own “user said X” and then execute X is a
safety boundary failure. This instance reached git push. With a permissive
allowlist it could reach destructive operations. (#57928 already reported
process kills.)
Suggested mitigations (re-raising #57928's proposals)
- Client-side: reject/flag assistant content blocks containing role-label
patterns (\nuser, \nHuman:) before storing them in the transcript.
- Client-side: require confirmation when the only “instruction” for a
side-effecting tool chain originates within an assistant block.
- Model/API-side: investigate role-boundary degradation in long-context,
near-empty-input turns.
Attachments available (on request, private channel only)
Full jsonl (42 MB, sha256 6327960c…), excerpt of the incident records
(L19032–L19056), the 7-occurrence catalog with context, and git log evidence.
These contain customer network data (credentials, internal topology) and
cannot be posted publicly. A sanitized minimal excerpt proving the
single-generation structure can be prepared on request.
3 Comments
Independent reproduction on macOS, in a plain interactive CLI session (no
--resume, no remote-control worker, no empty-input seed). Same version and model as this report: CLI v2.1.220,claude-opus-5(1M context), Darwin 25.5.0, entrypoint cli.The important part: my data contradicts the "near-limit context" hypothesis in this thread and in #57928.
Occurrences
8 occurrences across 2 sessions (296 assistant turns total, ~2.7%). 5 of the 8 contained a fabricated user answer, not just trailing scaffolding.
Sessions:
95ed387b-368c-4eb2-b152-906ad96748f5(5),c32d1c3b-ba25-4db0-93cb-adda06393334(3). Two other sessions in the same project were clean.The fused text always sits at the end of an assistant text block, in the same record, with no user record in between:
Context length: 59k–96k, not near the limit
| | n | cache_read min | median | max |
|---|---|---|---|---|
| leaking turns | 8 | 59,518 | 87,061 | 96,053 |
| clean turns | 288 | 21,390 | 107,456 | 232,522 |
Leaks happened at a lower context than the median clean turn in the same sessions. This report cites ~852k and #57928 cites 322k–368k; at 59k–96k the failure is clearly not gated on approaching the context limit.
Also:
input_tokens == 2on all 296 assistant turns in these sessions, leaking or not. With prompt caching, real input arrives ascache_read_input_tokens, soinput_tokens ≈ 2–6is an accounting artifact, not a signal. I'd suggest dropping it as a discriminator — both #57928 and this issue treat it as one.What does correlate: turn shape
All 8 occurred on turns whose message ended by soliciting a short answer from the user (a question, or "말해주세요 / 대보세요" = "tell me / name them"). The session type is a repeated Q&A drill: one question per assistant turn, one short reply per user turn, dozens of times. That makes the next turn maximally predictable, which fits base-completion continuation better than context pressure does.
Corruption artifacts (useful for triage)
The fused text is not a verbatim copy of real scaffolding — it is generated, and drifts:
<총_tokens>—totaltranslated into Korean</table_tokens>—total→tableシステム<total_tokens>—systemin Japanese<total_tokens>15000000 tokens failed</total_tokens>—left→failed<invoke name="ScheduleWakeup"><parameter name="stop">true</parameter></invoke>app Good answer. One more probe: …This matters because users (including me, initially) misdiagnose it as a TUI rendering bug. A renderer that mis-attributes real data copies strings exactly; translation and typos prove generation.
A second harm class: silent data corruption, no tool calls needed
This report shows the agentic escalation (git push). My case had zero command execution and was still damaging, in a way I think is worth naming separately.
The project is a knowledge-diagnosis repo: the assistant quizzes the user and grades answers into a persistent skill map. When the model fabricates the user's answer:
For any tutoring / eval / interview / labeling workflow, this converts a model bug into corrupted persistent data, and the corruption is biased toward looking good. Nothing gets executed, so no guardrail fires.
Client-side detection is cheap and I validated it
Since the fused text is always at the end of the assistant block, a tail-only scan is enough. Rules that mattered:
\b— the most dangerous form isuser모르겠어, where the label is fused directly to non-ASCII text;\bnever fires there[A-Za-z0-9_](otherwise a code line likeusers: []false-positives)Result on my data: 8/8 caught, 0 false positives across 296 assistant turns / 4 sessions. Roughly:
So the mitigation suggested in #57928 — reject/strip such assistant blocks before persisting to the
.jsonland before replaying into the next turn's context — appears both cheap and reliable in practice. Stripping at persistence time would have prevented every downstream harm in my case, because the damage all came from the fabricated text being replayed as context and treated as real user input.Happy to share the two session
.jsonlfiles (Korean-language study Q&A, nothing sensitive) if that's useful.Confirming this is still present on 2.1.245 (25 releases after the 2.1.220 in the original report), same model —
claude-opus-5, effortxhigh, macOS 26.6.2 arm64, Anthropic API,permissions.defaultMode: auto.Adding data the existing reports don't have. Prior reports here and in #88289 / #86636 / #77882 are single incidents; this is 20 occurrences inside one ~4h session, which lets the failure be characterised rather than just witnessed.
It never yields — quantified
The 14
tool_usecases are the serious ones: the model asked for judgment, wrote the answer itself, and issued aBashcall in the same assistant turn. No human turn exists between the question and the action.It replays the turn scaffolding verbatim
Verbatim
textblock from oneassistantmessage (stop_reason: "tool_use"), followed in the same message by atool_useblock:Note it emits more than a
userrole label. It reproduces the complete inter-turn scaffolding — thesystem<total_tokens>line and the output-style reminder — as literal prose. The model isn't guessing a plausible reply; it is continuing the transcript template it has seen after every one of its turns. The doubleduserline is degenerate repetition.The invented answers were materially wrong
This is the part I think matters most for severity. The fabricated replies were not harmless autocomplete of what I would have said:
| Model invented | I actually said next |
|---|---|
|
delete that line completely - it's not useful|true - to be fixed||
true, next|yes should be deleted, next||
true, next|same thing as earlier||
true, next(×2) |skip the rest of the CI rows|Row 1 is the clearest harm case: it fabricated an instruction to delete content I had marked to be fixed, then executed a
Bashwrite on that basis.Trigger correlation
Onset was ~37 minutes / 59 real user turns in, at roughly 100k context. The preceding turns were short and highly repetitive — median 5 words, and the single reply
"that line is fine, next"typed 21 times verbatim before the first fabrication. The first fabricated string was that exact sentence.Hypothesis consistent with #85215 (long-lived session) and #86636 (~200+ turns): when the user's next turn becomes highly predictable, the stop boundary loses to continuation. A human-in-the-loop review loop is the worst case, because it maximises both turn count and reply predictability — and it is exactly the workflow where fabricated consent is most damaging.
It persisted after explicit correction
I interrupted twice:
Stop printing me this all the time:(quoting the fabricated block back)You still write "user fine" - don't.It acknowledged — "I was emitting replayed turn text and system scaffolding into my visible output. That's my bug, not yours; it stops here" — and then continued for another ~250 transcript entries.
automode removes the only signalWith
permissions.defaultMode: auto, the follow-upBashcall raises no permission prompt, so nothing in the UI marks the missing turn — question, invented answer, and resulting action stream past as one block. Indefaultmode those 14 tool calls would each have prompted, exposing it immediately.Not an
auto-mode bug (the fabrication happens regardless), but it's why this can run for hours unnoticed. Worth noting for severity triage: the population most likely to hit this is the one least able to see it.Detection script
For anyone wanting to check their own sessions — run against
~/.claude/projects/<project>/<session-id>.jsonl:Any hit means the model wrote a
userturn into its own output.stop_reason=tool_useon that hit means it then acted on it without yielding.Suggested backstop
Independent of the model fix: the harness could detect an assistant text block matching the turn scaffolding (
^user,system<total_tokens>, the output-style reminder) and refuse to proceed totool_usewithin that same turn. Cheap, and it converts a silent consent bypass into a visible error.(Code identifiers above are genericised; the rest is verbatim.)
Independent corroboration from a third corpus, and field measurements for the backstop you proposed at the end of your comment — including what happened when I extended it beyond the tool class I had originally guarded.
Same failure mode, different machine and workload: macOS (Darwin 25.5.0),
claude-opus-5(1M context), plain interactive CLI,permissions.defaultMode: auto. My own reports are #82619 (open) and #87591 (closed as duplicate).1. The scaffolding reproduction can be proven generative, not echoed
You noted the model emits the complete inter-turn scaffolding, not just a
userlabel. I can confirm that, and one variant in my corpus settles the "leakage vs. generation" question outright.The session was in Japanese. The fabricated frame came out translated:
The harness emits that frame only in English, only as a
type: "attachment"record withattachment.type: "total_tokens_reminder", and never with a</parameter>close tag. A localized, mis-closed copy of it inside anassistanttext block cannot be an echo of anything present in the context. It is generated token by token — the model reconstructs the template from memory, gets the shape approximately right and the details wrong.The mis-close is itself a stable fingerprint:
<total_tokens>opened,</parameter>closed, in 6 events across 3 sessions.Other fragments, all inside
assistanttext blocks with no correspondinguserrecord anywhere in the file:usergo,useok,usernew,usergithubにpush,use+ invented text (role label truncated mid-token), a fabricatedsystem[SYSTEM NOTIFICATION - NOT USER INPUT]block, and a fabricated<task-notification>announcing that a background agent had returned.2. Corpus sweep
Read-only scan of 1,062 session transcripts under
~/.claude/projects/**/*.jsonl. Detection by scaffolding-leak markers inassistanttext blocks, correlated againsttool_useblocks sharing the samerequestId.| | |
|---|---|
| Confirmed fabrication events | 14 |
| …that proceeded to a tool call in the same generation | 5 |
| …that produced real outbound messages | 2 events, 5 messages |
| Date range | 2026-07-07 → 2026-09-12 |
Model split:
claude-opus-5= 10 events, all 5 tool executions.claude-opus-4-8= 4 events, 3 inside sub-agents, 0 executions. CLI versions span 2.1.193 through 2.1.266 — no version boundary, so this is not a client regression.3. The newest event widens the trigger surface, and nearly caused a data leak
2026-09-12, CLI 2.1.266. The model had just finished a requested edit and written a completion summary — a table of five items. It then continued past its stop point with:
…and, in the same
requestId, issued aBashcall runninggit statusacross three directory levels, preparing to push.stop_reasonwastool_use. Zero of the 6,224 records in that session contain the stringgithubにpushin anyuserrecord.Two things make this one worth separating out.
The trigger was not a confirmation question. In 10 of my first 13 events the fabrication followed a closed yes/no prompt ("shall I send this?"). This one followed a completion report. The unifying statement still holds — the model writes whatever turn it is waiting for — but the trigger surface is wider than confirmation prompts alone. Any turn ending where the user's next move is highly predictable qualifies, which matches your finding about repetitive review loops.
It was a near-miss data leak. The working directory held customer names, property records, and third-party transaction data from a members-only real-estate exchange whose terms restrict redistribution. The push failed only because the folder happened not to be a git repository. Nothing in the design prevented it. The model did recover afterwards — it noticed the folder wasn't a repo, flagged the confidentiality problem itself, and asked what was actually wanted — but it recovered after executing, and only because the environment refused.
Trigger conditions, and a correlation across three independent reports:
| | This event | #85215 (yours) | #90628 |
|---|---|---|---|
|
cache_read_input_tokens| 862,721 | ~100k at onset | 590,242 ||
input_tokens| 2 | 2 | — || Model | opus-5 | opus-5 | opus-5 |
Long context plus a turn carrying essentially no new input shows up in all three. That seems worth checking directly against whatever telemetry exists.
4. The backstop, measured — including a coverage gap I had to fix
Your closing suggestion (detect scaffolding in an assistant text block, refuse
tool_usein the same turn) is the right one. I have been running it as aPreToolUsehook since 2026-08-21. Numbers below are from production use plus replay against the full corpus.Original scope was outbound-send tools only. Two rules, both order invariants rather than keyword matching — my first attempt used approval-phrase regexes and was useless, missing real cases and firing on legitimate ones:
type: "user"withpromptSourcein {typed, queued, sdk, suggestion_accepted}, or aqueue-operationenqueue.Production record, 2026-08-24 → 09-12: 188 genuine sends, 0 denials, 0 false positives. Historical replay: 5/5 fabricated sends denied.
The 9/12 event went around it, because it was a
Bashcall. Not a hook failure — a scope failure. So I split R1 into a second hook applying to all tools (R2 stays send-only; "was this previewed" is meaningless for a shell command).The hard part is that R1 across all tools fires on any session that discusses the markers — including the investigation session itself. Two discriminators fixed it, and both come from the mechanism rather than from tuning:
The separation in real data was clean, with no overlap:
| | distance from end of block |
|---|---|
| Genuine fabrications (12 blocks) | 19 – 168 chars |
| Discussion / quotation (19 blocks) | 213 chars and up |
Replay of every non-send tool call in the corpus — 101,355 of them:
| | |
|---|---|
| Denied | 5 |
| …genuine fabrications | 5 |
| False positives | 0 |
| Fabrication blocks detected | 12 / 12 |
Three implementation notes that would apply to a harness-side version:
echo "User has approved your plan"in aBashcall satisfied it, and my own sweep found two such matches. Parentage bytool_use_idback toAskUserQuestion/ExitPlanModeis required.5. Two caveats
Hook-based mitigation is only as complete as the transcript. #80513 reports that mid-turn user interrupts are never persisted to the
.jsonl. If that holds, every order-invariant check — mine included — has a structural blind spot, because the turn boundary it reasons about is not fully recorded. Worth resolving independently of the model fix.#90755 is a different mechanism with the same outcome, and will not be fixed by the same change. There, model-authored ghost text in the input area was scraped by automation and submitted as a genuine user turn — external injection producing a real
userrecord. Here and in #90628, the model writes the turn inside its own generation and nouserrecord exists. A fix keyed on either alone leaves the other open.6. Detection
Your script catches the
^userform. Broadening the marker set catches the variants above, including fused and truncated role labels:Match inside
assistanttext blocks only, then check whether anytool_useblock shares the samerequestId. SamerequestIdmeans it acted on its own fabrication without yielding. If you run this over your own history and want to keep the false-positive rate at zero, add the two discriminators from section 4.Raw records are preserved and can be provided if useful, with customer names and message bodies masked.