Part 2 · Lesson 9 of 16
Automate Claude Code with Hooks
Event-driven automation. One real example end-to-end.
10 min
Step 1 of 7 · What are hooks?
A hook is a command that Claude Code runs automatically when a specific event happens in your session — before it runs a tool, after it edits a file, when it finishes responding, and more.
Hooks turn "please remember to run the formatter every time" into a guarantee. Instead of asking the agent to behave, you wire a shell command to an event, and the agent's harness runs it deterministically. Two of the most useful events for beginners are:
PreToolUse— fires before a tool call runs, so you can inspect and even block it (great as a safety guard).PostToolUse— fires after a tool call succeeds, so you can react to it (great for auto-formatting a file Claude just edited).
Step 2 of 7 · Where hooks are configured
Hooks live in your settings.json, not in a special "hooks file." The same settings files you'd use for permissions or environment variables hold your hooks:
~/.claude/settings.json— applies to all your projects (user scope)..claude/settings.json— applies to this project and can be committed so your team shares it..claude/settings.local.json— project scope, but git-ignored (personal overrides).
Step 3 of 7 · The shape of a hook config
Every hook config follows the same nested shape: a top-level "hooks" object, keyed by the event name, containing an array of matchers. Each matcher has a "matcher" (which tools it applies to) and a "hooks" array of commands to run:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "./.claude/hooks/guard-bash.sh" }
]
}
]
}
}The "matcher" is matched against the tool name (e.g. Bash, Edit, Write). Use "Edit|Write" to match several, or "*" (or an empty string) to match every tool. Each entry in the inner "hooks" array runs a "command" — and Claude Code passes the event's JSON (tool name, inputs, cwd, and more) to that command's stdin.
Step 4 of 7 · Event 1 — a PreToolUse guard
Let's block a dangerous command before it ever runs. Create the guard script under your project's .claude/hooks/ folder:
mkdir -p .claude/hookscat > .claude/hooks/guard-bash.sh <<'EOF'
#!/bin/sh
# Read the event JSON from stdin and pull out the command.
command=$(jq -r '.tool_input.command' < /dev/stdin)
case "$command" in
*"rm -rf"*)
echo "Blocked: 'rm -rf' is not allowed by the hook." >&2
exit 2 # exit code 2 tells Claude Code to BLOCK the tool call
;;
esac
exit 0 # exit 0 = allow, fall back to normal permission flow
EOFchmod +x .claude/hooks/guard-bash.shThe PreToolUse event passes the proposed Bash command in tool_input.command. Exiting with code 2 tells the harness to deny the call and feed your stderr message back to Claude; exiting 0 lets the normal permission flow continue.
Step 5 of 7 · Event 2 — a PostToolUse formatter
Now add an event that runs after Claude edits a file, so your code is always formatted. Add a PostToolUse matcher for the Edit and Write tools to your .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "./.claude/hooks/guard-bash.sh" }
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "./.claude/hooks/format.sh" }
]
}
]
}
}The PostToolUse event passes the path of the file Claude touched in tool_input.file_path. The script formats just that file:
cat > .claude/hooks/format.sh <<'EOF'
#!/bin/sh
file=$(jq -r '.tool_input.file_path' < /dev/stdin)
case "$file" in
*.ts|*.tsx|*.js|*.json) npx prettier --write "$file" ;;
esac
exit 0
EOF
chmod +x .claude/hooks/format.shStep 6 of 7 · Bonus — a Git pre-commit hook
Claude Code hooks fire inside a session. But you can also use Claude from a Git hook, which fires on git commit regardless of any session. Git looks for a script at .git/hooks/pre-commit and aborts the commit if it exits non-zero.
The key is headless mode: claude -p "<prompt>" runs a single prompt non-interactively and prints the result, which is perfect for scripts and CI.
cat > .git/hooks/pre-commit <<'EOF'
#!/bin/sh
# Run Claude Code headlessly to sanity-check staged files.
claude -p "Review the staged git changes for obvious typos or
secrets. Reply with only the word OK, or a one-line problem." \
--allowedTools "Read,Bash(git diff:*)"
EOF
chmod +x .git/hooks/pre-commitStep 7 of 7 · Checkpoint & Recap
Recap
- Hooks are commands Claude Code runs automatically on session events, configured in
settings.json(user, project, or local scope). PreToolUsefires before a tool runs; exit code 2 blocks the call. Great as a safety guard.PostToolUsefires after a tool succeeds; use it to auto-format or lint the file Claude just changed.- The config nests
"hooks"→ event name →"matcher"+"hooks"array of{ "type": "command", "command": ... }, and the event JSON arrives on the command's stdin. - Git pre-commit hooks (
.git/hooks/) are separate — callclaude -p "<prompt>"for a non-interactive check at commit time.
Frequently asked questions
Where do I configure Claude Code hooks?
In a settings.json file under the "hooks" key. Use ~/.claude/settings.json to apply hooks to every project, .claude/settings.json to share them with your team via the repo, or .claude/settings.local.json for personal, git-ignored overrides. The in-session /hooks command only views hooks — it can't edit them, so change the JSON directly or ask the agent to.
How do I make a hook block a command?
Use a PreToolUse hook. Your command reads the event JSON from stdin (for Bash, the proposed command is in tool_input.command), and if you exit with code 2, Claude Code denies the tool call and shows the agent your stderr message. Exiting 0 allows it and falls back to the normal permission flow.
What's the difference between PreToolUse and PostToolUse?
PreToolUse runs before a tool call, so it can inspect or block it — ideal for safety guards. PostToolUse runs after a tool call succeeds, so it can react to the result — ideal for auto-formatting or linting a file Claude just edited (the path arrives in tool_input.file_path).
Are Claude Code hooks the same as Git hooks?
No. Claude Code hooks live in settings.json and fire on events inside a Claude Code session (like PreToolUse and PostToolUse). Git hooks live in .git/hooks/ and fire on Git events like pre-commit, independent of any session. You can combine them by having a Git pre-commit hook call Claude in headless mode with claude -p "<prompt>".