Claude Code Hooks: A Complete Guide to Automating Your AI Coding Workflow
Claude Code Hooks: A Complete Guide to Automating Your AI Coding Workflow
If you've been using Claude Code for more than a week, you've probably noticed a pattern: you keep telling it the same things. "Run prettier after editing." "Don't touch the .env file." "Run the tests before you stop." These aren't complex instructions — they're rules. And rules shouldn't depend on an LLM remembering to follow them.
That's exactly what Claude Code hooks solve. They're deterministic automation that runs at specific points in Claude Code's lifecycle, executed by the harness itself — not by Claude. If you configure a hook to format code after every edit, it will format code after every edit. No exceptions. No "I forgot."
What Are Hooks, Really?
Hooks are shell commands, HTTP endpoints, or LLM prompts that fire automatically when specific events happen in Claude Code. Think of them like git hooks, but for your AI coding assistant.
The key distinction is reliability:
- CLAUDE.md instructions = Claude should do this (but might forget)
- Hooks = The system will do this (deterministic, every time)
Hooks are configured in settings.json files at three scopes:
- Project (
.claude/settings.json) — shared with your team via git - Local (
.claude/settings.local.json) — personal, gitignored - Global (
~/.claude/settings.json) — applies to all projects
The Hook Lifecycle
Claude Code exposes 27 lifecycle events. Here are the ones that matter most for day-to-day development:
| Event | When It Fires | Best Use Case |
|---|---|---|
PreToolUse | Before a tool runs | Block dangerous commands, validate file edits |
PostToolUse | After a tool succeeds | Auto-format code, log activity |
PermissionRequest | Permission dialog appears | Auto-approve safe operations |
Stop | Claude finishes a response | Run tests, type checking |
Notification | Claude needs attention | Desktop notifications, Slack alerts |
SessionStart | Session begins or resumes | Load environment, re-inject context |
CwdChanged | Directory changes | Reload env vars with direnv |
FileChanged | Watched file changes | React to .env or config changes |
Each event can be filtered with a matcher — a regex pattern that narrows which specific tools or sources trigger the hook.
Configuration Format
Here's the basic structure in .claude/settings.json:
{
"hooks": {
"EventName": [
{
"matcher": "regex_pattern",
"hooks": [
{
"type": "command",
"command": "your-script.sh",
"timeout": 30
}
]
}
]
}
}
Hooks receive JSON context on stdin (tool name, arguments, file paths) and communicate back through exit codes:
- Exit 0 = success, allow the action
- Exit 2 = block the action, with feedback to Claude via stderr
Practical Patterns That Actually Work
I've been running hooks across my projects for months now. Here are the patterns that have stuck.
Pattern 1: Auto-Format After Every Edit
This is the single most impactful hook. Claude writes code, and it's immediately formatted to your project's standards. No more "can you run prettier on that?"
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null",
"timeout": 30
}
]
}
]
}
}
The hook reads the file path from the tool's JSON input and runs prettier on it. Works with any formatter — swap in black for Python, gofmt for Go, rustfmt for Rust.
Pattern 2: Protect Sensitive Files
Some files should never be edited by an AI assistant. Period.
.claude/hooks/protect-files.sh:
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROTECTED=(".env" ".env.local" "secrets.yml" "credentials.json")
for pattern in "${PROTECTED[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH is a protected file" >&2
exit 2
fi
done
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/protect-files.sh"
}
]
}
]
}
}
Exit code 2 blocks the action and sends the stderr message back to Claude as feedback, so it knows why it was blocked and can adjust.
Pattern 3: Desktop Notifications
When Claude finishes a task or needs input, you want to know — especially if you've tabbed away to something else.
macOS:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}
Linux:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "notify-send 'Claude Code' 'Claude needs your attention'"
}
]
}
]
}
}
Pattern 4: Quality Gates Before Stopping
This is where hooks get powerful. Instead of hoping Claude remembers to run tests, you can require it:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "agent",
"prompt": "Run the project's test suite and type checker. If any tests fail or type errors exist, respond with {\"ok\": false, \"reason\": \"description of failures\"}. If everything passes, respond with {\"ok\": true}.",
"timeout": 120
}
]
}
]
}
}
The agent hook type spins up a subagent with full tool access. If it returns ok: false, Claude continues working to fix the issues instead of stopping. This is a game-changer for code quality.
Important: Check the stop_hook_active field in the Stop event input to prevent infinite loops — if your Stop hook triggers Claude to do more work, which triggers another Stop, you'd loop forever.
Pattern 5: Re-inject Context After Compaction
Long sessions hit the context window limit, and Claude compacts (summarizes) the conversation. Critical context can get lost. Fix it:
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "cat .claude/critical-context.md"
}
]
}
]
}
}
Whatever your hook outputs to stdout gets injected into Claude's context. Keep a .claude/critical-context.md file with must-remember information, and it survives compaction automatically.
Pattern 6: Auto-Approve Safe Operations
Tired of clicking "Allow" for read-only operations? Auto-approve them:
{
"hooks": {
"PermissionRequest": [
{
"matcher": "Read|Glob|Grep",
"hooks": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"PermissionRequest\", \"decision\": {\"behavior\": \"allow\"}}}'"
}
]
}
]
}
}
Pattern 7: Environment Management with direnv
If your project uses direnv for environment variables, hooks keep Claude in sync:
{
"hooks": {
"CwdChanged": [
{
"hooks": [
{
"type": "command",
"command": "direnv export bash >> \"$CLAUDE_ENV_FILE\""
}
]
}
],
"FileChanged": [
{
"matcher": ".envrc|.env",
"hooks": [
{
"type": "command",
"command": "direnv export bash >> \"$CLAUDE_ENV_FILE\""
}
]
}
]
}
}
The $CLAUDE_ENV_FILE variable points to a file where you can persist environment variables across the session. Writing to it with direnv export keeps Claude's environment aligned with your shell.
Hook Types: When to Use Each
Claude Code supports four hook types, each suited to different needs:
| Type | Execution | Best For |
|---|---|---|
command | Shell script | Formatting, file protection, notifications |
http | HTTP endpoint | External service integration, webhooks |
prompt | Single LLM turn | Quick yes/no decisions |
agent | Multi-turn subagent | Complex verification (running tests, type checking) |
command handles 90% of use cases. Use agent for Stop hooks that need to run commands and reason about results. Use http for integrating with external services like Slack or custom dashboards. Use prompt for lightweight LLM-based decisions.
Team Configuration: What to Share
Here's how I recommend structuring hooks for a team:
.claude/settings.json (committed to git):
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null"
}
]
}
],
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/protect-files.sh"
}
]
}
]
}
}
These are your team's rules — formatting standards, file protection, security gates. Everyone gets them automatically.
.claude/settings.local.json (gitignored):
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude needs you\" with title \"Claude Code\"'"
}
]
}
]
}
}
Personal preferences like notifications and auto-approvals stay local.
Debugging Hooks
When a hook isn't working:
- Check configuration: Run
/hooksin Claude Code to see all registered hooks - Enable verbose mode: Press
Ctrl+Oduring a session to see hook output in the transcript - Test manually: Pipe sample JSON into your script:
echo '{"tool_name":"Edit","tool_input":{"file_path":"src/app.ts"}}' | .claude/hooks/protect-files.sh
echo $? - Check permissions: Ensure hook scripts are executable (
chmod +x) - Full debug mode: Run
claude --debugfor complete execution traces
Common Pitfalls
Shell profile noise: If your ~/.zshrc or ~/.bashrc prints output (welcome messages, fortune quotes), it can interfere with hook JSON parsing. Guard with [[ $- == *i* ]] checks.
Stop hook loops: A Stop hook that causes Claude to do more work will trigger another Stop event. Always check stop_hook_active in the input and return ok: true if it's already active.
Matcher case sensitivity: Tool names are case-sensitive. It's Bash, not bash. It's Edit, not edit.
Parallel hook conflicts: Multiple hooks on the same event run in parallel. If two PostToolUse hooks both modify the same file, only the last one to finish wins. Keep hooks independent.
What's Next
Hooks are one piece of the Claude Code customization puzzle. Combined with CLAUDE.md patterns, MCP server integrations, and custom slash commands, you can build a development environment where AI assistance is deeply integrated with your team's specific workflow.
The teams I've seen get the most out of AI coding tools aren't the ones with the best prompts — they're the ones who've automated the repetitive parts so they can focus on the creative work. Hooks are the foundation for that.
Related Posts
- Claude Code Template — Starter template for AI-assisted development with CLAUDE.md patterns
- PromptConduit: Building Analytics for AI Coding Assistants — Analytics suite for understanding your AI coding patterns
- AI Agents and the Future of Development — Lessons from a hackathon on agent-led development