You’ve been there. You add a rule to CLAUDE.md — “always run prettier after editing files” — and Claude follows it, most of the time. Then it doesn’t. The formatter doesn’t run, the lint check gets skipped, and you’re back to reviewing diffs manually.
Hooks fix this. Claude Code hooks are shell commands, HTTP endpoints, or LLM prompts that fire deterministically at specific points in Claude’s agentic loop. Unlike CLAUDE.md instructions, which are advisory, hooks are enforced at the execution layer — Claude cannot skip them.
As of early 2026, Claude Code ships with 21 lifecycle events across four hook types. This article covers the two that matter most for daily workflow: PreToolUse and PostToolUse.
How Hooks Work Architecturally
Claude Code’s agent loop is a continuous cycle: receive input → plan → execute tools → observe results → repeat. Hooks intercept this loop at named checkpoints.
Every hook is defined in .claude/settings.json under a hooks key. A hook entry has three parts: the lifecycle event name, an optional matcher (a regex against tool names), and the handler definition — either a shell command, an HTTP endpoint, or an LLM prompt.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npx prettier --write "$CLAUDE_TOOL_INPUT_FILE_PATH""
}
]
}
]
}
}
That’s it. Every file Claude writes or edits now auto-formats. No CLAUDE.md reminders, no hoping Claude remembers — the formatter runs on every single Write or Edit tool call, period.
PreToolUse: Enforce Before Claude Acts
PreToolUse fires before Claude executes any tool. Your hook receives the full tool call — name, inputs, arguments — and can return one of three signals:
- Exit 0 → allow the tool call to proceed
- Exit 2 → block the tool call; Claude receives your error message and adjusts
- Exit 1 → hook error; Claude proceeds but logs the failure
This makes PreToolUse the right place for guardrails. Here’s a real example: blocking npm in a bun project.
#!/bin/bash
# .claude/hooks/check-package-manager.sh
# Blocks npm commands in projects that use bun
if echo "$CLAUDE_TOOL_INPUT_COMMAND" | grep -qE "^npm "; then
echo "Error: This project uses bun, not npm. Use: bun install / bun run / bun add" >&2
exit 2
fi
exit 0
Wire it in settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/check-package-manager.sh"
}
]
}
]
}
}
Now when Claude tries npm install, the hook exits 2, Claude sees the error message, and it switches to bun install without you intervening. The correction happens in the same turn.
Another production pattern: blocking writes to protected paths.
#!/bin/bash
# Prevent Claude from modifying migration files already run in production
if echo "$CLAUDE_TOOL_INPUT_FILE_PATH" | grep -qE "db/migrations/"; then
echo "Error: Migration files are immutable after deployment. Create a new migration instead." >&2
exit 2
fi
exit 0
PostToolUse: React After Claude Acts
PostToolUse fires after a tool completes successfully. It can’t block execution, but it can provide feedback — and it can run any side-effect you need automatically.
Auto-format every edit:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npx prettier --write "$CLAUDE_TOOL_INPUT_FILE_PATH" 2>/dev/null || true"
}
]
}
]
}
}
Run tests after code changes:
#!/bin/bash
# Run affected tests after any source file edit
FILE="$CLAUDE_TOOL_INPUT_FILE_PATH"
if echo "$FILE" | grep -qE "\.(ts|js|py)$"; then
if [ -f "package.json" ]; then
npx jest --testPathPattern="$(basename ${FILE%.*})" --passWithNoTests 2>&1 | tail -5
fi
fi
Desktop notification on task completion:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification "Claude finished" with title "Claude Code"'"
}
]
}
]
}
}
Environment Variables Available to Hooks
Claude Code exposes context about the triggering tool call through environment variables. The ones you’ll use most:
| Variable | Value |
|---|---|
$CLAUDE_TOOL_NAME | Name of the tool being called (e.g., Edit, Bash, Write) |
$CLAUDE_TOOL_INPUT_FILE_PATH | File path for Edit, Write, Read calls |
$CLAUDE_TOOL_INPUT_COMMAND | Shell command for Bash calls |
$CLAUDE_SESSION_ID | Current session ID — useful for audit logging |
$CLAUDE_TOOL_RESULT_OUTPUT | Output of the tool (PostToolUse only) |
These are injected by Claude Code before your hook runs. You don’t configure them — they’re always there.
The Model Question: Which Claude Runs Agentic Tasks?
One practical consideration for hook-heavy workflows: the default model affects how well Claude responds to hook feedback. As of May 2026:
claude-opus-4-7($5/MTok input, $25/MTok output) — highest agentic coding capability; best at interpreting hook rejection messages and self-correcting without re-askingclaude-sonnet-4-6($3/MTok input, $15/MTok output) — strong balance of speed and reasoning; handles most hook-corrected flows wellclaude-haiku-4-5-20251001($1/MTok input, $5/MTok output) — fastest; may require more explicit hook messages to course-correct reliably
For workflows with complex PreToolUse guardrails — especially ones that provide long error messages with corrective instructions — Opus 4.7 handles the feedback loop most reliably. For simpler PostToolUse automation (formatters, notifications), model choice doesn’t matter; the hook runs regardless.
To configure the model: export ANTHROPIC_MODEL=claude-opus-4-7 before launching Claude Code, or set it in your team’s .env.
Hooks vs. CLAUDE.md: When to Use Each
CLAUDE.md is the right place for context, preferences, and guidance — things you want Claude to know about your project. Hooks are the right place for behavior that must happen every time without exception.
The practical test: if failing to follow the instruction costs you five minutes of manual cleanup, put it in a hook. If it’s a style preference or a reminder about architecture decisions, put it in CLAUDE.md. The two are complementary — you’ll likely end up with both in any mature project setup.
A team that gets this right builds CLAUDE.md as documentation for Claude and hooks as the CI/CD equivalent for the agentic loop.
Getting Started
The fastest path to a working hook setup:
- Create
.claude/settings.jsonin your project root if it doesn’t exist - Add a PostToolUse hook wired to your formatter — this is low-risk and immediately valuable
- Test it by asking Claude to edit a file; the formatter should run automatically
- Add PreToolUse guardrails for any tool calls that have caused problems in the past
The official hooks reference is at code.claude.com/docs/en/hooks — it covers all 21 lifecycle events, HTTP handler format, and the full JSON output schema for hook responses.
Hooks are the difference between Claude Code as a powerful suggestion engine and Claude Code as a reliable automation layer. Once you have a PostToolUse formatter running on every edit, going back feels like working without version control.

Leave a Reply