Claude Code Hooks: Automate Your Workflow with Event-Driven Scripts
Hooks are one of Claude Code's most powerful features. They let you run shell commands automatically in response to events during a Claude Code session -- before or after tool calls, when Claude stops responding, or when notifications fire. Think of them as git hooks, but for your AI coding assistant.
Unlike rules or skills that shape how Claude thinks, hooks shape what happens around Claude's actions. They're deterministic, run every time their trigger fires, and execute outside the model -- meaning they're fast, reliable, and not subject to the model deciding whether to follow an instruction.
How Hooks Work
Hooks are configured in your Claude Code settings file (.claude/settings.json at the project or user level). Each hook specifies a matcher (which event to respond to), a command to run, and optionally whether it should block execution or run in the background.
When an event fires, Claude Code checks if any hooks match, executes the command, and feeds the result (stdout) back as context. If a hook exits with a non-zero code, it can block the action from proceeding -- giving you a veto over Claude's behavior.
Hook Types
PreToolUse
Runs before Claude executes a tool. This is your chance to validate, gate, or modify what Claude is about to do. If the hook exits non-zero, the tool call is blocked and Claude sees the error output.
Common tool names you can match against:
Bash-- shell command executionWrite-- file creationEdit-- file editingWebFetch-- URL fetching
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hook": "python3 .claude/validate-command.py"
}
]
}
}
The hook receives the tool input as JSON on stdin. For a Bash tool call, that includes the command string, so your validation script can parse it and decide whether to allow it.
PostToolUse
Runs after a tool completes. Useful for triggering side effects like auto-formatting code after Claude writes a file, running linters, or logging what happened.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hook": "npx prettier --write $CLAUDE_FILE_PATH"
}
]
}
}
PostToolUse hooks receive both the tool input and the tool output on stdin. The exit code of a PostToolUse hook doesn't block anything -- the action already happened -- but the stdout is still fed back to Claude as context.
Notification
Fires when Claude Code sends a notification (for example, when a long-running task completes and Claude wants to alert you). You can wire this up to system notifications, Slack webhooks, or custom alerting.
{
"hooks": {
"Notification": [
{
"hook": "terminal-notifier -title 'Claude Code' -message \"$CLAUDE_NOTIFICATION\""
}
]
}
}
Stop
Runs when Claude finishes its turn (stops generating). This is useful for post-processing like running the full test suite, generating summaries, or updating dashboards.
{
"hooks": {
"Stop": [
{
"hook": "npm test 2>&1 | tail -20"
}
]
}
}
If a Stop hook fails (non-zero exit), Claude sees the output and continues working -- essentially getting another turn to fix whatever the hook flagged. This creates a powerful feedback loop: Claude writes code, tests run, failures feed back to Claude, and it iterates.
Practical Examples
Auto-format on every file write
Never worry about Claude writing code that doesn't match your style guide:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hook": "npx prettier --write $CLAUDE_FILE_PATH 2>/dev/null; exit 0"
}
]
}
}
Prevent writes to protected files
Block Claude from modifying your lock files, CI config, or other sensitive paths:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hook": "python3 -c \"import sys,json; d=json.load(sys.stdin); p=d.get('file_path',''); sys.exit(1 if any(x in p for x in ['package-lock.json','.github/','migration']) else 0)\""
}
]
}
}
Auto-run tests after code changes
Use a Stop hook to run tests every time Claude finishes a turn, creating a continuous feedback loop:
{
"hooks": {
"Stop": [
{
"hook": "npm test -- --bail 2>&1 | tail -30"
}
]
}
}
Environment Variables
Hooks have access to several environment variables set by Claude Code:
CLAUDE_FILE_PATH-- the file being written or edited (for Write/Edit tools)CLAUDE_NOTIFICATION-- the notification message (for Notification hooks)CLAUDE_TOOL_NAME-- the name of the tool being invoked
Hook commands also receive structured JSON on stdin containing the full tool input (and output, for PostToolUse hooks), letting you write sophisticated validation logic.
Tips for Writing Hooks
- Keep hooks fast. They run synchronously and block Claude's execution. A slow hook means a slow workflow.
- Use exit codes intentionally. For PreToolUse, non-zero blocks the action. For Stop, non-zero gives Claude another turn. Design your hooks around this.
- Log stdout carefully. Whatever your hook prints to stdout becomes context for Claude. Use this to provide helpful error messages or suggestions.
- Test hooks independently. Run your hook scripts manually with sample JSON input before wiring them into Claude Code.
- Scope hooks narrowly. Use the
matcherfield to target specific tools rather than running hooks on every action.
Hooks vs Skills vs CLAUDE.md
It's worth understanding where hooks fit in the Claude Code customization stack:
- CLAUDE.md -- natural language instructions that guide Claude's behavior (soft, advisory)
- Skills -- reusable prompt templates for specific tasks (invoked on demand)
- Hooks -- shell commands that execute deterministically on events (hard, enforced)
Use CLAUDE.md to tell Claude how to work. Use skills to define what Claude can do. Use hooks to enforce what must happen around Claude's actions. For a deeper comparison, see our guide on Skills vs MCP.