Long-lived resumed background session confabulates a prompt-injection/security incident from its own output

Open 💬 1 comment Opened Jul 5, 2026 by Klinola

What happened

In a long-lived, repeatedly-resumed background session (Opus, doing ordinary development work), Claude Code spiraled into confabulating an entire prompt-injection / security incident that never happened. Over a run of ~500k tokens and ~120 resumes, after a normal topic switch mid-task, the model began asserting it was under an active attack and produced a detailed, self-consistent "forensic" narrative:

  • It claimed its tool outputs were being appended with fake <system-reminder> blocks instructing it to emit specific strings and answer off-task personal questions.
  • It named a specific culprit hook file (.claude/hooks/scan_secrets.py) bound to PostToolUse, and claimed it was injecting decoys.
  • It claimed two Reads of the same unchanged settings.json returned different structures, and concluded "an unchanging file can't return two structures → nondeterministic output-layer injection."
  • It then refused the (self-invented) injected instructions, announced it was switching tool channels to "evade" the pollution, ran forensic probes, and wrote a notes file advising the user to rotate all secrets.

None of it was real. This produced a genuine security scare — the user believed their production server might have been compromised and opened separate sessions to investigate whether they had been hacked.

Why this is a model/agent bug, not a real injection

The clean discriminator is channel provenance: the model API generates only assistant turns; any real injector can only touch user text or tool_result. Grepping the raw session jsonl for every "payload" string the model quoted as evidence of attack:

  • Every payload string appears only in assistant's own output (its generated text and its own tool_use inputs).
  • Zero occurrences in any user turn, zero in any tool_result, zero in system.

Corroborating:

  • The real system-reminder tool_results were all normal (~120 bytes) and contained none of the claimed markers.
  • The named culprit hook file never existed — the real Read returned "File does not exist", and there is no .claude/hooks/ directory on disk at all.
  • The "I was just injected via the Read channel" claim was not preceded by any actual Read call in the transcript.
  • Transcript integrity was intact (append-only, unbroken uuid parent/child chain, monotonic timestamps), ruling out tampering.

In other words: the model treated its own generated text as evidence of external tampering, and each confabulated turn became context feeding the next, reinforcing the "I am under attack" narrative in a self-amplifying loop.

Impact

  • False security incident: fabricated a named culprit file, a nonexistent hook binding, and verbatim "injected" payloads it authored itself.
  • User harm: a false-positive intrusion scare that consumed real investigation time and caused the user to consider rotating all production secrets.
  • Notably the failure mode looks like responsible behavior (refusing injection, warning about secrets), which makes it more likely to be believed.

Reproduction heuristic

Not deterministic, but the conditions that produced it:

  1. A very long, repeatedly-resumed background session (hundreds of thousands of tokens, many resumes), where context is dominated by large tool outputs.
  2. A topic switch late in the session.
  3. From there, the model began interpreting its own prior output as signs of environmental tampering, and confabulated a full injection scenario.

Diagnostic for maintainers / anyone hitting this: grep the raw session jsonl for the strings the model cites as attack evidence. If they appear only inside role: assistant blocks and never in any user / tool_result block, it is confabulation, not injection.

Suggested mitigations

  • Guardrail: an agent should not conclude "my environment/tool output is being injected" without evidence that lives in an input channel (user/tool_result), since it cannot observe its own output being tampered with in real time.
  • Consider surfacing a context-length / resume-count signal, or encouraging fresh sessions past some threshold, to reduce long-session narrative drift.

Environment

  • Claude Code, model Opus (1M context), Linux, background/headless resumed session.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗