Hooks: Automated Guardrails
CLAUDE.md tells Claude what to do. Hooks make sure it actually happens - automatically, every time, without relying on the model to remember.
A hook is a script that runs in response to a Claude Code event. Edit a file? Auto-format it. About to run a dangerous command? Block it. Session ending? Run your test suite.
Hook Events
There are 7 events you can hook into:
| Event | When it fires | Common use |
|---|---|---|
| PreToolUse | Before a tool executes | Block dangerous commands |
| PostToolUse | After a tool completes | Auto-format edited files |
| UserPromptSubmit | When user sends a message | Validate input, add context |
| Stop | When Claude finishes responding | Run tests, deploy |
| Notification | When Claude sends a notification | External alerts |
| SubagentStop | When a subagent completes | Aggregate results |
| InstructionsLoaded | After CLAUDE.md files are loaded | Inject dynamic context |
Hook Types
Each hook has a type that determines how it runs:
command - Runs a shell command. The workhorse. Gets JSON on stdin, returns output on stdout.
http - Sends a POST request to a URL. Useful for webhooks and external integrations.
prompt - Asks the AI to make a decision based on the hook context. The model evaluates and decides whether to allow, deny, or modify.
Where Hooks Live
Hooks are configured in your settings files:
.claude/settings.json- shared with team (committed).claude/settings.local.json- personal (gitignored)
Exit Codes
For command hooks, the exit code determines what happens next:
| Exit Code | Meaning | Use when |
|---|---|---|
| 0 | Allow - proceed normally | Everything is fine, or auto-fix succeeded |
| 1 | Deny - block the action | The action is dangerous or violates a rule |
| 2 | Prompt - ask the user to decide | The action might be OK but needs human review |
Matchers
Hooks use glob patterns to match tool names. This is how you target specific tools:
"Edit" # Matches the Edit tool
"Edit|Write" # Matches Edit OR Write
"Bash(npm *)" # Matches Bash calls starting with "npm "
"Bash(rm *)" # Matches Bash calls starting with "rm "
"*" # Matches everything (use sparingly)
The Data Flow
When a hook fires, it receives JSON on stdin with context about the event:
For PreToolUse and PostToolUse, the JSON includes tool_name, tool_input, and (for PostToolUse) tool_output:
{
"tool_name": "Edit",
"tool_input": {
"file_path": "/app/src/utils.ts",
"old_string": "...",
"new_string": "..."
}
}Your hook script reads this JSON, does its work, and exits with the appropriate code.
Practical Examples
Auto-format on save
The most common hook. Every time Claude edits a file, run Prettier on it:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}This reads the edited file path from stdin JSON, pipes it to Prettier. Exit code 0 means "all good, carry on."
Block dangerous commands
Prevent Claude from running destructive commands:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash(rm -rf *)",
"hooks": [
{
"type": "command",
"command": "echo 'BLOCKED: rm -rf is not allowed' && exit 1"
}
]
}
]
}
}Exit code 1 means deny - the command never executes.
Run linter after file edits
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx eslint --fix"
}
]
}
]
}
}Ask for confirmation on force push
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash(git push --force*)",
"hooks": [
{
"type": "command",
"command": "echo 'This will force push. Are you sure?' && exit 2"
}
]
}
]
}
}Exit code 2 prompts the user to decide. The message is shown and they can approve or reject.
The model is probabilistic - it will occasionally forget a rule from CLAUDE.md. Hooks are deterministic. If you have a rule that absolutely must be followed every time, make it a hook, not just a CLAUDE.md instruction.
Hooks run shell commands with your user permissions. A malicious CLAUDE.md in a cloned repo could define hooks that execute arbitrary code. Always review .claude/settings.json when cloning unfamiliar repositories, just like you would review a Makefile or postinstall script.
Combining Multiple Hooks
You can chain multiple hooks on the same event. They run in order - if any hook returns exit code 1, the chain stops:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
},
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx eslint --fix"
}
]
}
]
}
}Exercises
Create an auto-format hook
Set up a PostToolUse hook in .claude/settings.json that runs Prettier on any file Claude edits or writes.
Requirements:
- Should trigger on both
EditandWritetools - Should extract the file path from the hook's stdin JSON
- Should not block Claude if formatting fails
Edit|Write matcher to catch both tools. The file path is at .tool_input.file_path in the JSON.{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null; exit 0"
}
]
}
]
}
}The 2>/dev/null; exit 0 ensures the hook always exits cleanly even if Prettier fails (e.g., on a non-supported file type). This prevents a formatting error from blocking Claude's work.
Create a safety hook that blocks rm -rf
Create a PreToolUse hook that prevents Claude from running any rm -rf command. The hook should output a clear message explaining why it was blocked.
Bash(rm -rf*). Exit code 1 denies the action.{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash(rm -rf *)",
"hooks": [
{
"type": "command",
"command": "echo 'BLOCKED: rm -rf is prohibited. Remove files individually or use git clean for tracked files.' && exit 1"
}
]
}
]
}
}When Claude tries to run rm -rf anything, the hook fires first, prints the message, and exits with code 1. Claude sees the denial message and will find an alternative approach.
Show a live demo of the PostToolUse Prettier hook. Edit a file with intentionally bad formatting, then show how Prettier auto-fixes it immediately after Claude's edit. The instant feedback loop is compelling.