Claude Code in Action · lesson 5 of 6
Claude Code Hooks
Shell commands that fire on lifecycle events. The harness — not the model — runs them. This is how you build deterministic guardrails Claude can't ignore.
Event types
| Event | Fires when |
|---|---|
UserPromptSubmit | User submits a prompt |
PreToolUse | Before a tool runs (can block) |
PostToolUse | After a tool runs |
Stop | Main agent finishes a turn |
SubagentStop | A subagent finishes |
Notification | Display-only notifications |
SessionStart | New session begins |
Configuration
In settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{"type": "command", "command": "pnpm lint --fix"}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{"type": "command", "command": "./scripts/check-bash.sh"}
]
}
]
}
}
Hooks receive a JSON payload over stdin with event details. They can:
- Print to stdout — shown to the user.
- Exit code
0— allow. - Exit code
1(UserPromptSubmit-blocking) or2(PreToolUse-blocking) — block the action; stdout is sent to Claude as the reason.
Typical uses
- Auto-format / auto-lint on
Edit|Write. - Run tests on
Stop(catches regressions before turn ends). - Block dangerous bash (
rm -rf /, etc.) onPreToolUse. - Notify Slack on
Stopwhen CI builds. - Audit log every tool call.
Hook vs permission
- Permission = static yes/no based on the call.
- Hook = dynamic — can inspect actual command, run other tools, format reasons.
Use permissions for blanket policies, hooks for context-dependent checks.
CCA-F angle
The CI/CD scenario tests hook patterns: how to fail a Claude Code run when tests don't pass, how to enforce conventional-commit messages, how to scan commits for secrets before push. Hooks are the answer.