The Complete Guide to Claude Code Hooks: Deterministic Automation
Introduction
In my previous CLAUDE.md guide, I mentioned that CLAUDE.md tells Claude "what to do" while settings.json tells Claude "how to do it." And in the advanced guide, I showed a few Hook examples briefly.
But Hooks deserve their own dedicated post. They solve a fundamental problem: CLAUDE.md is advisory — Claude might not follow it. Hooks are deterministic — they always execute.
This post covers Hooks thoroughly.
What Are Hooks
In one sentence: Hooks are Shell commands that auto-execute at specific lifecycle points in Claude Code.
Think of them as the Claude Code equivalent of Git Hooks. Git has pre-commit and post-commit; Claude Code has PreToolUse and PostToolUse.
The key difference is determinism:
| CLAUDE.md | Hooks | |
|---|---|---|
| Nature | Natural language instructions | Shell commands |
| Execution | Claude "tries its best" to follow | System auto-executes, 100% guaranteed |
| On failure | Claude might ignore it | Non-zero exit code blocks the operation |
| Best for | Coding style, architecture conventions | Formatting, linting, security blocking |
| Analogy | Employee handbook | Access control system |
Rule of thumb: If violating a rule has serious consequences, use a Hook. If it's "preferably follow but occasional exceptions are fine," use CLAUDE.md.
Event Types
Hooks support the following event types:
| Event | Trigger | Typical Use |
|---|---|---|
PreToolUse | Before a tool call | Block dangerous operations, validate params, permission checks |
PostToolUse | After a tool call | Auto-format, lint, logging |
Notification | When Claude sends a notification | Forward to Slack, system notifications |
Stop | When Claude finishes a response | Auto-run tests, clean up temp files |
SubagentStop | When a sub-agent completes | Validate sub-agent output quality |
The most commonly used are PreToolUse and PostToolUse, covering 90% of use cases.
Configuration
Interactive Setup
The simplest approach — type this in Claude Code:
/hooksThis opens an interactive UI that guides you through selecting event types, matchers, and commands. Great for quickly adding a single Hook.
Manual settings.json
More flexible — edit the config file directly:
~/.claude/settings.json → Global Hooks (all projects)
project-root/.claude/settings.json → Project-level Hooks (team-shared)
JSON structure:
{
"hooks": {
"EventType": [
{
"matcher": "matching rule",
"command": "shell command to execute"
}
]
}
}Full example:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "echo 'Checking command safety...'"
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "npx prettier --write \"$CLAUDE_FILE_PATH\""
}
]
}
}Configuration Hierarchy
Like CLAUDE.md, Hooks have levels too:
~/.claude/settings.json → Global (personal preferences)
.claude/settings.json → Project-level (team-shared)
Hooks from both levels are merged and all execute — they don't override each other. Both global and project-level Hooks will fire.
Matcher Rules
The matcher determines which tool calls trigger a Hook.
Matching Methods
| Method | Syntax | Example |
|---|---|---|
| Exact match | Tool name | "Bash" |
| Multi-match | Pipe-separated | "Edit|Write" |
| Match all | Empty or omitted | "" or omit matcher |
Common Tool Names
| Tool Name | Description |
|---|---|
Bash | Execute shell commands |
Edit | Edit files (partial modification) |
Write | Write files (full overwrite) |
Read | Read files |
Glob | File pattern search |
Grep | Content search |
WebFetch | HTTP requests |
WebSearch | Web search |
Matching Tips
// Match all file write operations
"matcher": "Edit|Write"
// Only match shell commands
"matcher": "Bash"
// Match all tools (omit matcher or leave empty)
"matcher": ""Hook Input and Output
This is the most powerful part of Hooks — they don't just "run a command." They receive context and can return decisions.
Input (stdin)
Each Hook receives a JSON object via stdin when executed:
{
"session_id": "abc123",
"tool_name": "Edit",
"tool_input": {
"file_path": "/src/app.tsx",
"old_string": "...",
"new_string": "..."
}
}You can read this JSON in your Hook command for conditional logic.
Output (exit code)
| Exit Code | Meaning | Effect |
|---|---|---|
0 | Pass | Operation proceeds |
2 | Block | Operation cancelled, stdout shown as reason |
| Other non-zero | Error | Hook itself errored, operation continues (not blocked) |
Output (stdout JSON)
Beyond exit codes, Hooks can return JSON via stdout for precise control:
{
"decision": "block",
"reason": "Detected rm -rf command, blocked for safety"
}{
"decision": "approve"
}Complete Flow
Claude wants to call a tool
↓
System checks PreToolUse Hooks
↓
Matching Hook receives stdin JSON
↓
Hook executes command, returns exit code + stdout
↓
exit 0 → Tool executes
exit 2 → Blocked, reason displayed
↓
Tool execution completes
↓
System checks PostToolUse Hooks
↓
Matching Hook executes (format, lint, etc.)
Practical Examples: General Development
Auto-Format on Save
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "npx prettier --write \"$CLAUDE_FILE_PATH\""
}
]
}
}Every time Claude edits or creates a file, Prettier auto-formats it. No more writing "please use Prettier" in CLAUDE.md.
Block Dangerous Commands
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -qE 'rm -rf|git push --force|git reset --hard|drop table|DROP TABLE'; then echo '{\"decision\": \"block\", \"reason\": \"Dangerous operation blocked: destructive command detected\"}'; exit 2; fi"
}
]
}
}Far more reliable than writing "don't run dangerous commands" in CLAUDE.md — CLAUDE.md is a suggestion, a Hook is a gate.
Auto-Lint After Edit
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "ext=\"${CLAUDE_FILE_PATH##*.}\"; if [ \"$ext\" = \"ts\" ] || [ \"$ext\" = \"tsx\" ]; then npx eslint --fix \"$CLAUDE_FILE_PATH\" 2>/dev/null; fi"
}
]
}
}Runs ESLint auto-fix only on TypeScript files.
Notification on Task Completion
{
"hooks": {
"Notification": [
{
"matcher": "",
"command": "osascript -e 'display notification \"$CLAUDE_NOTIFICATION\" with title \"Claude Code\"'"
}
]
}
}Shows a macOS system notification. On Windows, use PowerShell's New-BurntToastNotification. On Linux, use notify-send.
Practical Examples: Flutter Development
Flutter projects have their own toolchain, and Hooks integrate perfectly. Here's a complete Flutter development Hooks setup:
Complete Configuration
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -qE 'setState\\('; then echo '{\"decision\": \"block\", \"reason\": \"setState is prohibited. Use Riverpod for state management.\"}'; exit 2; fi"
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "if echo \"$CLAUDE_FILE_PATH\" | grep -qE '\\.dart$'; then dart format \"$CLAUDE_FILE_PATH\"; fi"
},
{
"matcher": "Edit|Write",
"command": "if echo \"$CLAUDE_FILE_PATH\" | grep -qE '\\.dart$'; then dart analyze \"$CLAUDE_FILE_PATH\" 2>&1 | head -20; fi"
}
],
"Stop": [
{
"matcher": "",
"command": "flutter test --reporter compact 2>&1 | tail -5"
}
]
}
}Hook-by-Hook Breakdown
Block setState usage:
{
"matcher": "Bash",
"command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -qE 'setState\\('; then echo '{\"decision\": \"block\", \"reason\": \"setState is prohibited. Use Riverpod for state management.\"}'; exit 2; fi"
}If your project standardizes on Riverpod, this Hook blocks setState at the source. Much stronger enforcement than writing "don't use setState" in CLAUDE.md.
Auto-format Dart files:
{
"matcher": "Edit|Write",
"command": "if echo \"$CLAUDE_FILE_PATH\" | grep -qE '\\.dart$'; then dart format \"$CLAUDE_FILE_PATH\"; fi"
}Auto-runs dart format after every .dart file edit, ensuring consistent code style.
Auto-analyze Dart files:
{
"matcher": "Edit|Write",
"command": "if echo \"$CLAUDE_FILE_PATH\" | grep -qE '\\.dart$'; then dart analyze \"$CLAUDE_FILE_PATH\" 2>&1 | head -20; fi"
}Runs dart analyze immediately after edits — catch issues at write time, not build time.
Auto-run tests after response:
{
"matcher": "",
"command": "flutter test --reporter compact 2>&1 | tail -5"
}Automatically runs tests after every Claude response, ensuring changes don't break existing functionality. tail -5 shows just the summary.
Flutter Hooks + CLAUDE.md Together
Hooks and CLAUDE.md work best in combination:
CLAUDE.md Hooks
───────────────────────────── ─────────────────────────────
"Use Riverpod, not setState" → Block setState (enforced)
"Follow dart format style" → Auto-run dart format (guaranteed)
"Must pass analyze before commit" → Auto-run dart analyze (instant feedback)
"All changes must pass tests" → Auto-run flutter test (continuous verification)
CLAUDE.md tells Claude why to do it. Hooks ensure it actually happens.
Advanced Tips
Hook Types
Hooks in settings.json come in three types:
| Type | Description |
|---|---|
command | Execute a shell command (default, most common) |
prompt | Inject additional prompt text to Claude |
agent | Launch a sub-agent to handle it |
command covers most scenarios. prompt type is useful for injecting extra context before specific operations.
Environment Variables
These environment variables are available in Hook commands:
| Variable | Description | Available In |
|---|---|---|
$CLAUDE_FILE_PATH | Path of the file being operated on | PostToolUse (Edit/Write) |
$CLAUDE_TOOL_INPUT | Tool input parameters (JSON) | PreToolUse |
$CLAUDE_NOTIFICATION | Notification content | Notification |
$CLAUDE_SESSION_ID | Current session ID | All events |
Debugging Hooks
When a Hook isn't working, troubleshoot step by step:
- Check JSON syntax — A malformed settings.json breaks the entire config
- Check matcher — Tool names are case-sensitive:
bash≠Bash - Test the command manually — Run the Hook command standalone in your terminal
- Check exit codes —
exit 2blocks;exit 1just errors without blocking - Check stderr — Hook stderr output is displayed in Claude Code
# Manually test a Hook command
echo '{"tool_name":"Bash","tool_input":"rm -rf /"}' | your-hook-command
echo $? # Check exit codePerformance Considerations
- Hooks execute synchronously and block Claude's operations
- Avoid time-consuming operations in Hooks (like a full
flutter testsuite) PostToolUseformatting is typically fast (< 1 second) — safe to use freelyStopevents are better for slightly longer operations (like running tests), since Claude has already finished responding
FAQ
Q: How should Hooks and CLAUDE.md work together? A: CLAUDE.md handles "soft rules" (coding style, architecture preferences). Hooks handle "hard rules" (formatting, security blocking). They complement each other — they're not replacements.
Q: What happens when a Hook fails?
A: Depends on the exit code. exit 2 blocks the operation. exit 1 or other non-zero values just report an error — the operation continues. This design prevents Hook bugs from disrupting your workflow.
Q: Can a Hook modify Claude's tool input?
A: Not directly. But a PreToolUse Hook can block the operation with exit 2, and Claude will adjust its behavior based on the block reason.
Q: What happens when multiple Hooks match the same event?
A: They execute in configuration order. If any Hook returns exit 2, the operation is blocked.
Q: Can I use Node.js scripts in Hook commands? A: Absolutely. Hook commands are regular shell commands — you can call any executable:
{
"command": "node .claude/hooks/check-security.js"
}Put complex logic in script files and have the Hook command just call them. Much easier to maintain.
Q: What about team members who don't use Claude Code?
A: .claude/settings.json only affects Claude Code users. It's completely transparent to everyone else. Safe to commit to Git.
Conclusion
Hooks are the core mechanism for Claude Code automation. Two key takeaways:
- CLAUDE.md is advisory, Hooks are guaranteed — Enforce critical rules with Hooks
- PreToolUse blocks, PostToolUse fixes — Block dangerous operations before execution, fix code quality after execution
Spending 15 minutes setting up Hooks saves you from manual checks in every Claude Code session.
Further Reading
- The Complete Guide to CLAUDE.md — My deep dive into CLAUDE.md
- Claude Code Advanced Guide — Claude Code advanced tips roundup
- Claude Code Hooks Documentation — Official Hooks documentation