Intermittent tool-call serialization failure: tool calls rendered as literal prose (76 instances on v2.1.199; not yet reproduced on v2.1.217)

Status Open
Reported on v2.1.92
Maintainer reply None cached
Activity 0 comments · opened Jul 27, 2026

Environment

  • Deployment shape: a single long-running Claude Code CLI session per container

lifetime, invoked as claude --dangerously-skip-permissions --model claude-opus-4-8
--channels <2 chat-platform plugins> --append-system-prompt-file <file>
, restarted
periodically (auth/error recovery, scheduled context-hygiene recycle, or container
rebuild) — so "one session" below means one CLI process lifetime, one
~/.claude/projects/<cwd-slug>/<sessionId>.jsonl transcript file.

  • Model: claude-opus-4-8 (1M-context beta), set via --model, unrelated to the CLI

version under discussion — included here only in case the bugs below are
model/beta-header-interaction-specific rather than pure-CLI bugs.

  • Two chat-platform integration MCP plugins are loaded (a Discord-shaped one and a

Telegram-shaped one), plus several first-party MCP servers (browser automation via
Playwright, a graph-database server, a Google-Workspace server). The bugs below are
plugin/MCP-agnostic in principle; Bug 2 was only actually observed in sessions using
the browser-automation (Playwright) MCP server heavily, and one of Bug 3's three
confirmed instances froze immediately after a Playwright tool result — the other two
froze on an idle turn boundary with no tool call involved, so Bug 3 does not appear
to be Playwright-specific the way Bug 2 might be.

  • Bug 3's evidence (below) additionally draws on an internal watchdog process (our own

infrastructure, not part of the CLI) that polls session liveness independently of the
CLI and keeps its own status/log stream — included here only as the source of the
wall-clock probe/restart timestamps that corroborate the transcript-level gaps.

  • CLI versions in play: 2.1.92 (last known-good), 2.1.199 (the version an

unpinned @latest silently resolved to on 2026-07-02, and the version Bugs 1–2 were
originally reported against), and 2.1.217, deliberately pinned in the deployment's
Dockerfile on 2026-07-22 (it was the actual npm latest/next dist-tag at pin time;
npm's stable dist-tag has since moved independently of what's actually deployed
here) and running in this deployment since ~2026-07-24.

  • Update (2026-07-26) — 2.1.217 status for all three bugs, based on the 11 session

files run on 2.1.217 to date (2026-07-24 → present):

  • Bug 1 (auto-compaction not firing): unchanged. grep -c '"isCompactSummary":true'

across all 11 files is 0, same as on 2.1.199.

  • Bug 2 (pseudo-invoke artifact): has not recurred on 2.1.217 so far — 0

instances across the 11 files, versus 76 across 4-of-14 sessions on 2.1.199. The
2.1.217 sample is smaller and we have not confirmed it contains sessions as
Playwright-heavy as the worst 2.1.199 ones (the correlation noted under Bug 2), so
we are not claiming this is fixed — only that it hasn't reproduced yet in the
window we've observed.

  • Bug 3 (this addendum, mid-session freeze): confirmed present on 2.1.217 —

two new instances, 2026-07-25 and 2026-07-26, both ending in a watchdog-triggered
restart. The instance that originally surfaced this bug (2026-07-18) was on 2.1.199
and self-recovered without a restart; see the Bug 3 section for all three.

  • No npm changelog ships with the package (checked: no CHANGELOG.md in the

installed package tree), so the regression below is bisectable only to "somewhere
between 2.1.92 and 2.1.199" — not to a specific intermediate version. Narrowing that
would require installing and replaying against intermediate versions, which we have
not done (this report is from read-only inspection of the two versions actually
deployed).

---

Bug 2 — intermittent tool-call serialization failure (<invoke name="mcp__..."> rendered as prose)

Symptom

On an intermittent basis, an assistant turn that intends to call a tool instead emits
the tool-call's XML-ish internal representation as literal, visible prose text —
<invoke name="mcp__...">...</invoke> (sometimes with stray leading tokens before it,
observed once as the literal token court followed by a newline, i.e. the artifact
shape court\n<invoke name="mcp__...">, and sometimes wrapped in markdown code
fences) — instead of emitting an actual structured tool_use block. When this
happens, nothing executes: not the intended tool, not anything else. From the
application's perspective it looks exactly like the assistant "decided" to just talk
about calling a tool instead of calling it — a completely silent failure with no error
surfaced anywhere in the transcript or the CLI's own output.

Evidence

  • Frequency/spread: 76 instances of the artifact across 4 of the 14 sessions run on

2.1.199 (~29% of sessions affected — never observed in the 2.1.92 sessions we have
transcripts for, though our sample of 2.1.92 sessions is smaller, 2 vs. 14).

  • Trigger correlation: sessions containing the artifact correlate with windows of

heavy use of our browser-automation (Playwright) MCP server — long tool-call
chains, larger tool-result payloads (screenshots, DOM snapshots) than our other MCP
servers produce. We have not established causation, only that every affected
session had a Playwright-heavy window shortly before or during the first
occurrence in that session.

  • Propagation across tool types: once the artifact appears in a session, it is not

confined to whatever tool triggered it first — in the sessions where it occurred,
it recurred later in the same session against unrelated tools, including our chat
platform reply tools. This is the operationally serious part: a turn that was
supposed to deliver a reply to a real end user instead rendered the reply tool's
invocation as unexecuted prose, so the user received nothing, with no error and
nothing to distinguish it from a normal completed turn by inspecting the CLI's own
output. This was the direct, confirmed cause of at least one real incident of
undelivered end-user messages in this deployment (2026-07-13).

  • We built and ship our own defensive detector for this exact shape, since nothing in

the CLI surfaces it as an error. Our Stop-hook code (scripts/reply-guard.js in
this repo) has carried this pattern since our v2.3 hardening pass:
``js
// v2.3 rule 1 (header note 11): a pseudo tool-invoke emitted as plain text.
const INVOKE_PATTERN = /<invoke\s+name="(mcp__[^"]*)"/;
`
with the accompanying design note (unchanged from our code, quoted verbatim as
supporting evidence of the shape and our operational response to it):
> Malformed-invoke detector: if the trailing text of the LAST assistant turn —
> specifically, whatever text comes after that turn's own final genuine
tool_use
> block, or the whole turn's text if it made no real
tool_use at all — contains a
> literal
<invoke name="mcp__... this blocks unconditionally... this is a plain
> unanchored substring search, so stray leading tokens (
"court\n<invoke..."`) or
> code fences around the block never hide it.
In other words: this bug was frequent and costly enough in our production use that
we had to add a purpose-built guard against it at the application layer, because the
CLI gives no signal of its own that a turn silently failed to invoke a tool it
intended to invoke.

Impact

Silent, undetectable-without-external-tooling failure to execute an intended tool
call. In an interactive coding session this might just look like the assistant "said"
it would do something and didn't; in an automated/always-on deployment where a tool
call is the only channel by which output reaches an end user (e.g., a chat-platform
reply tool), it is a direct message-delivery failure indistinguishable from a normal
successful turn by inspecting the CLI's own transcript/output — it required an
independent, purpose-built detector on our side to even notice, and only after it had
already caused a real incident.

Repro difficulty

Intermittent, not reliably reproducible on demand. Correlates with, but is not
conclusively caused by, heavy MCP tool use (specifically browser-automation-style
tool chains with larger result payloads) within a session. We have not been able to
isolate a minimal repro; all 76 observed instances are from organic production use,
not a synthetic test.

---

What we can provide on request

  • The exact grep/strings commands used above, rerunnable against any 2.1.x

build (they target env var names and function shape, not line numbers, so should
survive minor version bumps).

  • Full per-session transcript metadata (file name/sessionId, CLI version, first/last

timestamp, isCompactSummary count) for all 16 sessions referenced above — happy to
share as a table with the sessionId/file-name column removed if that's a concern,
or in full to a private channel upstream can verify us on.

  • Redacted transcript excerpts around each of the 76 Bug 2 artifact occurrences

(assistant-turn text only, chat-platform message content stripped) — these would
need to be prepared per-occurrence rather than bulk-dumped, since removing all
message content requires per-file manual redaction; we have not pre-generated this
and would do so if upstream wants to inspect the exact artifact text/context rather
than just the shape quoted above.

  • A copy of scripts/reply-guard.js's detector logic in full (it's already

application code in this public-facing sense — no secrets in it) if a concrete
implementation of the workaround is useful context for triage.

  • Sanitized row-structure excerpts around each of the three Bug 3 freeze boundaries

(the handful of transcript rows immediately before and after each gap, row
type/timestamps/field names preserved, all message content and channel
identifiers stripped) — these are already prepared in a shareable form, unlike Bug
2's 76 per-occurrence excerpts which need per-file manual redaction; the independent
watchdog log lines (probe/restart timestamps) corroborating each gap are equally
available.

What we cannot provide

  • No root-cause visibility into Bug 3 beyond "the process is alive and idle-shaped

when checked" — we have not attached a debugger/profiler to a live-frozen process
(in practice, by the time a freeze is noticed the watchdog has typically already
restarted the container), so we cannot say what, specifically, the loop is blocked
on.

  • We have not reproduced Bug 1 or Bug 2 on any version besides 2.1.92 and 2.1.199 (Bug

2 hasn't recurred at all on 2.1.217 yet — see Environment), so we cannot say whether
Bug 2 is already fixed upstream; Bug 1 remains unchanged on every version we've run
it on (2.1.199 and 2.1.217 both show zero compaction events).

  • No tighter bisection range than "somewhere between 2.1.92 and 2.1.199" for Bug 1 —

no intermediate versions were installed/tested.

  • No minimal/synthetic repro for Bug 2 or Bug 3 — only organic production occurrences

for both.

View original on GitHub ↗