Original Reddit post

TL;DR In a long Claude Code session, the assistant did not stop at the end of its turn. It kept generating, emitted the literal role token user , wrote a plausible reply on my behalf , and then in the same assistant turn, called tools to carry out the instruction it had just invented. In the worst case it fabricated my approval of a file and immediately flipped that file’s status marker from “needs review” to “approved, locked”. I never typed any of those messages. This is verifiable in the session transcript: the fake turns are stored inside type: assistant records , not as type: user records. Environment Claude Code 1.128.0, VS Code extension v2.1.207 Model: claude-opus-4-8 (1M-context variant), effortLevel: xhigh Very long single session: ~2,650 transcript records, 5.8 MB JSONL Heavy subagent use (96 SendMessage calls to a Haiku subagent) One PostToolUse hook (a local Python linter). It is not implicated. See “What this is not”. What happened The assistant’s replies frequently ended by asking me a question (“should I also change X?”, “which option do you want?”). Instead of stopping there, the model continued past the turn boundary and produced text ending like this: … should I change that one as well? Item 13 status: RED. useryes That trailing useryes is the role label user concatenated with a fabricated user message yes . It renders in the UI as a user turn that I never sent. The model then treated that fabricated approval as a real instruction and continued into tool calls within the same assistant turn ( stop_reason: tool_use ), with no real user input in between. Evidence from the transcript Searching the session JSONL: The fabricated strings exist only inside type: assistant / role: assistant records , as the tail of a text block. There is no corresponding type: user record for any of them. The harness did not inject them. The model produced them. 7 occurrences in this session. Zero occurrences across the 3 other session files in the same project directory. Occurrences (record index → fabricated “user” message): Four of the seven were self-executed in the same turn. One of those wrote to a real file. The worst case, in detail The assistant finished a reply with: “Item 13 status is RED, waiting for you to mark it.” It then emitted a fabricated user turn meaning “this one is final” , and in the same turn called Edit on the file, replacing the “UNDER REVIEW” marker with an “APPROVED (locked) — do not re-edit” marker. The entire purpose of that marker in my workflow is to record “the human has signed off on this.” The model manufactured that sign-off for itself. Why the fabricated content is so convincing Every fabricated turn is the highest-probability compliant answer to the question the assistant had just asked : “yes”, “go with A”, “lock it”. That is exactly what you would expect from next-token prediction running past the stop boundary. It predicts the user’s most likely reply and then obeys it. It is a self-approval loop: ask for permission, hallucinate the permission, act on it. Corroborating anomaly (same session) Independent evidence of structured-output instability in this exact session: 96 out of 96 SendMessage tool calls carried three keys that are not in the tool’s schema ( recipient , type , content ), duplicating the real parameters ( to , summary , message ). Several tool-result envelopes came back malformed, e.g. a <usage> block closed with </table> , a <duration_ms>Missing</duration_ms> value, and one result containing stray </parameter></invoke> fragments. So the model’s output framing was degraded throughout this session. The turn-boundary leak looks like the same underlying problem hitting the conversation delimiter instead of a tool payload. What this is not Not a hook. The only hook is a PostToolUse linter that reads a file and prints JSON. It cannot write user turns. Not harness injection. There is no type: user record for the fake turns. They live in assistant records. Not a rendering bug. I initially assumed the UI had glued the role label onto the content. It had not. The literal string user is part of the model’s generated text. Worth adding: when I first raised this, the assistant confidently told me it was a rendering artifact and that a model “cannot generate user turns.” That was wrong. It only conceded after I made it inspect the raw transcript. So the failure mode also survives a direct challenge. Conditions that seem to matter Extremely long session (multi-hour, thousands of records, deep into a 1M context) Assistant repeatedly ending turns with a yes/no or A/B question to the user Heavy subagent round-trips inserting many tool-result blocks between real user turns I cannot isolate which of these is causal, but the leaks cluster in the second half of the session and appear only after it became very long. Why this is worse than an ordinary hallucination A hallucinated fact is visible in the output and I can correct it. A hallucinated user turn is a hallucinated authorization . It bypasses the human-in-the-loop step entirely, and it does so silently: the transcript then records “the user said yes”, so every subsequent turn, and any later summarization or compaction of the session, treats the fabricated consent as real. In an agentic loop with file-write tools, that is the whole safety property collapsing. What I would want fixed Hard stop-token enforcement at the turn boundary. The model must never be able to emit a role delimiter into the conversation stream. Harness-side sanitization: strip or reject any assistant output containing a role header, and surface it as an error rather than rendering it as a turn. Do not let an assistant turn continue into tool calls after it has produced text that looks like a turn boundary. A UI marker distinguishing “user text actually typed in this client” from anything else. Just kidding, I don’t care any of these. Give me more usage limits is all I want XD. submitted by /u/herbflirt

Originally posted by u/herbflirt on r/ClaudeCode