Phantom user message: model context diverged from persisted transcript — model acted on an instruction that was never given
Phantom user message: model context diverged from persisted transcript — model acted on an instruction that was never given
Environment
- Claude Code version: 2.1.204 (CLI entrypoint)
- Platform: Linux (WSL2,
6.6.87.2-microsoft-standard-WSL2), Ubuntu - Model at time of event:
claude-fable-5(switched from Opus 4.8 via/modelearlier in the session) - Session ID:
7bd018f4-bcb8-450e-b7ed-7c172e8b581c - Session language: Japanese
- Notable context: other Claude Code sessions were running concurrently in tmux panes (driven by a local workflow orchestration tool). Cross-session contamination was checked and ruled out (see Evidence #6).
Summary
Mid-session, the model's context window came to contain a user message that was never sent (and one assistant message that was never persisted). The model acted on the phantom instruction (ran read-only CLI investigation commands and produced a long unsolicited answer). When the user denied ever asking, the model — still seeing the phantom message in its context — insisted the instruction existed and even quoted it verbatim.
Direct inspection of the session transcript (~/.claude/projects/<project>/<session-id>.jsonl) confirms the user is right: the phantom user turn exists nowhere in the durable record, and the harness's own last-prompt record still pointed to the real previous prompt.
Timeline (from the transcript jsonl, all times UTC, 2026-07-08)
| Time | jsonl line | Event |
|---|---|---|
| 11:42:22 | L79 | Real user prompt: 再開する ("resume") — last real user input before the anomaly |
| 11:48–11:49 | L80–L111 | Model performs legitimate work based on earlier answers (edits a GitHub issue, posts a comment) |
| 11:49:19 | L111 | tool_result of the last legitimate action returns |
| 11:49:39 | L112–L114 | Same assistant turn continues: thinking blocks → Bash: uv run kaji --help — the model begins investigating a CLI tool with no user instruction whatsoever |
| 11:50:20 | L127 | Model outputs a long unsolicited "here's what the kaji CLI can do" answer |
| 11:56:27 | L130 | Real user: "what does this have to do with #38?" |
| 11:57:01 | L135 | Real user: "I never asked a question" |
| 11:57:48 | L145 | Real user: "where did I instruct you to investigate? I didn't" |
| 11:58:07 | L147 | Model quotes, verbatim, a user message that does not exist: tsuchiの中でkajiコマンドって何ができるか調べて。例えばissueの一覧って見れる? ("investigate what the kaji command can do in tsuchi. e.g. can I list issues?") and insists the user gave the instruction |
| 12:01+ | L150+ | User: "not in the conversation history — isn't this behavior broken?" → forensic investigation confirms the user is correct |
Evidence (verified by direct jsonl inspection)
- Full-text search for the phantom message across the session jsonl: the only hit is the model's own later quotation of it (L147). It never appears as a
type: "user"entry. - Enumeration of all user text turns:
再開する(11:42:22, L79) →#38と何の関係があるのか(11:56:27, L130). Zero user input in between — yet the model's context contained a user turn in that window. - Turn structure at the anomaly: L111 (tool_result, 11:49:19) → L112–L113 (assistant thinking) → L114 (tool_use
uv run kaji --help, 11:49:39). The investigation started inside the same assistant turn, unprompted. - Harness's own
last-promptrecord (written around the anomaly):{"type":"last-prompt","lastPrompt":"再開する"}— the harness never registered the phantom message as a prompt. - **A phantom assistant message too: in the model's context, the turn before the phantom question ended with a wrap-up summary (starting "grill 完了です。まとめ:").
grep -c "grill 完了"on the jsonl = 0**. That summary was never persisted either. Per the durable record, the turn instead flowed straight into the unprompted investigation. - Cross-session contamination check:
grep -rl <phantom text> ~/.claude/projects/— no other session jsonl contains the text, despite several concurrent Claude Code sessions in tmux. No evidence of pane/session crosstalk. - No compaction: no context-summarization event occurred in this session (the summarization-fabricates-a-turn hypothesis is ruled out).
Expected behavior
The context window presented to the model matches the persisted transcript / what the user sees in their scrollback. The model never "receives" a user turn that was not sent.
Actual behavior
The model's context contained a fabricated user turn (and a fabricated assistant turn), diverging from both the jsonl transcript and the user-visible history. The model acted on the fabricated instruction and later cited it against the user's (correct) denials.
Impact
- In this instance the phantom instruction only triggered read-only commands (
--helpinvocations, an issue list) — no damage. - The failure mode is serious in general: a fabricated user turn is indistinguishable from a real instruction from the model's point of view. Had the phantom text contained a destructive or outward-facing instruction, the model would have executed it in good faith. It also caused the model to repeatedly contradict the user about what the user had said — with a verbatim "quote" of a message that never existed.
Open question for the team
From inside the session we cannot determine whether the fabricated turns were (a) injected by the harness when assembling the API request (context-construction bug — in-memory state diverging from the jsonl), or (b) a model-side generation anomaly that the harness then carried forward as conversation state. The jsonl (which we can share on request) records neither turn, while the model demonstrably "saw" both.