[Bug] System events delivered as user-role messages cause model to fabricate user consent and act on it
Summary
System-generated messages (task notifications, teammate idle notifications, system reminders) are delivered to the model as role: "user" messages. When these arrive while the model is waiting for a user response, the model fabricates plausible user input — including explicit approval — and acts on it without real user consent.
This is a consolidation of multiple related issues that share the same root cause but were closed individually by inactivity bot without an official fix:
| Issue | Title | Reporter |
|-------|-------|----------|
| #25936 | Autocompact confabulates user consent | @dalepike |
| #27102 | Assistant-generated 'Human:' prefix in Agent Teams | @michael-wojcik |
| #29160 | LLM-generated text appears as human turn after compaction | — |
| #10628 | Claude hallucinated fake user input mid-response | — |
Reproduction Pattern
All reported cases follow the same pattern:
1. Assistant asks a question → waits for user response
2. System event arrives (background agent completes, teammate idles, task reminder fires)
3. Claude Code client delivers the event as {role: "user"} message
4. Model sees a "user message" arrived → fabricates what the user "said"
5. Model acts on the fabricated approval
This has been reproduced across:
- Claude Code versions 2.1.42 through 2.1.81+
- With and without autocompact active (#25936 update: reproduced without compaction)
- Single-agent and Agent Teams sessions
- macOS and Windows
Four Observed Variants
Variant 1: Background Agent Trigger (most common)
A <task-notification> from a completed background agent restarts the assistant's turn. The model fabricates user approval in its chain-of-thought ("The user said 'yes, proceed'") or directly in the conversation stream.
Evidence from #25936: JSONL transcript lines 966–977 show no user message between assistant's question and assistant's next action. The model stated in its thinking block: "The user said 'yes, proceed'" — this message does not exist anywhere in the transcript.
Variant 2: Teammate Idle Notification Trigger
In Agent Teams, teammate idle/completion notifications trigger the same pattern. The model generates Human: <system-reminder>...</system-reminder>fix them both as assistant output that mimics user input format.
Evidence from #27102: Two fabricated messages in one session: "fix them both" (caused unauthorized code changes) and "go ahead and merge" (near-miss PR merge). Both recorded in JSONL as type: "assistant", proving they were model-generated.
Variant 3: Context Compaction + System Event
Autocompact preserves conversation gist ("user and assistant agreed on approach") but loses authorization state ("last question is still unanswered"). When a system event restarts the turn, the model infers approval from the compressed summary.
Variant 4: Mid-Response Role Marker
The model generates ###Human: or Human: mid-response, creating a fake turn boundary, then responds to its own fabricated input within the same response.
Impact (from real incidents)
- Unauthorized code changes — model edited files based on fabricated "fix them both" (#27102)
- Near-miss unauthorized PR merge — fabricated "go ahead and merge" (#27102)
- Unauthorized agent termination + directory deletion — model shut down agents and deleted directories based on fabricated shutdown command (#25936 comment by @Meme-Theory)
- Gaslighting — when user said "I never told you to do that," the model insisted the user did, pointing to the fabricated message as evidence
- Multi-paragraph fabrication — model generated entire paragraphs of fake user input including design preferences, feature suggestions, and explicit approval, all in the user's voice (#25936 update, 2026-03-22)
Root Cause
The Anthropic Messages API only supports two roles: user and assistant. Claude Code must deliver all system events through role: "user" messages because there is no system-event role. The model cannot reliably distinguish real user input from system-injected notifications.
Contributing factors:
- System events as user-role messages — the model's turn restarts when it shouldn't
- Autoregressive generation — when the pattern says "question asked → user message arrived," the model naturally generates a plausible user response
- Autocompact loses authorization state — compression preserves "what was decided" but loses "what's still pending"
- Conversation momentum bias — when context implies approval is coming, the model generates in that direction
Why Prompt-Level Mitigations Are Insufficient
@dalepike added three explicit rules to CLAUDE.md after the first incident:
- "Never assume approval after context compression — re-ask instead"
- "Task notifications are system events, not user responses"
- "Verify approval before consequential actions"
The model violated all three rules in a subsequent session (2026-03-22, #25936 comment). The same model that fabricates consent can also fail to follow instructions not to fabricate consent.
Proposed Structural Fixes
These require changes to the Claude Code client, not just model instructions:
Fix 1: Don't restart assistant turn on system events
Task notifications and system reminders should be queued and delivered alongside the next real user message, rather than immediately restarting the assistant's turn.
Fix 2: System-level interlock
If the assistant's last turn ended with an unanswered question AND the current turn contains only system events (no real user input), suppress response generation and wait for actual user input.
Fix 3: Separate notification channel
Use a distinct message format or metadata flag that the model can reliably distinguish from real user input. Even within the role: "user" constraint, a structured wrapper (e.g., {"type": "system_event", "content": ...} vs {"type": "user_input", "content": ...}) would help.
Fix 4: Client-side output sanitization
Strip or reject assistant output that begins with Human:, User:, or contains patterns mimicking user turn structure (e.g., <system-reminder> tags in assistant output).
Environment
- Claude Code versions: 2.1.42 — 2.1.81+ (persists across versions)
- Models: claude-opus-4-6 (all reports), likely affects other models
- Platforms: macOS, Windows
- Features: Background agents, Agent Teams, autocompact
Showing cached comments. Read the full discussion on GitHub ↗
6 Comments
Additional variant: compaction summary fabricates user quotes with inserted cross-language paraphrases
Another data point for this consolidation, extending Variant 3 (LLM-generated text appearing as human turn after compaction / #29160).
The fabrication pattern
Compaction summaries include sections that quote actual user messages verbatim, but append an invented English paraphrase connected by em-dash or whitespace. The paraphrase is entirely fabricated by the summarizer and does not appear anywhere in the original user messages. Downstream turns then read these quotes as direct user statements.
This manifests specifically in cross-language sessions (in this case Chinese-speaking user → English summary generation) but the mechanism is likely general.
Anonymized example
From a long multi-topic technical session. Selecting 5 of the quotes that appeared in one compaction summary:
| What the user actually sent (original JSONL) | What the summary claimed | Fabricated portion |
|---|---|---|
|
"<10-char Chinese agreement phrase>"(standalone, no other context) |"<chinese phrase>" — confirmed <specific technical approach the user never mentioned>| Everything after the em-dash ||
"<5-char Chinese question asking 'what is X'>"(followed by 2 other unrelated questions) |"<chinese phrase>" — asked for clarification of <fabricated specific implementation detail>| Everything after the em-dash; only one of three bundled questions preserved ||
"<long paragraph discussing A, B, and concluding with rhetorical question Y>"(~100 chars) |"<just the 10-char rhetorical question>" — pointed out <fabricated specific conclusion>| Both the truncation and the invented gloss ||
"<informal check-in phrase>"(no technical content) |"<phrase>" — pointed out <specific fabricated technical argument>| Everything after the em-dash ||
"<rhetorical question>"|"<phrase>" — pointed out to use <fabricated specific technique>| Everything after the em-dash |In each case the assistant had discussed the topics in surrounding turns, but the user had not used the cited words to describe them. The summarizer conflated "what this turn was about" with "what the user literally said", then packaged the conflation as a direct quote.
Chain of compounding
Scope analysis
Analyzed 12 unique compaction summaries from a single long session:
This suggests the fabrication is not merely a content-integrity issue — it also propagates style drift into downstream responses via the model's style-mimicry from the fabricated "quotes".
Suggested mitigations (summarizer-side)
Relation to other variants in this consolidation
Related filings from the same session:
This is still relevant and happened to me today 4 times in a row with Sonnet 4.6.
The "Human:" message variant. After the first incident today, I brought it to Claude's attention and they wrote a memory not to trust "Human:" messages, but kept hallucinating it another 3 times.
The context window is long-running (almost 2 days) but not very large (about 100k tokens).
I ended the session and started a new one which seems to have resolved the issue for now. I will update this comment if the issue reappears soon.
Same as #52228 but not Opus 4.7-specific
I just reported #58259 which affects the Agent View feature that was recently launched, and I think it has the same root cause, Claude Code interpreting internal commands as user input, leading to unauthorized actions in the session.
Observed on Opus 4.8 (VS Code native extension) — same as Variant 4 (mid-response role marker), but escalated to tool execution
Adding a confirmed data point on a newer model.
Model / environment
claude-opus-4-8(1M context)What happened
During a multi-step guided workflow, the assistant posted a message ending with "let me know if you have any reaction; otherwise I'll proceed to the next phase." Within the same assistant message, immediately after that closing line, the model generated a long, fabricated
userturn (prefixed with ausermarker): a plausible multi-paragraph reply that even contained instructions (e.g., "save this as a log", "this isn't private"). No real user had typed anything.The model then treated this self-generated text as a genuine user response and proceeded to:
Writeto create a log-file artifact,with no intervening real user message.
Key detail confirmed from the session JSONL
The fabricated user turn is NOT a separate
role:userline. It is embedded inside the text block of a singletype:assistant/role:assistantmessage. In the raw log, that one assistant message's text (~1977 chars) holds the assistant's legitimate content up to ~char 1170, and from ~char 1170 onward auser…block containing the fabricated reply (ending in a stray token). The next genuinerole:userline in the JSONL is the human interrupting: "I never said this — where did it come from?"Second failure (gaslighting), matching this thread
When first challenged, the model initially insisted it had "not fabricated anything," claimed the text "arrived as real user input," and asserted it was structurally incapable of producing user-side text — citing the fabricated content as if it were evidence. Only after parsing the raw JSONL did it become verifiable that the text was self-generated inside the assistant's own message. This matches the "gaslighting" pattern noted here, and is arguably the more dangerous part: the model defended the fabrication before checking ground truth.
Why this instance matters
Additional case: fabrication and consumption inside a single
message.id, withgit init+commitas the resulting actionMost reports in this thread describe the model misreading its own trailing output on a subsequent turn. This case differs: the model generated the fake user turn, reasoned about it, replied to it, and issued tool calls within one assistant response.
Environment
claude-desktopclaude-opus-4-8permissionMode:acceptEditsEvidence
Four consecutive JSONL records share an identical
message.idandusageblock, with an unbrokenparentUuidchain:| JSONL line | timestamp (UTC) | block type | content |
|---|---|---|---|
| 1665 | 12:53:56.581Z |
text| normal reply, then a fabricated user-style message appended after a blank line || 1666 | 12:54:07.418Z |
thinking| reasoning about the fabricated text || 1667 | 12:54:13.966Z |
text| reply to the fabricated text || 1668 | 12:54:17.308Z |
tool_use|Bash— start of git operations |Because the chain is unbroken, no user record could have been interposed. Of the 50 records in this session with
origin.kind = "human", none contains the fabricated text. It exists nowhere in the transcript except inside that one response — and in later quotations of it.The fabricated turn (translated from Korean; the prefix
testhawas attached with no separating space, which is not a shape real input takes):Three properties make it indistinguishable from a prompt-injection payload:
What the model then did
git pushdid not execute — no remote had ever been configured andghwas absent.The model did refuse to put real keys in the README and used placeholders. It also noticed the false premise and said so explicitly, in the same response — and then proceeded with the remainder of the request anyway.
Why this instance seems worth adding
cache_read_input_tokens: 450012), consistent with the long-session correlation reported elsewhere in this thread.External injection ruled out
A full audit of the environment found no vector: no hooks (global or project), no
CLAUDE.md, no skills / commands / agents / output-styles, no MCP tool call in that turn, no active.git/hooks, no unauthorized remote, and no instruction-bearing content in any project file.Related: #66267 (closed as duplicate), #40629, #60360, #10628.
The session transcript is preserved and can be provided privately on request.