[BUG] PostToolUse hooks do not fire on failed Bash commands (non-zero exit code)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
PostToolUse hooks do not fire when a Bash command exits with a non-zero exit code. The hook only fires on successful (exit 0) tool completions, silently skipping failures.
This was previously reported in #16282 (auto-closed as stale on 2026-03-01), #16599, and #6371. The bug is still present in v2.1.63. Filing fresh per the stale-bot's instructions.
What Should Happen?
PostToolUse should fire after all Bash tool completions, including failures. The hook payload should include success/failure context (e.g., "success": false, exit code, stderr) so hook consumers can distinguish outcomes.
A "post" hook that only fires on success is semantically a PostToolUseSuccess hook — not a general post-hook. Every standard use case for post-event hooks (logging, cleanup, analytics, error handling) requires firing on all completions.
Error Messages/Logs
# Hook log after running 3 Bash commands (success, failure, success):
12:31:17 PostToolUse fired for: Bash ← echo PASS_1 (exit 0)
← ls ./nonexistent_xyz (exit 1) — MISSING
12:31:22 PostToolUse fired for: Bash ← echo PASS_2 (exit 0)
# Expected: 3 entries. Actual: 2. Failed command produced no hook invocation.
Steps to Reproduce
Minimal, self-contained reproduction using only claude CLI and .claude/settings.json:
# 1. Setup
mkdir -p /tmp/hook-bug-repro/.claude
cat > /tmp/hook-bug-repro/hook.sh << 'HOOKEOF'
#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_name','?'))" 2>/dev/null)
echo "$(date '+%H:%M:%S') PostToolUse fired for: $TOOL" >> /tmp/hook-bug-repro/hook.log
HOOKEOF
chmod +x /tmp/hook-bug-repro/hook.sh
cat > /tmp/hook-bug-repro/.claude/settings.json << 'SETEOF'
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "bash /tmp/hook-bug-repro/hook.sh"}]
}
]
}
}
SETEOF
# 2. Run (3 Bash commands: success → failure → success)
cd /tmp/hook-bug-repro
rm -f hook.log
claude -p 'Run these 3 bash commands in separate tool calls:
1. echo PASS_1
2. ls ./nonexistent_xyz
3. echo PASS_2
Command 2 WILL fail — that is intentional.' --allowedTools 'Bash(command:*)'
# 3. Verify
cat /tmp/hook-bug-repro/hook.log
# Expected: 3 log entries (one per command)
# Actual: 2 log entries (failed command skipped)
Claude Model
Opus
Is this a regression?
I don't know
Claude Code Version
2.1.63 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
iTerm2
Additional Information
Prior issues (all still unresolved):
- #16282 — Original report, auto-closed as stale (closing comment)
- #16599 — Consolidated into #16282
- #6371 — Original tracking issue
Use cases blocked by this bug:
- Automated failure logging / learning-from-mistakes workflows
- Hook-based error detection and notification (e.g., sound alerts on failure)
- CI-style failure reporting
- Analytics that need complete tool usage data
- Automated remediation hints based on stderr parsing
Suggested fix (Option B from #16282): Extend PostToolUse to always fire, with a success boolean and failure context:
{
"tool_name": "Bash",
"tool_input": {"command": "ls ./nonexistent"},
"success": false,
"tool_response": {
"stdout": "",
"stderr": "ls: ./nonexistent: No such file or directory",
"exit_code": 1
}
}
As noted in #16282 comment, a trigger field ("success" | "failure" | "both", defaulting to "success") would preserve backward compatibility for existing hooks.
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗