Leaked tool call as text + stop_reason: end_turn permanently stalls a --print --resume session (and recurs on resume)

Resolved 💬 2 comments Opened Jun 27, 2026 by zijiess Closed Jul 1, 2026

Environment

  • Claude Code 2.1.195 (also observed on 2.1.191)
  • Linux, headless usage via claude --print --resume <session-id> (programmatic / agent harness, not interactive TUI)
  • Model: Claude Opus class

Summary

In long-running --print --resume sessions, the model occasionally emits a tool call as plain text inside an assistant text block (literal <invoke name="...">...</invoke>) instead of a structured tool_use content block. When this happens and the API closes the turn with stop_reason: "end_turn" (rather than tool_use), Claude Code does not execute the textual <invoke> and there is no pending tool to drive the next turn. The session silently stalls — the process stays alive but never makes progress.

Worse: because the leaked turn is appended to the session .jsonl, every subsequent --resume replays it as part of the context. The leaked pattern then acts as a few-shot example and the model reproduces it, so the failure recurs immediately on the next Continue even after the stuck turn is manually removed.

Why this is distinct from the existing malformed-tool-call retry

Claude Code already has a recovery path: when a tool call is malformed it injects Your tool call was malformed and could not be parsed. Please retry. and the model retries with a real tool_use block. We observed this triggering correctly on a leaked turn whose stop_reason was tool_use.

The gap is the stop_reason: end_turn (and stop_sequence) path: there the turn is treated as a normal final answer, the textual <invoke> is never recognized as an attempted tool call, no retry is injected, and the session ends with nothing pending → permanent stall.

Repro (minimal, sanitized)

A session .jsonl ends with an assistant record like this (business content removed; structure preserved):

// user
{"type":"user","message":{"role":"user","content":"Continue from where you left off."}}
// assistant — THE BUG: tool call leaked as text, closed with end_turn
{"type":"assistant","message":{
  "stop_reason":"end_turn",
  "content":[{"type":"text","text":"<some normal prose>\n\ncall\n<invoke name=\"Bash\">\n<parameter name=\"command\">echo hi</parameter>\n</invoke>"}]
}}

Key signature of the failing record:

  • type: assistant
  • stop_reason ∈ {end_turn, stop_sequence}
  • no tool_use content block
  • a text block containing <invoke name=...> followed (within a few hundred chars) by <parameter name=...> or </invoke>

Resuming this session and sending Continue produces another assistant turn with the same leaked-text shape, confirming the few-shot recurrence.

Observed sequence in our case

  1. Long agent task over --print --resume.
  2. One assistant turn leaked a tool call as text with stop_reason: tool_use → malformed-retry fired → model recovered with a real tool_use. (recovery worked here)
  3. A later turn leaked the same way but with stop_reason: end_turnno retry, permanent stall.
  4. Removing only the last leaked turn and resuming → model immediately re-leaked (the earlier tool_use-path leak text was still in the transcript acting as a few-shot seed). Only after removing all leaked text from the transcript did resume proceed cleanly.

Suggested fix (in Claude Code, root cause)

When an assistant turn closes with end_turn/stop_sequence and has no tool_use block but a text block contains a structurally-complete <invoke name=...> (followed by <parameter name=> or </invoke> within a small window), treat it the same as a malformed tool call: inject the malformed-retry nudge and re-prompt, rather than finishing the turn. The structural-completeness check avoids false positives where the model is merely discussing <invoke> syntax in prose.

(We worked around it externally by sanitizing the transcript — trimming the leaked <invoke...> tail off every affected text block across the whole file — but the root fix belongs in the turn-finalization path, and a transcript written by a stalled session won't self-heal without it.)

Impact

  • Headless / agent / CI usages of --print --resume can stall silently with no error surfaced.
  • The recurrence-on-resume makes it look like a hard failure of that session until the transcript is cleaned.

Happy to provide more detail or a fuller sanitized transcript snippet if useful.

View original on GitHub ↗

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