Hooks
You write into your CLAUDE.md: "run Prettier after every file edit." Most sessions, Claude does. One session, it does not. You only notice when the PR lands with ugly diffs and a grumpy reviewer. The difference between "Claude usually does this" and "this always happens" is the difference between a prompt and a hook. Hooks are the last — and the most uncompromising — of the extension mechanisms.
Hooks are deterministic. That is the point.
A hook is a shell command that runs at a specific point in Claude Code's lifecycle, every single time, with no exceptions. Everything else in this course is probabilistic. CLAUDE.md instructions, skills, MCP tool calls, subagent behaviour — Claude decides whether and how to apply them based on context. That works most of the time. Hooks work all of the time.
If something needs to happen every time without fail, it does not belong in a prompt. It belongs in a hook.
Common use cases
- Auto-formatting after file edits — run Prettier, gofmt, Ruff, or whatever your project uses, guaranteed.
- Logging all executed commands for compliance — audit trails your legal team can point to.
- Blocking dangerous operations — writes to production configs,
rm -rf, commits to main. - Notifications when Claude finishes a task — ping yourself, post to Slack, open a desktop notification.
The five lifecycle events
Hooks attach to specific events. There are five, and their names tell you when they fire.
| Event | Fires when |
|---|---|
| PreToolUse | Before a tool call is executed |
| PostToolUse | After a tool call completes |
| UserPromptSubmit | When you submit a prompt, before Claude processes it |
| Stop | When Claude finishes responding |
| Notification | When Claude sends a notification |
You pick an event, optionally attach a matcher that narrows which tools it applies to, and provide a command to run. Configuration lives in settings.json — edit it directly, or run /hooks inside a Claude Code session for a guided flow.
Walkthrough: auto-formatting after edits
The most common hook in the wild. Fire a PostToolUse hook on any file-modifying tool, check the file extension, run the right formatter.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|MultiEdit|Write",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/scripts/format.sh"
}
]
}
]
}
}
The matcher Edit|MultiEdit|Write means the hook fires whenever Claude modifies a file — regardless of which specific write tool it used. Inside format.sh, you check the file extension and shell out to the right formatter: Prettier for TypeScript, gofmt for Go, Ruff for Python, whatever your project uses.
Note the $CLAUDE_PROJECT_DIR — more on that in a moment.
Blocking with PreToolUse
PreToolUse is where hooks turn from automation into enforcement. A PreToolUse hook receives the tool name and input as JSON on stdin, then uses its exit code to decide what happens next.
Exit code 0 — proceed normally. The tool call runs.
Exit code 2 — block the action. Whatever you wrote to stderr gets fed back to Claude as feedback, so Claude knows why it was blocked and can adjust its plan accordingly. This is the setting you want for hard rules.
Any other exit code — a non-blocking error. Shown to you but does not stop anything. Useful for warnings.
The blocking pattern is how you enforce hard rules that no prompt can override:
- Block writes to a production config directory.
- Block bash commands that contain
rm -rf. - Block commits to
main.
Anything your team needs to be guaranteed, not suggested, is a PreToolUse hook. Prompts say "please." Hooks say "no."
Sharing hooks with your team
Hooks configured in .claude/settings.json are project-level, and you can check them into your repo. Once you do, every developer who clones the repo gets the same hooks automatically — the same enforcement, the same guarantees, applied to everyone without anyone having to remember to configure them.
Use the CLAUDE_PROJECT_DIR environment variable in your commands to reference scripts stored in your project. This makes your hook commands portable — they work no matter what directory Claude Code is currently operating in.
A reflection — the course so far
You have now seen all five extension mechanisms. Think about which one you would reach for in each of these situations:
- "I keep telling Claude my stack is Next.js 15 and Drizzle ORM." → CLAUDE.md. It applies to every conversation and costs nothing to read.
- "I need to investigate how the payment system works without blowing up my context." → Subagent. Get the answer without the journey.
- "Every PR review, I paste the same five-point review checklist." → Skill. Task-specific, loads on demand.
- "I want Claude to query our Linear tickets when I ask about current work." → MCP server. Brings external systems into reach.
- "Prettier must run after every file edit, no exceptions." → Hook. Deterministic, non-negotiable.
Each tool has a job. The skill is knowing which lever to reach for when.
The closing principle
One sentence to carry with you. If something needs to happen every time without fail — don't put it in a prompt. Put it in a hook.
Key Takeaways
- 1Hooks are deterministic: they always run. That is what separates them from CLAUDE.md, skills, MCP, and every other mechanism that relies on Claude's judgement.
- 2There are five lifecycle events: PreToolUse, PostToolUse, UserPromptSubmit, Stop, and Notification — pair an event with a matcher and a command in settings.json.
- 3PostToolUse with an Edit|MultiEdit|Write matcher is the canonical pattern for auto-formatting after every file change.
- 4PreToolUse hooks can block actions by exiting with code 2 — the stderr message is fed back to Claude so it understands why and can adjust.
- 5Commit hooks to .claude/settings.json so the whole team shares the same guarantees; use $CLAUDE_PROJECT_DIR for portable script references.
- 6The course's closing principle: if something needs to happen every time without fail, don't put it in a prompt — put it in a hook.