PostToolUse: malformed updatedToolOutput passes validation and throws 'e.reduce is not a function' instead of falling back to original output
Summary
A PostToolUse hook that returns a malformed updatedToolOutput (an object that matches no tool output shape, e.g. {"text": ""}) is not caught by the validation/fallback path the binary itself promises. Instead of degrading to the original tool output, the malformed value reaches a downstream content-length reducer and throws:
API Error: ... (In 'e.reduce((t,r)=>t+(r.type==="text"?r.text.length:0),0)', 'e.reduce' is undefined)
Every call to the affected MCP tool then hard-fails in live sessions.
Expected behavior
The binary's own validation message documents the intended contract:
"updatedToolOutput that does not match <tool>'s output shape: ... using original output."
i.e. a shape mismatch should be non-fatal: log, discard the hook's replacement, and fall back to the original tool output.
Actual behavior
The malformed value ({"text": ""} where an MCP tool's output shape is an array of content blocks) passes validation and then throws inside a reducer that assumes an array of content blocks (e.reduce(...) with e being the non-array object). The user-visible result is an API Error on every call to that tool - no fallback, no degradation.
Minimal repro
- Register any MCP server with any tool.
- Add a
PostToolUsehook matching that tool which returns:
{"hookSpecificOutput":{"hookEventName":"PostToolUse","updatedToolOutput":{"text":""}}}
- Call the tool. Observed:
e.reduce is not a functionAPI error surfaced to the session instead of the original tool output.
Why it matters
In our case a small bug in our own hook (emitting {text: string} instead of content blocks) was converted by this gap into a weeks-long total outage of the MCP-backed feature - which is exactly the failure class the documented "using original output" fallback exists to prevent. The hook bug was ours; the missing fallback that amplified it is Claude Code's.
Environment
- Observed and reproduced on Claude Code 2.x (current at filing: 2.1.226), Linux (WSL2) and Windows.
- The validation string and the
updatedToolOutputdispatch are present in the shipped binary (grep-able), so the intended non-fatal path exists - it just does not engage for this shape class.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗