Claude / Operating Manual · Reference

← All Operating Manual pages

Hooks

Shell commands that fire at lifecycle events. Use them to validate, block, log, or format without touching Claude's logic.

GA · updated 2026-06-07

Hooks let you attach arbitrary shell commands to specific points in a Claude Code session. When an event fires (a tool about to run, a prompt submitted, the session ending), Claude pipes context JSON into your script and acts on what comes back. Exit 0 and work proceeds normally; exit 2 and the operation is blocked with your stderr as the error message. Any other non-zero exit is a non-blocking warning. That three-way contract is the whole interface.

Configure them in ~/.claude/settings.json (user-wide) or .claude/settings.json / .claude/settings.local.json (project). Hooks execute real shell commands as your user. Treat them like cron jobs: audit what's in there, quote all variables, use absolute paths.

How you use it day-to-day

  • Block dangerous Bash patterns. Wire a PreToolUse hook matched to Bash. Read tool_input.command from stdin, check it against a deny-list (e.g. DROP TABLE, rm -rf /), and exit 2 with a plain-text reason. Claude sees the error, rethinks, and doesn't run the command.
  • Narrow a hook with if, not just the event matcher. A hook entry takes an optional if field that uses permission-rule syntax ("Bash(git *)", "Edit(*.ts)") so the script only spawns when the command or file argument matches, layered on top of the matcher. Wiring if: "Bash(rm *)" onto a Bash matcher means your block-script never even launches except on rm commands. That is cheaper than starting the process and bailing inside it.
  • Enforce tool conventions. Use PreToolUse matched to Bash to redirect greprg, or any other house-rule about which tools to prefer. Exit 2 with the suggestion; the model adjusts.
  • Auto-approve low-risk reads. On a PermissionRequest hook matched to Read, inspect tool_input.file_path and emit {"permissionDecision": "allow"} for .md/.json/.txt files to cut down on prompts.
  • Log every file write. A PostToolUse hook matched to Write|Edit can append tool_input.file_path to a daily audit log: one line, no ceremony.
  • Run a formatter after edits. PostToolUse matched to Edit|Write calls prettier --write or gofmt on the changed file. The model sees the result in the next read.
  • Wire up notifications. The Notification hook fires for permission prompts, idle waits (60+ seconds), and auth events. Pipe them to a terminal-notifier or osascript call so you don't miss a stalled session.
  • Inject setup context. SessionStart and Setup hooks have access to CLAUDE_ENV_FILE. Write variables there to inject them into the session environment without touching your shell profile.

Gotchas

  • Matchers are case-sensitive. bash won't catch Bash. Check the exact tool name from the reference table (capitals: Bash, Write, Edit, Read, Glob, Grep, WebFetch, Task).
  • if only fires on tool events. The if field is evaluated solely on PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, and PermissionDenied. Set it on any other event (SessionStart, Notification, Stop, and so on) and the hook silently never runs: the condition can't match, so the command is skipped.
  • MCP tools use a compound name. Match them as mcp__<server>__<tool>, e.g. mcp__filesystem__read_file. A wildcard mcp__filesystem__.* regex works if you want the whole server.
  • Timeout defaults matter. The timeout field in the hook definition is in seconds (default 60). Long-running scripts that exceed it are killed silently, so set an explicit timeout for anything slow.
  • JSON output must be valid. If your script prints malformed JSON, the hook is treated as a non-blocking error. Test scripts with echo '{"tool_name":"Bash",...}' | your-script.py before wiring them in.
  • Hooks don't run in bypassPermissions mode. If you're relying on a hook for a security check, know that mode defeats it.
  • Use full paths for commands. Hook scripts run in a minimal environment. python3 might not be on PATH, so use /usr/bin/python3 or the absolute path from which.

Start with logging, not blocking. Wire a PostToolUse hook that appends to a file first, and see what data arrives and what patterns you actually want to catch. Add blocking (exit 2) once you know the shape of the input.