A hook in Claude Code is a shell command the harness runs in response to a session event. Unlike slash commands (which Claude triggers) or skills (which the model invokes), hooks fire deterministically — they do not depend on the model deciding to call them. That makes them the right tool for anything that must happen: linting, formatting, blocking dangerous commands, sending desktop notifications, logging tool calls.
Hook Events
The harness exposes several events you can hook:
SessionStart— Fires when a session starts. Useful for printing a welcome banner, loading project state, or seeding context.UserPromptSubmit— Fires every time you submit a prompt. Can inject extra context or block the prompt entirely.PreToolUse— Fires before any tool call. Can approve, deny, or modify the call. The right place to blockrm -rfor enforce a permission policy.PostToolUse— Fires after a tool returns. Use for formatters (run Prettier after every Edit) or audit logs.Stop— Fires when Claude finishes its turn. Common use: a desktop notification so you can switch away while waiting.SubagentStop— Same as Stop but for subagent completion.Notification— Fires when Claude sends a notification (waiting for input, permission request).
Defining Hooks
Hooks live in .claude/settings.json (project) or ~/.claude/settings.json (user). The schema:
| |
The matcher field is a regex against the tool name. Omit it to match every event of that type. Hook commands receive event metadata via environment variables (CLAUDE_FILE_PATHS, CLAUDE_TOOL_NAME, CLAUDE_PROJECT_DIR, etc.) and through stdin as JSON.
Real Patterns That Pay Off
Auto-format on every edit. The Prettier example above is the highest-ROI hook. You stop caring whether Claude got the indentation right because it gets fixed automatically.
Block dangerous commands. A PreToolUse hook that scans for rm -rf /, git push --force to main, or DROP TABLE and exits non-zero will refuse the call. The harness honors the exit code.
| |
Desktop notification on stop. On Windows, a one-liner using BurntToast or PowerShell’s beep keeps long agent runs ergonomic — start the task, switch tabs, get pulled back when done.
Inject project state. A SessionStart hook that runs git status --short and prints the result seeds Claude with the current working tree without waiting for it to ask.
Audit log. A PostToolUse hook that appends {tool, args, timestamp} to a JSONL file gives you a forensic record of what the agent did, which is invaluable when something goes sideways.
Exit Codes Matter
For PreToolUse and UserPromptSubmit, the hook’s exit code controls whether the action proceeds:
0— Allow.2— Block, and feed the hook’s stderr back to Claude as a reason. Claude will see the message and adjust.- Anything else — Treated as an error, but the action proceeds.
This is what makes hooks a real policy layer rather than just notifications.
A Note on Trust
Hooks run with your user permissions. They execute every event, every session, often without confirmation. Treat .claude/settings.json like any other code — review it before pulling someone else’s project, and never paste hook configs from untrusted sources.
Used well, hooks turn Claude Code from a smart REPL into a disciplined collaborator that respects the rules of your codebase whether the model remembers to or not.