Hooks

Claude Code · Advanced · 16 min

Hooks are deterministic: unlike CLAUDE.md advice they always run, so they enforce rules the model might forget.

CLAUDE.md says "please run the formatter"; Claude usually does. A hook guarantees it: it is a command Claude Code runs automatically at a specific event in the session, whatever the model decides.

Useful events include PreToolUse (before a tool call; can block it), PostToolUse (after a tool succeeds), UserPromptSubmit (when you send a prompt), Stop (when Claude is about to finish its turn), SessionStart, Notification and PreCompact. There are many more in the reference.

Hooks are configured in a settings file under hooks: event, then a matcher (for tool events, the tool name, such as Bash or Edit|Write), then the handlers. The common handler type is command; others send the event to a URL, call an MCP tool, or ask a model. This formats files after every edit:

{"hooks": {"PostToolUse": [{"matcher": "Edit|Write",
  "hooks": [{"type": "command", "command": "npx prettier --write . --log-level warn"}]}]}}

How a command hook talks to Claude Code:

  • it receives the event as JSON on stdin (standard input), including tool_name and tool_input,
  • exit code 0 means success; stdout may contain JSON with a decision,
  • exit code 2 means block: on events that can block, the action is stopped and your stderr message is shown to Claude as the reason,
  • other exit codes are non-blocking errors.

Typical hooks: run the formatter after every edit, block edits to migrations/ or generated files, run the test suite in a Stop hook so Claude cannot finish while tests fail, send a desktop notification when Claude needs input, or filter huge test output down to failures.

/hooks lists configured hooks and where each comes from. Use ${CLAUDE_PROJECT_DIR} to reference scripts in the repo. Hooks run with your user's full permissions, so review hooks in any repository before trusting it.

Example

#!/usr/bin/env bash
# .claude/hooks/protect-migrations.sh  (chmod +x it)
# Registered in .claude/settings.json:
#   "PreToolUse": [{ "matcher": "Edit|Write",
#     "hooks": [{ "type": "command",
#       "command": "\"${CLAUDE_PROJECT_DIR}\"/.claude/hooks/protect-migrations.sh" }] }]

path=$(jq -r '.tool_input.file_path // empty')     # event JSON arrives on stdin
if [[ "$path" == *"/migrations/"* ]]; then
  echo "Migrations are generated; change the model and run make migrations." >&2
  exit 2                                          # 2 = block, stderr goes to Claude
fi
exit 0

Remember

  • Hooks run your commands at events like PreToolUse, PostToolUse and Stop
  • Event JSON arrives on stdin; exit 2 blocks (where the event allows), stderr explains why
  • Use hooks for must-always rules; check /hooks and review repo hooks before trusting

Watch out for

  • Writing a slow hook on every PostToolUse (such as a full test suite), which makes every edit painfully slow.

Check yourself

  1. A PreToolUse hook script exits with code 2 and prints a message to stderr. What happens?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: code.claude.com/docs/en/hooks-guide