Claude / Operating Manual · Reference
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
PreToolUsehook matched toBash. Readtool_input.commandfrom 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 optionaliffield 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 thematcher. Wiringif: "Bash(rm *)"onto aBashmatcher means your block-script never even launches except onrmcommands. That is cheaper than starting the process and bailing inside it. - Enforce tool conventions. Use
PreToolUsematched toBashto redirectgrep→rg, 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
PermissionRequesthook matched toRead, inspecttool_input.file_pathand emit{"permissionDecision": "allow"}for.md/.json/.txtfiles to cut down on prompts. - Log every file write. A
PostToolUsehook matched toWrite|Editcan appendtool_input.file_pathto a daily audit log: one line, no ceremony. - Run a formatter after edits.
PostToolUsematched toEdit|Writecallsprettier --writeorgofmton the changed file. The model sees the result in the next read. - Wire up notifications. The
Notificationhook fires for permission prompts, idle waits (60+ seconds), and auth events. Pipe them to aterminal-notifierorosascriptcall so you don't miss a stalled session. - Inject setup context.
SessionStartandSetuphooks have access toCLAUDE_ENV_FILE. Write variables there to inject them into the session environment without touching your shell profile.
Gotchas
- Matchers are case-sensitive.
bashwon't catchBash. Check the exact tool name from the reference table (capitals:Bash,Write,Edit,Read,Glob,Grep,WebFetch,Task). ifonly fires on tool events. Theiffield is evaluated solely onPreToolUse,PostToolUse,PostToolUseFailure,PermissionRequest, andPermissionDenied. 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 wildcardmcp__filesystem__.*regex works if you want the whole server. - Timeout defaults matter. The
timeoutfield 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.pybefore wiring them in. - Hooks don't run in
bypassPermissionsmode. 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.
python3might not be onPATH, so use/usr/bin/python3or the absolute path fromwhich.
Start with logging, not blocking. Wire a
PostToolUsehook 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.