[BUG] Opus 4.7 mixes legacy XML tool-use format into JSON tool calls on longer payloads
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
- 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 severalz.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" }] })
);
- Connect it to Claude Code via
.mcp.jsonover stdio. Confirm it loads. - Start a Claude Code session on Opus 4.7.
- Prompt the model to perform any non-trivial work and then call
log_outcomewith a thorough summary (ask it to describe what it did in 2-3 sentences per field). - 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_
33 Comments
I have also noticed this as an issue not just in Claude Code but with Opus 4.7 via the API.
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:
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.
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 thecontentarray contains only an empty thinking block (0 chars, with valid signature) — thetool_useblock 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:
id,usage,diagnostics, thinkingsignature).output_tokensis 1,600–1,700 (non-trivial), meaning the model did generate output — but thetool_useblock was dropped from the response whilestop_reasonstill saystool_use.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.
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 inThe 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.
Adding a data point that matches @akige's empty-thinking-block + missing
tool_usebranch (not the XML-leak pattern in the OP). Captured from local session JSONL, so I can confirm the exact response structure.Environment
2.1.149on Windows 11standardspeed (no/fast)Frequency & impact
retry also failed) has surfaced intermittently — about 5 times recently.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):stop_reason: "tool_use"usage.output_tokens: 570(non-zero)content: [ { "type": "thinking", "len": 0, "has_signature": true } ]— notool_useblock at all"Your tool call was malformed and could not be parsed. Please retry."stop_reason: "tool_use",output_tokens: 795, only an emptythinking(0)blockmodel: "<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
output_tokensis 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.thinkingblock carries a valid signature despite being 0-length ("signed empty thought").model: "<synthetic>"with0tokens — 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.
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?
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, closedas a dup of this issue, shows the identical wording.)
Two trigger observations that may extend the "argument length" correlation in
the original report:
comes right after a large tool _output_ earlier in the session — e.g. a
git branchlisting 150+ lines, or a long sub-agent/Task report. Thepreceding output volume seems to matter, not just the length of the current
call's arguments.
tool_useblocks 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,ghcalls, anidf.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).
I encountered this too, +1
Even after disabling 1M or switching to medium, I can't avoid the issue.
Returning to 4.6 makes it work 100% correctly.
same here, if i use /model claude-opus-4-6[1m], can solve this problem
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 parsedfollowed byThe 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.
This still happens with newly released Opus 4.8.
This still happens with newly released Opus 4.8.
Opus 4.8 has the same problem.
Opus 4.8 has the same problem.
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 plaintextinstead of a structuredtool_use, so the tool never dispatches. In my sessions the stray opener iscourt(vscallin #63870) — the token varies but the failure is the same: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.
opus-4-8.2.1.156on 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.
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:
Severity is high for autonomous/looping agents: a silent no-op that mimics a normal response.
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.
At this point, it’s unclear how the authorities plan to resolve this issue, as it has already severely disrupted normal usage.
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, notool_result: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 Japanesedescriptionand 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.I’ve found that /effort with high or ultracode doesn’t trigger often, but with xhigh it happens frequently.
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 !
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:
<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)".calltoken, 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):
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.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:
<invokeis stored<-escaped,and naive token grep also over-counts prose, so this needs real parsing.)
<invoke name=…>block emitted into an assistanttextblock instead of a structuredtool_use.**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.
Claude Code 2.1.170**. Earliest was 2026-06-03; nothing before that.
count\n<invoke…(28×) orcourt\n<invoke…(10×) immediately before the block — so it isn't always the literal
counttoken;
courtshows up too. One case leaked two parallel invokes (Bash+an MCP tool) into a single text block.
ToolSearch 3 · Write 3 · Bash 1 · Skill 1 · Agent 1. Escape-dense
contentdominates (matching the "longer payloads" trigger here), but short calls leak
too after a long free-text preamble.
emitted as plain text and the turn just continues — there is no error
tool_resultin the JSONL. 20 of the 38 carriedstop_reason == "tool_use"with no
tool_useobject 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 mustcontain ≥1 structured
tool_useobject — otherwise the leak silently becomesprose and poisons the rest of the session (cf. #62344).
Happy to share a redacted parser + the per-occurrence table if useful.
It still occurs at 4.8 xhigh, max. Efforts below that have not been tested.
The format is also arbitrary.
Sometimes it is
and other times
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./compactsometimes 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.happens on opus 4.8 API
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.
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: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 wasnever executed, but the harness sees a normal
end_turnand does nothing.This is what happens when you see
<invoke name="Bash">rendered visibly in the UIand 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:
A per-session disk counter caps retries at
MAX_RETRIES = 2;stop_hook_activeprevents infinite loops.---
Drop-in hook with tests and install instructions:
https://github.com/TheGreatCBH/claude-code-toolcall-recovery
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:tool_useis 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.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.
@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:
last_assistant_messagefor the leaked<invoke>/<parameter>markup regardless of whether other calls in the same turn executed, so partial leaks are caught.MAX_RETRIES=2deliberately (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
This bug is NOT Opus-only: confirmed on
claude-sonnet-5(and user-observed onclaude-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
textblock with the legacy XML tool-call markup instead of a JSONtool_useblock: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 withThe model's tool call could not be parsed (retry also failed).Notable details that contradict some hypotheses in this thread:
input_tokens=6519, cache_read=24696(~31k total). Not a long-payload / near-limit situation.descriptionfield 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).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.
**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 takesreflectionsandfactsas arrays of objects. When the decoder flipped, those arrays arrived at the server asundefined— 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 loudexpected 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):
Array-of-string (
key_topics) and the stringnarrativesurvived; 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):
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
narrativestring 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.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
StructuredOutputtool (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 withmust have required property 'findings'; raw journal shows the same signature as above — a stray</summary>followed by<parameter name="findings">...embedded inside thesummarystring, 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.