[BUG] Self-contamination: literal tool-call XML in loaded context becomes a malformed-call template (relates #49747 / #63870)

Open 💬 4 comments Opened Jun 23, 2026 by mupozg823
Note on notation: throughout this issue I write tool-call tokens with look-alike unicode brackets (‹invoke›) instead of real angle brackets. That is deliberate — writing the literal token is itself the trigger described below (it would contaminate the very tooling reading this). Read ‹...› as the real ASCII angle-bracket form.

Summary

Self-emitted raw ‹invoke› corruption (#63870) and the XML/JSON tool-call format mixing on longer payloads (#49747) share a concrete, reproducible trigger I haven't seen documented: when the loaded context contains tool-call XML tokens as literal text — in CLAUDE.md, memory/notes files, skill docs, or earlier assistant output — the model starts treating that shape as a valid tool-call template and emits subsequent tool calls as raw text. The harness then rejects them as "malformed," or in the user-input variant crashes the CLI.

I call this self-contamination: documenting or explaining the bug requires writing the very tokens that cause it, so the explanation becomes the trigger. Once one malformed call lands as text in the transcript, it becomes the template for the next one — a loop that worsens the longer the session runs (matching "longer payloads" in #49747).

Why this is a distinct angle (not a pure duplicate)

  • #49747 identifies what happens (XML↔JSON mixing) and correlates it with payload length.
  • #63870 reports the symptom (raw ‹invoke› text instead of execution).
  • **This report identifies the mechanism behind the length correlation:** literal tool-call tokens present in always-on context (rule files, memory, docs) act as few-shot "templates" the model imitates. Length matters because longer context = higher chance such tokens are present and more imitation pressure.

Reproduction (observed, not synthetic)

  1. Long-running Opus session with project rule/memory files loaded into context.
  2. One or more of those files contains literal ‹invoke› / ‹parameter› / ‹function_calls› tokens as documentation — e.g. a note about this very bug.
  3. After enough turns, tool calls intermittently emit as raw text → "Your tool call was malformed and could not be parsed." Re-issuing immediately with correct tokens succeeds, confirming environment and harness are healthy — it's output-discipline contamination, not a config fault.
  4. Frequency rises with short follow-up prompts (less room for format self-correction) and with the number of literal XML tokens in context.

Mitigations that worked locally (deterministic, user-side)

These don't fix the model behavior but materially reduce frequency:

  1. Escape literal tool-call tokens in all always-on context files — write them as HTML entities or look-alike unicode instead of real angle brackets, so they're no longer a verbatim token sequence the model can imitate.
  2. A deterministic linter pass over context/memory files that flags any raw angle-bracket invoke / parameter / function_calls / tool_use / tool_result sequence, keeping the count at zero. (I added one to my own memory-diagnostic tool; it caught a re-contamination I introduced while writing the fix note itself.)
  3. Sanitize injected recall text — any hook injecting memory/snippets into the prompt should redact tool-call XML before injection.
  4. Starting a fresh session resets the contaminated context and the symptom disappears.

Suggestions for a real fix

  • Treat literal ‹invoke›-shaped text in the input context as inert documentation, never as a tool-call template — a clear separation between the "tool-call channel" and "text that mentions tool calls."
  • Harden the parser so a malformed/text tool call is recovered from in-session (re-prompt for correct format) rather than persisting as a contaminating template.

Environment

  • Claude Code CLI (current 2.1.x line), Opus 4.x [1m].
  • macOS.

Related: #49747 (root: XML/JSON mixing on long payloads), #63870 / #62407 (raw ‹invoke› symptom).

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗