Claude Code hooks
The PostToolUseFailure hook in Claude Code
PostToolUseFailure fires when a tool call fails, and receives the error. Useful for turning a cryptic failure into a message Claude can act on.
PostToolUse never sees this path at all: a failed call fires only PostToolUseFailure, so a rule written for the success event has no way to react when the same tool call goes wrong instead. Anything that needs to run either way has to be attached to both events separately.
- When it fires
- After a tool call fails.
- Can it block?
- Yes, via exit 2 or decision block.
- Matcher
- Yes, on
tool_name - Matcher examples
BashEdit|Write
What it receives
The event arrives as JSON on standard input. These are the fields worth reading.
| Field | Type | What it is |
|---|---|---|
tool_name | string | The tool that failed. |
tool_input | object | What it was called with. |
error | string | The failure itself. |
permission_mode | string | The mode in force. |
session_id | string | Stable for the session. |
cwd | string | Working directory. |
Exit codes
The exit code is the decision. Anything the script writes to standard error on a blocking exit is what Claude gets told.
| Exit | What happens |
|---|---|
0 | Success. stdout is added as context. |
2 | stderr is shown to Claude as a problem to address. |
other | Non-blocking error. |
What it can return
The value here is translation. A tool fails with a message that means something to the tool's author and nothing to a model. This hook matches the error and replaces it with the actual next step.
{
"decision": "block",
"reason": "string",
"additionalContext": "string",
"systemMessage": "string"
}
Turning a known error into the fix for it
Some failures have exactly one cause and exactly one remedy. Recognising them here saves the two or three turns Claude would otherwise spend investigating.
In settings.json
{
"hooks": {
"PostToolUseFailure": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "node ~/.claude/hooks/explain-failure.mjs" }
]
}
]
}
}
The script
const input = JSON.parse(require('fs').readFileSync(0, 'utf8')) || {};
if (!/EADDRINUSE/.test(input.error || '')) process.exit(0);
console.log('Port is already taken by the dev server from an earlier run. ' +
'Find and stop it rather than picking a different port.');
process.exit(0);
Exit 0 and print on stdout when you are adding information. Save exit 2 for when Claude should stop and deal with it.
The catch
This event does not fire when a tool is blocked by a permission rule or by a PreToolUse hook. Those are refusals, not failures, and they take different paths.