If you've been using Claude Code for a while, you probably have a rhythm: open terminal, type claude, describe what you want, review the output. It works. But there's a layer most developers never touch that can make Claude Code feel less like a tool you use and more like an automated teammate running in the background β hooks.
Claude Code hooks let you intercept the agent's lifecycle at specific points and inject your own logic. Think of them as middleware for AI-assisted development. They're defined in plain JSON, run as shell commands, and can do anything from blocking a dangerous operation to firing off a desktop notification when a long-running task finishes. This guide covers what hooks are, how to set them up, and four production-ready examples you can drop into your workflow today.
What Are Claude Code Hooks?
Hooks are user-defined shell commands that Claude Code runs at specific points in its execution lifecycle. There are currently four hook types:
- PreToolUse - fires before Claude executes a tool (bash, file read/write, etc.)
- PostToolUse - fires after a tool completes
- Notification - fires when Claude wants to surface something to the user
- Stop - fires when the agent finishes its turn
Each hook can exit with a status code that controls what Claude does next. Exit 0 means proceed normally. Exit 2 from a PreToolUse hook blocks the tool call and sends your stderr output back to Claude as context - so you can literally tell Claude 'don't do that, here's why' and it will adapt.
Hooks are configured in ~/.claude/settings.json (global) or .claude/settings.json at the repo level (project-specific). The project-level file takes precedence, which means you can define organization-wide safety guardrails globally, then layer project-specific automation on top.
Setting Up Your First Hook
Open or create ~/.claude/settings.json and add a hooks key:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/pre-bash.sh"
}
]
}
]
}
}
The matcher field accepts tool names like Bash, Write, Edit, or Read. You can also use * to match every tool. The hook receives the tool input as JSON on stdin, so your script can inspect what Claude is about to do before allowing it.
Here's the minimal scaffold for a PreToolUse hook script:
#!/usr/bin/env bash
# ~/.claude/hooks/pre-bash.sh
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.command // empty')
echo "Hook received: $CMD" >&2
exit 0
Make it executable with chmod +x ~/.claude/hooks/pre-bash.sh and you're live.
Hook Example 1: Block Dangerous Commands
The most immediately useful hook is a safety gate that prevents Claude from accidentally running destructive commands. This is especially important when you're iterating fast and Claude has broad filesystem access.
#!/usr/bin/env bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.command // empty')
DANGER_PATTERNS=("rm -rf /" "DROP TABLE" "truncate.*production" "> /dev/sda" "mkfs\.")
for pattern in "${DANGER_PATTERNS[@]}"; do
if echo "$CMD" | grep -qiE "$pattern"; then
echo "BLOCKED: matches danger pattern '$pattern'" >&2
exit 2
fi
done
exit 0
When Claude tries to run something that matches, it sees your error message and either explains why it can't proceed or suggests an alternative. This is much more useful than a blunt permission denial - Claude gets context and can adapt.
Hook Example 2: Auto-Lint After File Edits
A PostToolUse hook on Write or Edit lets you enforce code quality automatically. Every time Claude writes a file, run ESLint, Prettier, or whatever your project uses:
#!/usr/bin/env bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.path // empty')
if [[ "$FILE_PATH" =~ \.(js|ts|jsx|tsx)$ ]]; then
cd "$(dirname "$FILE_PATH")" || exit 0
if ! npx eslint --fix "$FILE_PATH" 2>&1; then
echo "ESLint found issues in $FILE_PATH that could not be auto-fixed." >&2
fi
fi
exit 0
Wire it up in settings.json:
{
"hooks": {
"PostToolUse": [
{ "matcher": "Write", "hooks": [{ "type": "command", "command": "~/.claude/hooks/post-write.sh" }] },
{ "matcher": "Edit", "hooks": [{ "type": "command", "command": "~/.claude/hooks/post-write.sh" }] }
]
}
}
Now every file Claude touches gets linted immediately. If there are unfixable issues, they surface as context Claude can act on in the next turn.
Hook Example 3: Command Audit Log
When you're running Claude Code in a shared dev environment or on production-adjacent infrastructure, you want an audit trail. A simple PreToolUse hook can log every bash command Claude runs with a timestamp:
#!/usr/bin/env bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.command // empty')
LOG_FILE="$HOME/.claude/audit.log"
echo "$(date -u +"%Y-%m-%dT%H:%M:%SZ") CMD: $CMD" >> "$LOG_FILE"
exit 0
Drop this in ~/.claude/settings.json globally so it covers every Claude session. If you're running a home lab server - something like a Beelink EQ12 Mini PC (~$189) running Proxmox or lightweight Linux - you can ship this log to Loki, Datadog, or any aggregator over your local network. The EQ12's N100 processor handles Claude Code sessions with room to spare, and its low idle power draw makes it a solid always-on dev node.
Hook Example 4: Desktop Notification on Task Complete
Long-running Claude Code tasks can take minutes. The Stop hook fires when Claude's turn ends, which makes it perfect for a system notification so you can step away and come back when it's done:
#!/usr/bin/env bash
INPUT=$(cat)
STOP_REASON=$(echo "$INPUT" | jq -r '.stop_reason // "completed"')
# macOS
if command -v osascript &>/dev/null; then
osascript -e "display notification \"Claude finished ($STOP_REASON)\" with title \"Claude Code\" sound name \"Glass\""
fi
# Linux
if command -v notify-send &>/dev/null; then
notify-send "Claude Code" "Task finished: $STOP_REASON"
fi
# Windows (WSL)
if command -v powershell.exe &>/dev/null; then
powershell.exe -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('Claude finished: $STOP_REASON', 'Claude Code')"
fi
exit 0
Wire this up under the Stop hook type in your global settings. It's zero overhead during normal operation and immediately useful the first time you kick off a large refactor and want to focus on something else.
Organizing Hooks at Scale
As your hook library grows, structure matters. A layout that scales well:
~/.claude/
+-- settings.json # Global hook registration
+-- hooks/
+-- pre-bash.sh # Safety gate
+-- post-write.sh # Auto-lint
+-- audit-log.sh # Audit trail
+-- on-stop.sh # Notifications
For teams, commit a .claude/settings.json at the repo root with project-specific hooks. Keep hooks with credentials or external API calls in the global ~/.claude/ directory and out of version control.
If you're building a serious home lab AI stack - multiple Claude sessions, Ollama serving local models, n8n orchestrating workflows - you'll want solid hardware. Adding 32GB of DDR4 RAM (~$59) prevents memory pressure when running multiple models and dev tools simultaneously. For storing model weights locally, a WD Black 2TB NVMe SSD (~$129) gives you the throughput to load 7B-13B parameter models in seconds rather than minutes.
Debugging Hooks
Hooks that fail silently are the worst. A few practices that help:
Always write diagnostic output to stderr (echo "..." >&2). Claude Code captures stderr from hooks and can surface it in the conversation. If your hook is misbehaving, Claude will tell you what it saw.
Test hooks independently before wiring them into Claude:
# Simulate what Claude sends to a PreToolUse Bash hook
echo '{"command": "ls -la /tmp"}' | ~/.claude/hooks/pre-bash.sh
echo "Exit code: $?"
Use set -euo pipefail at the top of your hook scripts to catch uninitialized variables and pipeline failures. A hook that crashes with exit 1 is treated as a non-blocking error - Claude proceeds anyway. Exit 2 is the only code that blocks tool execution.
Hooks vs. Custom Slash Commands
It's worth clarifying when to reach for hooks vs. Claude Code's custom slash commands. Slash commands are conversational shortcuts - you type /deploy and Claude runs your deploy workflow. Hooks are automated and invisible to the conversation - they fire without any user action.
Use hooks for enforcement (lint, safety gates, audit), observability (logging, notifications), and automatic side effects (formatting, tagging). Use slash commands for explicit workflows you initiate, multi-step procedures that need human checkpoints, and anything that benefits from Claude's reasoning before acting.
The two complement each other well. A slash command can trigger a deployment, and a PostToolUse hook on Bash can automatically log every command that deployment runs.
What's Next for Claude Code Automation
The hooks system is still relatively new, and the community is only beginning to explore what's possible. Some directions worth watching:
Hook compositions - chaining multiple hooks per event type is already supported. You can run a safety gate, then a logger, then a notifier, all as separate scripts that each do one thing well.
MCP server hooks - Model Context Protocol servers can expose tools Claude uses, and hooks can wrap those too. If you've built a custom MCP server (database queries, internal APIs, IoT sensors), you can gate and log those calls exactly like native tools.
Remote hook endpoints - nothing stops your hook script from POSTing to an HTTP endpoint. If you're running n8n or a home lab webhook server, you can route hook events there and trigger arbitrarily complex automations: update a database, send a Slack message, or kick off a CI pipeline whenever Claude completes a specific type of task.
If you're doing serious Claude Code work at your desk, ergonomics matter. The Logitech MX Keys keyboard (~$99) is genuinely worth it for long coding sessions - the key travel and tactile feedback reduce fatigue, and USB-C charging means one less proprietary cable on your desk.
Getting Started Today
Hooks have the best ROI of any Claude Code feature I've used. The setup is low - a few JSON lines and a shell script - but the payoff compounds every session. Start with the audit log (always useful), add the safety gate next, and build from there based on your workflow's actual pain points.
The fact that hooks communicate back to Claude via stderr is what makes them genuinely powerful. You're not just filtering - you're giving Claude more context to make better decisions. That's the direction this whole ecosystem is moving: less Claude doing things blindly, more Claude operating within guardrails you define and understanding the constraints you've set.
If you build something interesting with hooks, the Claude Code GitHub Discussions section is active and the community shares patterns regularly. The best hooks tend to be project-specific - shaped by the actual friction points in a real codebase - which is exactly why this extensibility mechanism exists.