[DOCS] PostToolUseFailure documented as firing 'after a tool call fails', but built-in tool_use_error rejections (e.g. Edit old_string mismatch) fire neither post event (#24908 closed not-planned; docs never updated)
Summary
The hooks documentation (https://code.claude.com/docs/en/hooks) defines the two post-tool events as:
PostToolUse— fires "after a tool call succeeds"PostToolUseFailure— fires "after a tool call fails" (withhookSpecificOutput.additionalContextsupport, decision control, etc.)
Read together, these promise every tool call ends in exactly one of the two events. In practice, the most common failures of the built-in file tools — Edit rejections such as "String to replace not found in file" or "File has not been read yet" — are returned as <tool_use_error> results and fire neither event. There is no carve-out anywhere in the hooks docs.
This exact behavioral gap was reported in #24908 and closed not planned (2026-03-12, auto-locked; the lock message invites a new issue referencing it). If the behavior is intended, the docs are wrong; if the docs are the intent, the behavior is a bug. Either way one of them should change — filing as [DOCS] since #24908 already decided the behavioral question once.
Environment
- Claude Code 2.1.198 (native install), macOS (darwin 25.5.0)
- Reproduced headless:
claude -p ... --model haiku --allowedTools "Read,Edit" --settings <file>
Repro
settings.json passed via --settings:
{
"hooks": {
"PreToolUse": [
{ "matcher": "Edit|Write|Read",
"hooks": [ { "type": "command", "command": "true 2>>/tmp/fired-pre" } ] }
],
"PostToolUse": [
{ "matcher": "Edit|Write|MultiEdit",
"hooks": [ { "type": "command", "command": "true 2>>/tmp/fired-post" } ] }
],
"PostToolUseFailure": [
{ "matcher": "Edit|Write|MultiEdit",
"hooks": [ { "type": "command", "command": "true 2>>/tmp/fired-fail" } ] }
]
}
}
Prompt (forces one failing then one succeeding Edit in a single session):
This is a tool-error-handling test. Step 1: Read app.py. Step 2: call the Edit tool on app.py with old_string set to exactly the text XYZNOTPRESENT and new_string set to hello — pass that old_string verbatim even though it does not appear in the file; the failure is intentional. Step 3: after the edit fails, edit greet() to return "hello".
The transcript confirms the failure occurred (tool_result with is_error: true, content <tool_use_error>String to replace not found in file. ...).
Observed
| Event | Failing Edit (tool_use_error) | Succeeding Edit |
|---|---|---|
| PreToolUse (control) | fires | fires |
| PostToolUse | does not fire | fires |
| PostToolUseFailure | does not fire | n/a |
Same result for the "File has not been read yet" rejection. So an Edit rejection is invisible to both post events — matching #24908's table for Read/Edit/Glob/Grep <tool_use_error> responses.
Expected (per current docs)
Either PostToolUseFailure fires (it is "after a tool call fails", and an edit rejection is the canonical failed tool call a hook author wants to react to), or the docs state explicitly that <tool_use_error> results from built-in tools fire neither post event.
Ask
- Docs fix (minimum): add the carve-out to the
PostToolUse/PostToolUseFailuresections — "built-in tool validation/rejection errors returned astool_use_errorcurrently fire neither event" — so hook authors don't build on the wrong event. Pointing readers at the workaround would help too:PostToolBatchdoes observe these failures (itstool_calls[].tool_responsecontains the<tool_use_error>text), and itsadditionalContextoutput is honored, verified end-to-end on 2.1.198. - Behavioral fix (preferred, reopening the #24908 question): route
tool_use_errorresults toPostToolUseFailure. Edit rejections are precisely the failure moment where injecting guidance is valuable (e.g. a reminder to re-apply the full intended change on retry rather than silently dropping parts of it — the failure mode apply-model architectures engineer around).
References: #24908 (same behavior, closed not planned — docs were never updated to match).