You've been using Claude Code for three weeks. It's phenomenal. It reads your codebase, understands your patterns, and ships code faster than your team ever has. But then you notice something: you're reviewing every single file change. You catch it before it breaks production—a missing error handler, a race condition hiding in PostToolUse, an API key logged to stdout.
Claude is powerful, but it's not deterministic. Sometimes it gets it right. Sometimes it needs guardrails. And the problem is that advisory rules in CLAUDE.md? They don't always stick. In long conversations, they deprioritize. Your carefully written guidelines fade into the noise.
That's where hooks come in. Hooks aren't suggestions. They're gates.
Claude Code hooks are user-defined commands—shell scripts, HTTP endpoints, LLM prompts, or multi-turn agents—that execute at specific lifecycle points in Claude's workflow. They fire every time a matching event occurs. No exceptions. No deprioritization.
The mental model is simple: Git hooks protect you from yourself. Claude Code hooks protect you from your AI agent.
There are 22 hook events across the full lifecycle:
Each hook can be a command (shell script), http (POST to your endpoint), prompt (single-turn LLM eval), or agent (multi-turn with tool access). Exit code 0 means proceed. Exit code 2 blocks the action. Everything else proceeds but gets logged.
Hooks live in your .claude/settings.json file—either project-level (shared with your team) or user-level (~/.claude/settings.json for personal workflows). They support regex matchers to filter which tools or events actually trigger them.
CLAUDE.md gives you a way to write instructions. It's a conversation tool. But here's what actually happens in real projects: Claude is halfway through a 40-file refactor. You wrote a rule six hours ago about never editing package-lock.json. It's day 6 of the conversation. The rule is buried. Claude reaches for package-lock.json and does the wrong thing.
Hooks are different. They execute regardless of conversation depth, regardless of how much context has been compacted, regardless of how many times Claude has been told to "prioritize speed over safety."
That's enforcement.
The problem: Claude writes valid JavaScript but doesn't match your team's formatting standards. Tabs vs. spaces, bracket style, import order. Someone runs prettier and suddenly every file looks different. Git blame becomes noise.
The solution: PostToolUse hook that auto-formats after any Edit or Write.
{
"hooks": [
{
"event": "PostToolUse",
"matcher": "Edit|Write",
"type": "command",
"command": "prettier --write $FILE_PATH"
}
]
}
This fires after every file edit. Claude writes the code. Prettier formats it. The formatted version is what gets committed. No style drift. No arguments.
The problem: .env files, package-lock.json, .git/config, terraform state files—these should never be touched by an AI agent. But Claude doesn't always understand the consequences.
The solution: PreToolUse hook that blocks Edit/Write to critical paths.
{
"hooks": [
{
"event": "PreToolUse",
"matcher": "Edit",
"type": "command",
"command": "bash -c 'FILE_PATH=$1; [[ $FILE_PATH =~ \\.(env|lock)$ ]] || [[ $FILE_PATH =~ /\\.git/ ]] && exit 2 || exit 0' -- $FILE_PATH"
}
]
}
Exit code 2 blocks the action. Claude gets a clear refusal. No guessing.
The problem: Claude needs to read a config file or run a test. It asks for permission. You're in the middle of something and just click yes on everything. Now Claude's running random shell commands.
The solution: PermissionRequest hook that auto-approves safe patterns and blocks risky ones.
{
"hooks": [
{
"event": "PermissionRequest",
"type": "prompt",
"prompt": "You are a permission evaluator. Evaluate this permission request: {request}. Return JSON with {approved: boolean, reason: string}. Auto-approve: read config, run tests, format code. Block: delete files, modify git, access secrets."
}
]
}
The hook receives the permission request, evaluates it with an LLM, and returns a decision. Claude only acts on approvals. For your team, this is a governance layer. For solo developers, it's a guardrail.
The problem: Claude finishes a feature. Runs tests locally (maybe). Says the task is done. You deploy. Tests fail in CI. You're back at square one debugging what went wrong.
The solution: Stop hook that validates the task completion criteria.
{
"hooks": [
{
"event": "Stop",
"type": "agent",
"agent": "You are a test verification agent. Run the test suite for the current task. If tests pass, return {proceed: true}. If tests fail, return {proceed: false, reason: 'Test failure details'}. Claude will only stop if you return proceed: true."
}
]
}
This is multi-turn. The agent runs tests, parses output, decides if the task is actually complete. Claude can only stop if the agent gives the green light. No false finishes.
Without hooks: Claude generates code → You review → You catch a mistake → You ask Claude to fix it → You review again → Deploy.
With hooks: Claude generates code → Hooks auto-format, block bad patterns, verify safety → Claude stops only when hooks approve → Deploy.
The difference is that in the first scenario, you're the quality gate. In the second, your hooks are. And hooks don't get tired. They don't miss edge cases because they've been reviewing for six hours.
A concrete example: Recipe 1 (auto-format) eliminates style review cycles. Recipe 2 (block protected files) eliminates the class of bugs where Claude touches files it shouldn't. Recipe 4 (test verification) eliminates shipping incomplete features. Each hook removes a class of problems from your review burden.
Not every quality gate needs a hook. Here's how to decide:
Hook it if:
Don't hook it if:
Default to CLAUDE.md if: It's contextual advice, architectural guidance, or team preferences that don't need hard enforcement.
Default to hooks if: It's a hard requirement that applies 100% of the time.
Your `.claude/settings.json` can live two places:
Project-level (./.claude/settings.json): Shared with your team. Enforces org standards. Everyone on the project follows the same hooks. Good for code formatting, protected file patterns, test verification.
User-level (~/.claude/settings.json): Your personal workflow. Doesn't affect the team. Good for personal preferences, notification patterns, or experimental hooks you're testing.
A mature team uses project-level hooks for non-negotiables (never edit package-lock.json) and user-level hooks for personal preferences (notify me when Claude needs user input).
Pitfall 1: Hooks that are too strict. You block too many actions and Claude spends time asking for permission on everything. Solution: Use matchers to narrow the scope. Block specific file patterns, not entire tool categories.
Pitfall 2: Hooks that fail silently. You write a hook, it breaks, and Claude proceeds anyway. Solution: Log everything. Use exit codes correctly. Test hooks locally before deploying to shared projects.
Pitfall 3: Hooks that are too slow. Your PermissionRequest hook calls an external API and adds 2 seconds to every permission. Claude gets slow. Solution: Keep hooks fast. Use simple shell commands where possible. Reserve agent-based hooks for rare, high-stakes decisions.
Pitfall 4: Hooks that drift from reality. You write a hook to verify tests pass, but then your test suite breaks. The hook blocks everything. Solution: Monitor and maintain hooks like any other production code. Use version control. Test regularly.
Here's where hooks shine: they enable longer conversations without quality degradation.
Without hooks, you get nervous around day 3 of a multi-day refactor. CLAUDE.md rules are starting to slip. You increase manual review. You interrupt Claude more. Productivity tanks.
With hooks, you trust the gates. Claude can work for days. Rules don't degrade. The system enforces what matters. You review outcomes, not every keystroke.
That's the real win. Not that hooks prevent bugs (though they do). But that they let you give Claude more autonomy without sacrificing safety.
Start small. Pick one hook. My recommendation: Recipe 1 (auto-format). It's safe, high-frequency, and removes busywork from your review process.
Add it to your project's `.claude/settings.json`:
{
"hooks": [
{
"event": "PostToolUse",
"matcher": "Edit|Write",
"type": "command",
"command": "prettier --write $FILE_PATH"
}
]
}
Next week, add Recipe 2 (protect critical files). Then Recipe 4 (test verification) when you're comfortable.
By month two, you'll have a full pipeline of hooks. Code formatting is automatic. Protected files are blocked. Tests must pass. Permissions are intelligently approved.
And you're not babysitting Claude anymore. You're orchestrating it.