hooks

PostToolUse Hook: React to Tool Results in Claude Code

Quick Answer

PostToolUse hooks run a shell command after each tool call completes. Environment variables include CLAUDE_TOOL_NAME, CLAUDE_TOOL_INPUT, and CLAUDE_TOOL_OUTPUT. Use them for auto-formatting (prettier after Write), auto-testing (vitest after editing test files), or logging (append tool outputs to audit logs).

PostToolUse hooks are the most practical hook type for day-to-day use. They automate the tedious side effects that should happen after every tool call: format the file you just wrote, run the tests you just edited, update the dependency lockfile after package.json changed. These are chores that Claude doesn't do by default but that you'd always want done.

The hook receives `CLAUDE_TOOL_OUTPUT` — the output of the completed tool call. For Write, this includes the path written. For Bash, this is the command's stdout. For Read, it's the file content. You can use this in your hook to make decisions: 'if the bash output contains FAIL, send a Slack alert'.

Auto-formatting is the most common PostToolUse hook. Every time Claude writes or edits a file, run Prettier or your formatter automatically. This ensures that even if Claude writes syntactically correct but poorly formatted code, it's formatted before Claude reviews its own output — and before you see the diff.

Auto-testing is the second most valuable pattern. After Claude writes to a .test.ts file, automatically run those tests. Claude gets immediate feedback in its tool output stream, which it can use to fix failing tests without you having to ask. This creates a tight write-test-fix loop.

Logging patterns are useful for compliance and debugging. A PostToolUse hook on all tool calls that appends to an audit log gives you a complete record of what Claude did during a session. This is valuable for code review of AI-assisted work and for post-incident analysis.

PostToolUse hooks run synchronously — Claude waits for the hook to complete before processing the tool result. Keep hooks under a few seconds for performance. If you need to trigger a slow process (deploy, full test suite), do it asynchronously in the background and log that it was triggered.

Examples

Auto-format on every Write/Editjson
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "command": "bash -c 'FILE=$(echo \"$CLAUDE_TOOL_INPUT\" | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get(\\\"path\\\", d.get(\\\"file_path\\\", \\\"\\\")))\" 2>/dev/null); [ -n \"$FILE\" ] && npx prettier --write \"$FILE\" 2>/dev/null || true'"
      }
    ]
  }
}
Auto-run tests after editing test filesbash
#!/bin/bash
# .claude/hooks/auto-test.sh
# Runs tests when Claude writes to a .test.ts file

[ "$CLAUDE_TOOL_NAME" = "Write" ] || [ "$CLAUDE_TOOL_NAME" = "Edit" ] || exit 0

# Get file path from tool input
FILE=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('path', d.get('file_path', '')))" 2>/dev/null)

# Only run for test files
if [[ "$FILE" == *.test.ts ]] || [[ "$FILE" == *.test.tsx ]] || [[ "$FILE" == *.spec.ts ]]; then
  echo "[Auto-test] Running tests for $FILE"
  # Run in background to avoid blocking, but show output
  pnpm test "$FILE" --reporter=verbose 2>&1 | tail -20
fi

exit 0
Audit log: record all tool callsbash
#!/bin/bash
# .claude/hooks/audit-log.sh
# Appends every tool call to a session audit log
# Used as PostToolUse hook on matcher: .*

TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
LOG_FILE="$HOME/.claude/audit-$(date +%Y-%m-%d).log"

# Truncate long outputs in the log
OUTPUT_PREVIEW=$(echo "$CLAUDE_TOOL_OUTPUT" | head -3 | tr '\n' ' ' | cut -c1-200)

echo "$TIMESTAMP | $CLAUDE_TOOL_NAME | $(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(list(d.values())[0] if d else '')" 2>/dev/null | cut -c1-100)" >> "$LOG_FILE"

exit 0

Tips

  • Always exit 0 from PostToolUse hooks unless you have a specific reason to fail — PostToolUse hooks that exit non-zero will cause Claude to see an error from the hook, which may confuse its next action.
  • Use `|| true` at the end of formatting commands in inline hook commands — formatters sometimes exit non-zero on unchanged files, and you don't want that to propagate.
  • Run tests asynchronously (with `&` and a small timeout) if they're slow — a PostToolUse hook that blocks for 30 seconds on every file write makes Claude feel unresponsive.
  • Filter in your hook script rather than writing multiple hooks with different matchers — one hook that checks `$CLAUDE_TOOL_NAME` is easier to maintain than five matcher-specific hooks.
  • Log the tool name and timestamp even if you don't log the full input/output — a lightweight timestamp log is low-cost and enables post-session review.
  • For auto-formatting: run the formatter in `--check` mode first and only run the full format if needed — saves time when code is already correctly formatted.

FAQ

Can a PostToolUse hook modify what Claude sees as the tool output?+

No — in the current version, PostToolUse hook output goes to stdout/stderr for your terminal, not back into Claude's context. Claude sees the original tool output from before the hook ran. If you want to affect Claude's next action, you'd need a UserPromptSubmit hook or a different architectural approach.

Does CLAUDE_TOOL_OUTPUT contain the full output or a truncated version?+

CLAUDE_TOOL_OUTPUT contains the full tool output as returned to Claude. For large outputs (files > a few thousand lines, bash commands with lots of output), Claude Code may have already truncated it before passing to the hook. Check the actual size in your hook scripts before assuming completeness.

Can I have multiple PostToolUse hooks?+

Yes — you can have multiple entries in the PostToolUse array in settings.json. Each hook runs in order after each tool call. If you have both an auto-format hook and an audit-log hook, both run after every matching tool call.

Related Guides