[BUG] PostToolUse hooks do not fire on failed Bash commands (non-zero exit code)

Resolved 💬 4 comments Opened Mar 1, 2026 by Elijas Closed Mar 3, 2026

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.

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗