[BUG] Opus 4.7 mixes legacy XML tool-use format into JSON tool calls on longer payloads

Open 💬 33 comments Opened Apr 17, 2026 by nextdev-labs

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

When calling a custom MCP tool with multiple required parameters (including an array field and a few string fields) on Opus 4.7, the model intermittently emits legacy Anthropic XML tool-use syntax inside a JSON-format tool call. Specifically, a string-valued argument ends with a literal </parameter_name> closing tag inside the JSON string, and subsequent parameters appear as <parameter name="X">value</parameter> XML blocks instead of JSON key-value pairs.

Because Claude Code parses tool calls as strict JSON, the XML-declared parameters never reach the parsed arguments object — required fields come through as undefined, schema validation fails, and the model is forced to retry. In practice this produces 4-6 consecutive tool-call attempts per logical turn, each progressively shorter, burning ~1k tokens per failed attempt before one finally succeeds.

The trigger correlates strongly with total output length of the tool call. Short arguments (one-phrase fields) succeed on first attempt. Paragraph-length arguments almost always trigger the XML leak. The model cannot self-correct via prompt instructions — I tested explicit system-prompt rules forbidding XML tags in tool-call strings; the bug still fires. This is a decoder-level format switch, not something overridable from the prompt layer.

This is a regression from Opus 4.6, which handled the same MCP tool with verbose arguments reliably.

What Should Happen?

The model should emit one tool call per turn with all arguments as JSON key-value pairs, no XML tags embedded in any string value. Schema validation passes on first attempt, no retries, no context-window waste.

Error Messages/Logs

MCP error -32602: Input validation error: Invalid arguments for tool <tool_name>: [
  {
    "expected": "array",
    "code": "invalid_type",
    "path": [
      "files_modified"
    ],
    "message": "Invalid input: expected array, received undefined"
  }
]


Example of the malformed tool-call output that causes this (note the `</summary_of_changes>` + `<parameter name="files_modified">` tokens *inside the JSON string* for `summary_of_changes`):


tool_name(
  user_intent: "...",
  ai_reasoning: "...",
  summary_of_changes: "...several sentences of content.</summary_of_changes>\n<parameter name=\"files_modified\">[\"item1\",\"item2\"]",
  status: "completed",
  model_name: "claude-opus-4-7[1m]",
  tags: ["tag-a"]
)


The JSON string for `summary_of_changes` ends with `</summary_of_changes>` followed by `<parameter name="files_modified">...` — the decoder flipped to legacy XML tool-use mid-argument. The `files_modified` array never becomes a JSON top-level key, so the parsed arguments have `files_modified: undefined`.

Steps to Reproduce

  1. Register a custom MCP tool via the official TypeScript MCP SDK with 5-7 required parameters — at least one z.array(z.string()) array field plus several z.string() fields whose descriptions invite verbose content (e.g., "detailed explanation", "concise summary of changes"). Example schema:

``ts
server.tool(
"log_outcome",
"Log the outcome of the turn.",
{
user_intent: z.string(),
ai_reasoning: z.string(),
summary_of_changes: z.string(),
files_modified: z.array(z.string()),
status: z.enum(["completed", "in_progress", "failed"]),
tags: z.array(z.string()).optional(),
model_name: z.string().optional(),
},
async (args) => ({ content: [{ type: "text", text: "ok" }] })
);
``

  1. Connect it to Claude Code via .mcp.json over stdio. Confirm it loads.
  2. Start a Claude Code session on Opus 4.7.
  3. Prompt the model to perform any non-trivial work and then call log_outcome with a thorough summary (ask it to describe what it did in 2-3 sentences per field).
  4. Observe the tool calls in the Claude Code UI. When any string argument crosses roughly 1-2 sentences of content, the call is rejected with the schema error above. The model retries 3-5 times before one attempt stays short enough to escape the decoder flip and succeeds.

Contrast: with tiny arguments (single short phrases), the call succeeds on the first attempt, no retries.

Claude Model

Opus

Is this a regression?

Yes, this worked in a previous version

Last Working Version

Claude Opus 4.6

Claude Code Version

2.1.112 (Claude Code)

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

_No response_

View original on GitHub ↗

33 Comments

Omegastick · 3 months ago

I have also noticed this as an issue not just in Claude Code but with Opus 4.7 via the API.

valentabc · 1 month ago

Confirming this from #60584 (closing that as a duplicate of this one — same root cause: legacy XML tool-use markup leaking into the output).

Adding two observations that may help narrow it down:

  1. It also leaks into the visible message, not just into tool-call args. In my sessions the assistant emitted the literal text court followed by a full <invoke name="Edit">...</invoke> block as plain markdown in the chat. No tool ran, no error was returned — completely silent. So the failure mode isn't only "XML inside a JSON arg" but also "the whole tool call renders as text".
  1. The trigger correlates with explanatory prose before the call, not only with total payload length. Tool calls emitted as the first element of the reply (little/no preceding prose) went through reliably. The same Edit, preceded by a sentence like "Now I'll edit X:", frequently leaked. Retrying that exact edit with the prose removed succeeded.

Reliable workaround: emit one tool call per message, as the first element of the reply, and move all explanation to after the tool result instead of before the call.

Env: Claude Code 2.1.143, Opus, VS Code native extension, Anthropic API, long session (50+ sequential Edit/Read calls on a Laravel + React repo). Malformed-call rate rose as the session grew.

akige · 1 month ago

Additional data point: empty thinking block + missing tool_use (5/20 onset)

I'm seeing a related but distinct failure mode on Opus 4.7 that started around May 20, 2026:

Symptom: The API returns stop_reason: "tool_use" but the content array contains only an empty thinking block (0 chars, with valid signature) — the tool_use block is completely absent. The SDK detects the mismatch, retries, and if the retry also fails, surfaces "The model's tool call could not be parsed (retry also failed)".

Key evidence from JSONL session analysis:

  • Before/after: 63+ Opus 4.7 sessions from May 15–19 had zero malformed tool calls across ~15,000 assistant turns. Starting May 20, 5 out of 11 sessions exhibit the issue (up to 8.4% of turns in a single session).
  • Opus 4.6: All sessions on 4.6 have 0 malformed responses. Switching to 4.6 completely eliminates the issue.
  • The response is finalized: The malformed entries have full API response structure (id, usage, diagnostics, thinking signature). output_tokens is 1,600–1,700 (non-trivial), meaning the model did generate output — but the tool_use block was dropped from the response while stop_reason still says tool_use.
  • Overall rate: ~0.26% of API calls (33 out of 12,690 assistant turns), but clustered in certain sessions.

Environment: Claude Code 2.1.146, macOS, Claude Agent SDK 0.2.141. No proxy. Same CLAUDE.md/config throughout — only the model behavior changed around May 20.

This may be a different manifestation of the same underlying issue (model output corruption on Opus 4.7), but the failure mode is distinct from the XML-leak pattern described in the OP — here the tool_use block is silently absent rather than malformed XML.

HossyKoki · 1 month ago

Still occurring on 2026-05-22 (the day after #61133 was closed as completed), so this doesn't appear resolved yet.

Environment: Claude Code on Windows 11 / Git Bash, Opus 4.7 (1M context), usage_speed standard (no /fast).

Repro matches the "longer payloads" correlation exactly: in a single session, large multi-line Edit/Write tool calls — especially several-hundred-line non-English (Japanese) bodies with nested quotes/code-fences — repeatedly triggered Your tool call was malformed and could not be parsed. Please retry., while short tool calls in the same session went through on the first attempt. Most retries eventually succeeded; a few ended in The model's tool call could not be parsed (retry also failed).

Splitting large edits into smaller chunks noticeably reduced the frequency but did not eliminate it — consistent with this being a decoder-level format switch rather than something promptable.

SomeoneKong · 1 month ago

Adding a data point that matches @akige's empty-thinking-block + missing tool_use branch (not the XML-leak pattern in the OP). Captured from local session JSONL, so I can confirm the exact response structure.

Environment

  • Claude Code 2.1.149 on Windows 11
  • Opus 4.7 (1M context), standard speed (no /fast)
  • Claude Max subscription — official Anthropic backend, no API key / no proxy / no third-party or custom model provider
  • Long single session with frequent Edit/Write of CJK long-form text with nested quotes/code-fences

Frequency & impact

  • The user-visible terminal failure (retry also failed) has surfaced intermittently — about 5 times recently.
  • Once a turn lands in this state, manually telling the agent to "continue" usually does NOT recover it — the next turn tends to reproduce the same empty-block shape. Editing/re-sending the request or switching model is what actually unblocks it.

Failure mode (confirmed from JSONL)
The malformed turn recurred repeatedly within one ~20-minute window; the client auto-retry recovered most, but some reached the terminal retry also failed. Minimal trace (N=1 retry):

  1. assistant (malformed) — model returned:
  • stop_reason: "tool_use"
  • usage.output_tokens: 570 (non-zero)
  • content: [ { "type": "thinking", "len": 0, "has_signature": true } ]no tool_use block at all
  1. client-injected user"Your tool call was malformed and could not be parsed. Please retry."
  2. assistant (retry, still malformed) — same shape: stop_reason: "tool_use", output_tokens: 795, only an empty thinking(0) block
  3. synthetic terminal messagemodel: "<synthetic>", output_tokens: 0, stop_reason: "stop_sequence", text = "The model's tool call could not be parsed (retry also failed)."

Three details not yet noted in this thread

  1. output_tokens is non-zero (570/795) on the malformed turns — the model was billed for output, so the loss happens in response serialization, not because the model produced nothing.
  2. The empty thinking block carries a valid signature despite being 0-length ("signed empty thought").
  3. The terminal error is model: "<synthetic>" with 0 tokens — i.e. it's client-synthesized, not a model reply. So "retry also failed" means the retry hit the same empty-block shape again, not the model declaring failure itself.

Correlation (weak): large / CJK tool inputs trigger it far more than short calls in the same session; splitting into smaller edits reduces frequency but doesn't eliminate it.

This still reproduces after #61133 was closed, so it doesn't appear resolved for this branch.

nextdev-labs · 1 month ago

Incredibly irritating that this is still happening! I validated that Opus-4.6 doesn't have this problem, so it's a degredation from Opus-4.7. I have a complex MCP server that runs on every turn, and it's essentially been nuked by this issue. Any updates from Anthropic team on when this'll be fixed?

maha0525 · 1 month ago

Also reproducing on Windows 11 (Claude Code 2.1.150, Opus 4.7 1M context)
this is not macOS-only. The user-facing message is
The model's tool call could not be parsed (retry also failed). (#61133, closed
as a dup of this issue, shows the identical wording.)

Two trigger observations that may extend the "argument length" correlation in
the original report:

  1. It fires even when the tool call's own arguments are short, if the call

comes right after a large tool _output_ earlier in the session — e.g. a
git branch listing 150+ lines, or a long sub-agent/Task report. The
preceding output volume seems to matter, not just the length of the current
call's arguments.

  1. Parallel tool calls (multiple tool_use blocks in one assistant message)

trigger it at a noticeably higher rate than single calls. Switching to
one-tool-per-turn + trimming output sharply reduces the frequency.

Behavioral note: in my sessions the preceding tool (Bash/PowerShell
git commit/push, gh calls, an idf.py app-flash) executes successfully,
and then the turn is terminated by the parse failure — control returns to the
user mid-workflow and the assistant cannot self-recover. Multi-step autonomous
runs (build -> flash -> commit -> push) end up needing a manual "continue" on
nearly every step.

Environment: Windows 11 Pro (10.0.26200), Claude Code 2.1.150, Opus 4.7 (1M context).

kubu0014 · 1 month ago

I encountered this too, +1

peka2 · 1 month ago

Even after disabling 1M or switching to medium, I can't avoid the issue.

Returning to 4.6 makes it work 100% correctly.

claude --model claude-opus-4-6
XiaTiaoQAQ · 1 month ago

same here, if i use /model claude-opus-4-6[1m], can solve this problem

Astro-Han · 1 month ago

Additional data: Remote Control correlation falsified + escalation curve

Environment: Claude Code 2.1.150, macOS (darwin), CLI, Opus 4.7, standard speed, Anthropic API (Claude Max subscription)

Escalation curve

Tracked malformed tool-call frequency across all local sessions over 3 days:

| Date | Malformed tool-call count |
|------|--------------------------|
| May 24 | 2 |
| May 25 | 18 |
| May 26 | 30 |

Remote Control hypothesis — tested and falsified

Ran a full-chain analysis across 175 local sessions. Initial finding looked promising: 55 out of 58 malformed tool calls (95%) occurred in sessions with remoteControlAtStartup: true. Sessions without RC had only 3.

Turned off RC based on this. The very next session (a CJK-heavy educational session on Socrates / tacit knowledge, Opus 4.7, ~190 assistant turns) still hit Your tool call was malformed and could not be parsed followed by The model's tool call could not be parsed (retry also failed).

A Codex second-opinion confirmed the stats weren't hard enough: odds ratio ~3.33, but 95% CI includes 1 (0.94–11.82), Fisher p = 0.081. RC was a confound (most sessions simply had RC on), not a cause.

CJK + long-form content trigger

Matches @HossyKoki's and @SomeoneKong's observations. The failing session was writing structured Chinese-language content about epistemology (nested quotes, markdown headers, code blocks). Short tool calls in the same session went through fine.

Workaround

Downgraded to Opus 4.6 (1M context) as default. Zero malformed tool calls across all 4.6 sessions in local history.

tomocrafter · 1 month ago

This still happens with newly released Opus 4.8.

garyzheng0714-lang · 1 month ago

This still happens with newly released Opus 4.8.

opencaw · 1 month ago

Opus 4.8 has the same problem.

a64307410 · 1 month ago

Opus 4.8 has the same problem.

wshino · 1 month ago

Adding a controlled data point that lines up with the "4.7+ regression, worse on 4.8" read, in case the clean A/B helps narrow it down.

Environment: macOS, Claude Code CLI 2.1.156 (run via Paseo), Opus 4.8 (1M context).

Symptom matches the OP and #60584. The assistant emits a literal stray token + <invoke> markup as plain text instead of a structured tool_use, so the tool never dispatches. In my sessions the stray opener is court (vs call in #63870) — the token varies but the failure is the same:

...
court
<invoke name="Bash">
<parameter name="command">...</parameter>
</invoke>

Once it starts it cascades within the session (one session hit 22 occurrences), consistent with the few-shot-poisoning angle in #62344.

What might help narrow it down: I scanned all my local session transcripts (jsonl), so these are counts rather than impressions.

  • Across all 1229 sessions from 2026-04-19 to 05-30, only 3 sessions had malformed calls — all on 05-29/05-30, all 100% opus-4-8.
  • Zero malformed calls in the 1127 sessions before 05-28 (all non-4.8).
  • On 05-28 my machine had a separate resource problem (runaway MCP processes), but 4.8 wasn't in use yet, and there was still zero malformed output — so the environment doesn't look like the cause.
  • Holding the CLI fixed at 2.1.156 on 05-29/30: 4.7-only sessions = 14, malformed 0; sessions using 4.8 = 25, malformed 3. Same CLI, same machine, the only differing variable is the model.

So in my data the model is the sole discriminator. 4.7 happened to be clean for me across those 14 sessions, though I haven't verified the "4.6 is 100% clean" reports from this thread.

One caveat on that 4.7 number: those 14 sessions may simply not have hit the payload-length / large-prior-output trigger noted above, so I'd read it as "didn't reproduce in my sample" rather than "4.7 is immune." The model-vs-model split is within my fixed CLI / fixed machine, not a claim that argument length is irrelevant.

Happy to share the exact jsonl lines if that's useful.

yaanfpv · 1 month ago

Confirming this is NOT Opus-4.7-specific — still reproducing on Claude Code 2.1.158 with claude-opus-4-8[1m] (1M tier), Claude Code Desktop app on macOS. So it spans model versions and persists well past the April 2.1.146 reports; not a single-model quirk.

Our manifestation is the more extreme end of the same failure: instead of XML leaking into a JSON tool call, the model emits the entire tool call as literal <invoke name="…">…</invoke> text in the assistant message, so nothing executes at all (a silent no-op — the session looks like it answered, but no tool ran). This is the same symptom as the open #63870 and the closed #61221 / #60584 ("court"/<invoke> text).

Trigger is specific and points at compaction, not just payload size: a long-lived session that polls on a timer, running the same 2-3 Bash commands every turn. After it /compacts (a few times over), it starts emitting <invoke> blocks as text — often the same block duplicated. A fresh session is fine; it returns a few compactions in. Hypothesis: /compact serializes the prior tool_use blocks into the summary as literal <invoke>/<parameter> markup, and the post-compaction model pattern-completes on that in the text channel. If that's right, this is partly a harness/compaction bug, not only a model-format one — worth splitting out if you consider it distinct.

Two fixes for the compaction variant:

  1. During compaction, don't render tool_use/tool_result blocks as literal <invoke> markup in the summary — summarize semantically or strip the wire format.
  2. Guard against <invoke name=…> emitted in the text channel — re-route to a real tool call or error rather than rendering it as content.

Severity is high for autonomous/looping agents: a silent no-op that mimics a normal response.

brian3583 · 1 month ago

Same regression on Opus 4.8 (1M context), Claude Code on macOS 25.5.0.

Reproduces reliably with the built-in Edit tool when old_string/new_string
are long multibyte (Chinese) strings containing full-width punctuation
(/|+=). Short edits almost never fail; long CJK edits failed 5-6 times
in a single session.

Not MCP-specific — happens with native tools too. Confirms this is at the
model decode layer, not the MCP schema. Prompt engineering does not help.

bwangll · 1 month ago

At this point, it’s unclear how the authorities plan to resolve this issue, as it has already severely disrupted normal usage.

shuhei0866 · 1 month ago

Still reproducing on the latest build — Claude Code 2.1.160, Opus 4.8 (1M tier, session model id claude-opus-4-8[1m]), macOS 26.2 (Apple Silicon). Not fixed as of 2.1.160.

Two data points from a live session that may help triage:

1. Bash is affected, not only Edit/Read. #64108 reported Bash calls as unaffected, but I just caught the exact court-prefixed leak on a plain Bash call. The whole invocation rendered as text instead of executing — silent no-op, no tool_result:

court

  <redacted-path>/board.py apply --player gote 'P*2c'
  <one-line Japanese description of the action>

The stray opener token was court (matching the OP and @wshino), with the namespace prefix dropped.

2. The trigger correlates with multibyte text anywhere in the call, including the description. The command above is ASCII, but the call carried a Japanese description and still leaked. Combined with @brian3583's long-Chinese-edit reports (full-width punctuation /|+=), this points the root cause at multibyte / full-width tokenization in general — not Chinese-specific, and not limited to the argument body.

Consistent with other reports, dropping to Opus 4.6 (/model claude-opus-4-6[1m]) avoids it entirely.

bwangll · 1 month ago

I’ve found that /effort with high or ultracode doesn’t trigger often, but with xhigh it happens frequently.

kaelzhang · 1 month ago

Occured today with Opus 4.8 (1M + MAX effort), repeated trillions of times !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 👎

I suspect that today's 4.8 was secretly downgraded to 4.7.

I want to order a token refund !

ShunmeiCho · 1 month ago

Still reproducing on Opus 4.8 (claude-opus-4-8, 1M context), Claude Code 2.1.168 — this legacy-XML-into-tool-call leak is not limited to 4.7. (#61133 was closed as a duplicate of this; recording fresh evidence here.)

Signature: an assistant message with stop_reason: "tool_use" but zero structured tool_use blocks — the call is emitted as literal text instead.

Three surface forms in a single session:

  1. Edit leaked as XML — one ~821-char text block containing literal <invoke name="Edit"> x2 + <parameter> x8, with no tool_use block. The next turn re-emitted the first Edit as a proper structured tool_use block (retry/recovery); when the retry also leaks, the client surfaces "The model's tool call could not be parsed (retry also failed)".
  2. Largest leak — a single ~12.8 KB block of tool-call XML rendered as plain text.
  3. Bash leaked as text — prose explanation, then a bare call token, then a full multi-line shell script rendered as plain text, ending with "Ran 1 shell command" (the harness partially recovered and ran one command out of the leaked text). cf #66011 ("leaked call text").

Frequency: 8 messages in a single transcript had stop_reason: "tool_use" + 0 tool_use blocks (pure leak), the largest being ~12.8 KB of tool-call XML emitted as text.

Trigger (consistent across all instances):

  • long narrative/explanatory text immediately before the tool call
  • multiple tool calls in one turn
  • high tool-density / long-context session

Workaround from #60584 (put the tool call first, explanation after) reduces it. Context-poisoning per #62344 confirmed — once one malformed call enters the context, subsequent calls repeat the broken format. The #61133 memo workaround ("Describe in text what you were going to do next without using any tools", otherwise /compact) also helps recover a poisoned session.

Env: Claude Code 2.1.168, model claude-opus-4-8 (1M context), macOS.

Sora-bluesky · 1 month ago

Another data point — byte-level numbers from my own transcripts, and it's
still happening on the latest build.

I parsed 21 of my own Claude Code main-session JSONL files (45,612 lines)
byte-by-byte. (grep undercounts this badly: <invoke is stored <-escaped,
and naive token grep also over-counts prose, so this needs real parsing.)

  • **38 genuine cases of a raw <invoke name=…> block emitted into an assistant

text block instead of a structured tool_use.**

  • 100% Opus 4.8 (38/38). In the same transcripts my Opus 4.7 sessions (6,453

assistant records) show zero genuine leaks. I don't read this as
contradicting the 4.7 reports above — leak frequency clearly depends on payload
shape and session length — but in my escape-dense, long-context editing
workflow the rate goes from ~0 on 4.7 to clearly non-zero on 4.8.

  • Still on the latest build: 9 of the 38 happened **today (2026-06-10) on

Claude Code 2.1.170**. Earliest was 2026-06-03; nothing before that.

  • Leak signature: a stray count\n<invoke… (28×) or court\n<invoke…

(10×) immediately before the block — so it isn't always the literal count
token; court shows up too. One case leaked two parallel invokes (Bash +
an MCP tool) into a single text block.

  • Tools go beyond Write/Edit: Edit 12 · Read 7 · PowerShell 6 · Grep 4 ·

ToolSearch 3 · Write 3 · Bash 1 · Skill 1 · Agent 1. Escape-dense content
dominates (matching the "longer payloads" trigger here), but short calls leak
too after a long free-text preamble.

  • No harness rejection is recorded. In all 38 cases the malformed invoke is

emitted as plain text and the turn just continues — there is no error
tool_result in the JSONL. 20 of the 38 carried stop_reason == "tool_use"
with no tool_use object in content at all.
That is a direct,
machine-checkable invariant violation and supports the client-side guard
proposed in #63870: if stop_reason == "tool_use", assistant content must
contain ≥1 structured tool_use object — otherwise the leak silently becomes
prose and poisons the rest of the session (cf. #62344).

Happy to share a redacted parser + the per-occurrence table if useful.

Honsal · 1 month ago

It still occurs at 4.8 xhigh, max. Efforts below that have not been tested.

The format is also arbitrary.

Sometimes it is

  court
  <invoke name="Bash">
  <parameter name="command">...</parameter>
  <parameter name="description">...</parameter>
  </invoke>

and other times

  call
  <invoke name="Bash">
  <parameter name="command">...</parameter>
  <parameter name="description">...</parameter>
  </invoke>

that are printed exactly as is.

This persists when doing /export -> new Claude session -> pasting. Therefore, it seems that a specific string is triggered and the model locks into a dumb direction.

/compact sometimes works and sometimes doesn't. It was effective in yesterday's session, but in today's recurrence (a completely new session), it reoccurred immediately on the first turn after compact.

eternalcomet · 1 month ago

happens on opus 4.8 API

v-karbovnichy · 1 month ago

For me it starts to happen around 500k context for Opus 4.8.
Sometimes earlier, sometimes later.

When it happens, the session is pretty much dead - the tool calls are not working anymore, although agent notices it and tries to fix itself and repeat them. All with the same failure.

TheGreatCBH · 27 days ago

Client-side workaround — auto-recovery via Stop hook (Strategy A + B)

(The root issue is tracked here; this comment is a workaround until a server-side fix ships.)

Two failure shapes are now detected and auto-recovered:

---

Strategy B — harness gave up after verbatim retry (original shape)

Turn ends with stop_reason="stop_sequence" and text containing:

The model's tool call could not be parsed (retry also failed).

Strategy A — model leaked XML as literal text (second shape, discovered empirically)

Turn ends with stop_reason="end_turn" but the assistant's text block contains literal
<invoke name="..."><parameter name="command">...</parameter> markup — the tool was
never executed, but the harness sees a normal end_turn and does nothing.

This is what happens when you see <invoke name="Bash"> rendered visibly in the UI
and the conversation ends without the model completing its intended work.

---

Detection signal (verified across 830+ real transcripts):

| Strategy | True positives | False positives |
|---|---|---|
| B: stop_sequence + exact terminal string | 7 | 0 |
| A: end_turn + <invoke name=" + <parameter name= after it | 33 | 1 (accepted†) |

†The 1 false positive occurs when the model writes a complete XML code example in prose. Impact is bounded by MAX_RETRIES=2.

Hook response on match:

{ "decision": "block", "reason": "Your tool call appeared as leaked XML text — the tool was NOT executed. Re-issue it now using the normal tool call mechanism. Keep the call SHORT." }

A per-session disk counter caps retries at MAX_RETRIES = 2; stop_hook_active prevents infinite loops.

---

Drop-in hook with tests and install instructions:
https://github.com/TheGreatCBH/claude-code-toolcall-recovery

gonnector · 20 days ago

We built and have been running a Stop-hook for this in production (Opus 4.8 1M, Windows) and came to share it — then saw @TheGreatCBH had already posted a nearly identical decision:block + per-session-counter approach above, early on. That's the good side of community echo: independently converging on the same fix is a decent signal it's the right shape. Sharing ours anyway since it differs in two ways that helped in tool-heavy / multi-agent sessions:

  1. Don't skip when a real tool_use is present in the turn. In multi-tool turns only some calls sometimes leak; gating on "no tool_use this turn" misses partial leaks. I match the leaked <invoke name="…"> + (<parameter name="…"> | </invoke>) structure regardless of whether other calls in the same turn executed.
  2. Strip code fences + inline backticks before matching, which removes the prose-XML false-positive class (instead of accepting ~1 FP bounded by retries).

I also raised the retry cap to 5 and, on give-up, keep the counter so the episode stops being blocked and the reason recommends a new session — matching the #62344 propagation/poisoning recovery pattern. Full Windows-thread writeup with the script is in #68354. Still reproducing on 2.1.191 here, and I don't see a tool-call fix in the 2.1.188–2.1.193 notes.

TheGreatCBH · 20 days ago

@gonnector Thanks for sharing this — and nice to see independent convergence on the same decision:block + per-session-counter shape. Agreed that's a decent signal it's the right fix.

I've adopted your code-formatting-stripping idea into my repo: before the Strategy A match I now strip fenced code blocks and inline-backtick spans, so a turn that merely quotes tool-call XML in prose no longer trips the detector. That removes the one false positive I'd previously documented as "accepted (bounded by MAX_RETRIES)" — credited to you in the code and the findings doc.

On your other two points:

  • Partial leaks in multi-tool turns: my detector doesn't gate on "no tool_use this turn" either. The primary path inspects the harness's last_assistant_message for the leaked <invoke>/<parameter> markup regardless of whether other calls in the same turn executed, so partial leaks are caught.
  • Retry cap: I keep MAX_RETRIES=2 deliberately (give the human the failure fast once it's clearly stuck), whereas you go to 5 and keep the counter on give-up to recommend a fresh session — both reasonable; yours leans toward more self-healing, mine toward failing loud sooner.

Still reproducing here too, no tool-call fix visible in the recent release notes. Repo (Python impl, with tests): https://github.com/TheGreatCBH/claude-code-toolcall-recovery

noaxis · 14 days ago

This bug is NOT Opus-only: confirmed on claude-sonnet-5 (and user-observed on claude-fable-5)

Most reports here involve Opus 4.7/4.8, and several workaround threads assume Sonnet is unaffected. We can confirm from session JSONL logs that the same legacy-XML-in-text failure occurs on Sonnet 5.

Environment: Claude Code CLI 2.1.198, Linux, claude-sonnet-5, heavy Japanese (CJK) usage in the session.

Captured occurrence (2026-07-01T11:04Z): the assistant message contains a plain text block with the legacy XML tool-call markup instead of a JSON tool_use block:

Bash
<invoke name="Bash">
<parameter name="command">cd /home/<user>/dev/zero-v2 && echo "=== branches ===" && git branch -a | head -30 && ...</parameter>
<parameter name="description">List branches, worktrees, and current time</parameter>
</invoke>

The harness responded with Your tool call was malformed and could not be parsed. Please retry., the retry emitted the same XML-in-text form again, and the turn ended with The model's tool call could not be parsed (retry also failed).

Notable details that contradict some hypotheses in this thread:

  • Small context: usage on the failing turn was input_tokens=6519, cache_read=24696 (~31k total). Not a long-payload / near-limit situation.
  • English-only tool descriptions: the malformed call's description field was plain English ("List branches, worktrees, and current time"), so CJK-in-description avoidance does not fully prevent it (though our long-run stats still show strong CJK-session bias overall).
  • Frequency is heavily Opus-skewed but nonzero elsewhere: across ~1.5 months of local logs we count 498 parse-error events; nearly all on Opus 4.7/4.8, 1 on Sonnet 5 so far (Sonnet usage share was small until recently). A user in our team also observed it on Fable 5 (claude-fable-5) shortly after switching, though that occurrence is not yet in our parsed logs.

Since "switch to Sonnet" is the de-facto workaround circulating for this issue, it seems worth flagging that it reduces frequency substantially but does not eliminate the bug — the server-side format-mixing can apparently affect the current model family as well.

bam93 · 12 days ago

**Additional data point: a silent, zero-error manifestation — the tool returns success with an empty result, so the loss goes undetected for weeks.**

Confirming this on Claude Code 2.1.201, Opus (1M tier), macOS, against a local stdio MCP server (Python). Same root cause as the OP — an array parameter never reaches the server on longer payloads — but the surface was different from every case above, and I think that difference is worth flagging because it makes the bug much harder to notice.

The failure was completely silent. My end_session-style tool takes reflections and facts as arrays of objects. When the decoder flipped, those arrays arrived at the server as undefined — but the handler treats a missing optional array as "nothing to store," not as an error. So the tool returned a successful result: {"reflections_stored": 0}. No schema validation error. No retry. No <invoke> XML in the chat. Just a success response with a zero count.

Consequence: /meditate-style session reflections silently failed to persist for ~2 weeks, and it was only caught when the human noticed his memory wasn't recalling recent sessions. Unlike the loud expected array, received undefined → 3-5 retries pattern in the OP, there was nothing to alert on — the harness, the model, and the server all reported success.

Server-side ground truth (I instrumented the handler to log raw received args):

received keys=['narrative']                 → reflections absent   (1 sent)
received keys=['narrative', 'key_topics']   → facts absent          (facts[] + key_topics[] sent)
received keys=['narrative']                 → reflections absent   (1 sent)

Array-of-string (key_topics) and the string narrative survived; the object-arrays did not. Delivered on roughly 1 of 5 identical calls — matching the "intermittent, correlates with payload length" behavior others report.

Things I tried that did NOT reliably work (in case it saves someone time):

  • Re-typing the param to string (JSON-encoded) in the tool schema — the added string param was also dropped on most calls, so this isn't a reliable client-side fix.

What DID work — a deterministic bypass rather than a retry hook. For the critical structured data I stopped sending it through the tool call at all: the caller writes the payload to a JSON file, and a tiny script invokes the MCP server's own storage functions directly (no MCP tool call → nothing to mis-serialize). 100% reliable because the flaky path is never exercised for that data; the short required narrative string still goes through the tool fine. This complements the Stop-hook auto-retry approaches (TheGreatCBH / gonnector) above — useful specifically for the silent variant, where there's no error for a retry-hook to catch.

Suggestion for the harness, independent of the model-side fix: when stop_reason == "tool_use" but the parsed arguments are missing a parameter the schema marked present in the raw output (or when leaked <parameter name=...> markup is detected inside a JSON string value), surface it — even just a warning — rather than passing a silently-truncated arguments object to the tool. Right now the silent variant produces a "successful" call with missing data and no signal at all.

thebbandj · 1 day ago

Confirms this bug outside the MCP-tool case: hit the identical failure in Claude Code's built-in Workflow tool, on claude-sonnet-5, using the native StructuredOutput tool (not a custom MCP tool) — so this isn't MCP- or Opus-specific.

A workflow's final synthesis step needed to emit a 6-item findings array via StructuredOutput. 3 consecutive attempts failed with must have required property 'findings'; raw journal shows the same signature as above — a stray </summary> followed by <parameter name="findings">... embedded inside the summary string, corrupting the JSON. After 3 failures, the model sent a deliberately tiny placeholder payload to "isolate the schema issue" — which validated (small enough to escape the glitch) and was immediately treated as the final, successful answer. No error surfaced anywhere; the workflow reported success with junk data. Worth flagging as a compounding failure mode: for any tool contract where a successful structured-output call ends the turn with no further chance to submit, this bug doesn't just waste retries — it can silently produce and lock in wrong output.