Malformed tool call ("could not be parsed"): expose a hook event + make the malformed-retry count configurable
Summary
On long sessions — and especially on the first tool call right after a /compact resume — the model intermittently emits tool-use markup as literal assistant text (a bare invoke / parameter block) instead of a real tool_use block. Nothing executes. Claude Code injects a single retry:
Your tool call was malformed and could not be parsed. Please retry.
and, if the retry is also malformed, gives up:
The model's tool call could not be parsed (retry also failed)
After that the turn stalls or burns minutes flailing, and sometimes only a /clear (fresh session) recovers.
Two gaps make this almost impossible to mitigate as a user:
- No hook surface for parse failures. Because the emission never becomes a real
tool_use, none ofPreToolUse/PostToolUse/PostToolUseFailurefire. There is no event asettings.jsonhook can attach to in order to detect the failure, inject a corrective reminder, or collect telemetry. - The malformed-retry count is not configurable. It retries exactly once, then gives up.
CLAUDE_CODE_MAX_RETRIES(and friends) only govern transport retries (network / 5xx / overload), not this parse-failure path.
Environment
- Claude Code CLI (latest)
- Model: Opus 4.8, 1M context
- Reproduces most often in long-running sessions and immediately after
/compact
Reproduction
- Run a long session, or
/compacta large one. - Have the model make a tool call whose payload contains complex/nested content — e.g. writing a file whose body itself contains tool-call-like markup, or a multi-line shell command with nested quotes / non-ASCII.
- Intermittently the call is emitted as plain text. The transcript shows the injected
... malformed ... Please retry.meta message, and on a second failure... could not be parsed (retry also failed).
Self-reinforcement (few-shot poisoning): once one malformed block lands in context, subsequent calls tend to imitate the broken form, so a single session can produce several in a row. This is why long sessions and post-/compact turns (where a malformed block may sit near the top of the fresh window) are the hot spots.
Impact
- Turn stalls / wastes minutes; sometimes only
/clearrecovers. - Disproportionately hits long,
/compact-heavy workflows, which are exactly the sessions where losing state is most expensive.
Proposed solution (any subset would help materially)
- A hook event for parse failures — e.g. a
ToolCallParseErrorevent, or aNotificationmatcher liketool_parse_failed, carrying the offending raw text. That alone would let users inject a corrective reminder or log occurrences. - Configurable malformed-retry count — e.g.
CLAUDE_CODE_MAX_MALFORMED_RETRIES(default1), so users hitting this often can raise it. - A more actionable built-in retry prompt — the current "Please retry." does not tell the model how to fix it. A concrete hint ("emit a real tool call; if the command is large or has nested quotes, write it to a file and run the file") measurably reduces recurrence in my testing.
- Context hygiene — optionally neutralize a prior malformed block in context (e.g. fence or drop it) so it stops seeding imitation on subsequent turns.
Current workaround (local, partial)
For anyone hitting this: two things help until there's an official fix.
- A
Stophook that, at turn end, checks the last assistant turn for an unrecovered malformed block (or the "could not be parsed (retry also failed)" marker) and re-nudges the model once to re-issue the call. This only catches the turn-end-stuck tail case, since most episodes resolve mid-turn where noStopfires — which is exactly why a real parse-failure hook (item 1) is needed. - A standing rule to never inline heavy commands: write any multi-line / nested-quote / non-ASCII command to a scratch file and execute the file. Inlining such commands is the single biggest trigger I've found.
Possibly related
- #62123, #62344, #67295 (long /
/compactsessions, "could not be parsed", few-shot poisoning)
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗