Tool calls intermittently leak as literal XML text (lone 'count' line + <invoke...>) at high context; retained bad exemplar causes 34% chained re-leaks

Open 💬 1 comment Opened Jul 13, 2026 by flyzhenghao

Summary

Tool calls are occasionally emitted as literal XML text in the assistant text channel instead of real tool_use blocks. The harness then either reports "Your tool call was malformed and could not be parsed. Please retry." (when stop_reason=tool_use arrives with no tool_use block) or silently ends the turn with the raw XML shown to the user (stop_reason=end_turn).

Measured across our transcript archive: 33 sessions / 222 leaked blocks over multiple projects — a chronic fleet-wide issue, not a one-off.

Environment

  • Claude Code 2.1.207 (also observed on earlier 2.1.x)
  • macOS (darwin 25.5.0)
  • Model: claude-opus-4-8 (observed on other models previously as well)
  • Long-context sessions (1M-context beta in use)

Stable failure signature

Every leaked block has the identical shape — a lone line count followed by the call body in training-format XML, with no namespace prefix and no closing wrapper:

...normal prose...

count
<invoke name="Bash">
<parameter name="command">git status --short</parameter>
<parameter name="description">Check working tree status</parameter>
</invoke>

The message containing this has no tool_use block at all; stop_reason is either tool_use (harness reports malformed) or end_turn (turn just ends).

A param-less variant also occurs (<invoke name="ExitPlanMode">\n</invoke> followed by resumed prose like "Wait — I need to use the correct tool call format").

Why we believe this is model-side token-stream, not harness/SSE

The leaked payload is training-format XML (<invoke name=...> / <parameter name=...>), not the API's JSON input_json_delta form. If SSE reassembly or client parsing were at fault, the leaked text would be partial JSON. It looks like the tool-call opening special token fails to emit (leaving the count residue), so the entire call body is sampled as plain text.

Two measured aggravators

  1. High context is a precondition. Across 6 affected sessions in one project, every first leak occurred at ctx ≥ 131k tokens (131k / 184k / 205k / 225k / 247k / 518k, median ~215k). Zero leaks below that. Transcript file size is uncorrelated (one session leaked at a 0.5 MB transcript).
  2. Self-imitation contamination. Leak rate before a session's first leak: 0/466 tool calls. From the first leak onward: 21/61 (34%). Once one leaked XML block sits in context as an assistant message, subsequent calls imitate it — we observed a retry reproduce the previous leak byte-identical (same count line, same parameter order). The harness's "please retry" message leaves the bad exemplar in context, so retries frequently re-leak.

Request IDs (for your tracing)

req_011CcyLGY2cJ2W9VqBqRhKCv   (leak, stop_reason=tool_use, ctx≈225k)
req_011CcyLHoTLBwobWUfG98mdF   (byte-identical re-leak on retry, ctx≈226k)
req_011CcyLQcWTM2X92hn2RPp3e   (leak on a trivial `git status` call, ctx≈233k)
req_011CcyLQzCBuFaBXBAAQkNHb   (leak, stop_reason=end_turn, ctx≈233k)
req_011CcyNBv619DuD1k8g3ehC8   (first leak of a later session, ctx≈131k)
req_011CcyNEELR67sVyaUkRXPtk   (chained leak on trivial call, ctx≈133k)

Suggested fixes

  1. Model/API layer: investigate the failed emission of the tool-call opening token at high context (the count residue should make it findable in sampling traces).
  2. Harness mitigation (high leverage): when a completion arrives with stop_reason=tool_use but no tool_use block (or when the text channel matches the leak signature), strip or rewrite the leaked text before the retry request instead of leaving it in context verbatim. Our measurements show the retained bad exemplar is what turns a one-off fault into a 34% chained failure mode.
  3. Optionally detect the signature (\ncount\n<invoke name=") on end_turn completions too — currently those end the turn silently and the user has to notice manually.

Local workaround we deployed (works, but shouldn't be necessary)

  • A Stop hook that detects the signature on the final assistant message and returns {"decision":"block"} with corrective guidance ("re-issue for real; do NOT imitate the literal XML in history") — this broke the chaining in practice.
  • A transcript cleaner that replaces leaked spans with an inert placeholder before resuming affected sessions.

Happy to provide sanitized transcript excerpts on request.

View original on GitHub ↗

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