Malformed tool call ("could not be parsed"): expose a hook event + make the malformed-retry count configurable

Open 💬 1 comment Opened Jul 8, 2026 by kirillDevPro

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:

  1. No hook surface for parse failures. Because the emission never becomes a real tool_use, none of PreToolUse / PostToolUse / PostToolUseFailure fire. There is no event a settings.json hook can attach to in order to detect the failure, inject a corrective reminder, or collect telemetry.
  2. 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

  1. Run a long session, or /compact a large one.
  2. 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.
  3. 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 /clear recovers.
  • Disproportionately hits long, /compact-heavy workflows, which are exactly the sessions where losing state is most expensive.

Proposed solution (any subset would help materially)

  1. A hook event for parse failures — e.g. a ToolCallParseError event, or a Notification matcher like tool_parse_failed, carrying the offending raw text. That alone would let users inject a corrective reminder or log occurrences.
  2. Configurable malformed-retry count — e.g. CLAUDE_CODE_MAX_MALFORMED_RETRIES (default 1), so users hitting this often can raise it.
  3. 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.
  4. 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 Stop hook 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 no Stop fires — 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 / /compact sessions, "could not be parsed", few-shot poisoning)

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗