Claude Code Hooks Documentation and Guide
TL;DR
- Claude Code hooks are shell commands that fire deterministically at lifecycle points during a session, giving you a hard guarantee instead of relying on prompt instructions Claude might forget.
- There are six hook types: PreToolUse and PostToolUse (the security and quality workhorses), plus Notification, Stop, PreCompact, and SessionStart.
- Hooks are configured in
.claude/settings.jsonunder the"hooks"key, where each type maps to matchers (glob patterns likeWrite,Edit, orBash(git:*)) and each matcher maps to commands. - Execution is controlled by exit code: exit 0 lets the action proceed, a non-zero exit blocks it, and anything printed to stdout flows back to Claude as feedback.
- A command hook is a hard gate that Claude cannot override, while a prompt hook is a soft gate that injects instructions; start with security (block sensitive-file writes, guard destructive git), then expand to linting, logging, and notifications.
What are Claude Code hooks?
Claude Code hooks are shell commands that run automatically at specific points during a Claude Code session. They let you enforce rules, run checks, and trigger workflows without relying on Claude to remember them, the hooks fire deterministically every time.
Think of hooks as git hooks for your AI workflow. A pre-commit hook blocks bad commits from entering your repository. A Claude Code hook blocks bad actions from executing in your session. The difference: Claude Code hooks cover a much wider surface area than just commits. They fire on file writes, tool calls, session events, and more. Hooks can also be packaged and distributed through Claude Code plugins, so a whole team can share the same quality gates.
If you've been using Claude Code without hooks, you're relying on prompt instructions alone to enforce quality. That works until it doesn't. Hooks give you a hard guarantee. (New to Claude Code? Start with our introduction to Claude Code.)
Hook types: when do they fire?
Claude Code supports six hook types, each tied to a different lifecycle event:
- PreToolUse: Runs before Claude executes a tool (Write, Edit, Bash, etc.). Can block the action entirely. This is your primary security and quality gate.
- PostToolUse: Runs after a tool completes. Useful for linting, formatting, or validation checks on the result.
- Notification: Fires when Claude produces a notification (e.g., task complete, waiting for input). Use this to send alerts to Slack, Discord, or your phone.
- Stop: Runs when Claude finishes a turn (stops generating). Good for logging, metrics, and session tracking.
- PreCompact: Fires before Claude auto-compacts a long conversation. Lets you save state before context gets compressed.
- SessionStart: Runs at session initialization. Useful for resetting stale files, loading environment state, or injecting startup context.
Each type serves a distinct purpose. PreToolUse and PostToolUse are the workhorses, they handle security enforcement and quality checks. The rest cover lifecycle management and observability.
Where hooks live: configuration structure
Hooks are defined in .claude/settings.json under the "hooks" key. The structure is straightforward: each hook type maps to an array of matchers, and each matcher maps to an array of commands.
Here's the skeleton:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "/path/to/your/script.sh"
}
]
}
],
"PostToolUse": [],
"Stop": [],
"Notification": []
}
}
The matcher field uses glob patterns to target specific tools. Some common patterns:
"Write": matches all file writes"Edit": matches all file edits"Bash(git:*)": matches any Bash command starting withgit"Bash(npm:*)": matches npm commands"*": matches everything (use sparingly)
The type field is either "command" (runs a shell command) or "prompt" (injects text back into Claude's context). Command hooks are the common case. Prompt hooks are useful when you want to dynamically inject instructions rather than enforce a hard gate.
How hook execution works
When a hook fires, Claude Code passes context to your script via environment variables and stdin. Your script's behavior determines what happens next:
- Exit code 0: Action proceeds normally. Any stdout is fed back to Claude as context.
- Non-zero exit code: The action is blocked. Claude sees the stderr/stdout as an error message and adjusts accordingly.
- Stdout content: Whatever your script prints to stdout becomes visible to Claude. This is powerful, it means your hooks can give Claude feedback, not just block or allow.
This design is elegant. A security hook that exits with code 1 and prints "Blocked: cannot write to .env files" gives Claude enough information to course-correct. A linting hook that exits with code 0 but prints warnings lets the action proceed while flagging issues.
Example 1: Block writes to sensitive files
The most common hook pattern is preventing Claude from touching files it shouldn't. Here's a PreToolUse hook that blocks writes to .env, credentials files, and secrets:
#!/bin/bash
# .claude/hooks/block-sensitive-writes.sh
# Read the tool input from stdin
INPUT=$(cat)
# Extract the file path from the tool input
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Block patterns
if [[ "$FILE_PATH" == *.env* ]] || \
[[ "$FILE_PATH" == *credentials* ]] || \
[[ "$FILE_PATH" == *secret* ]] || \
[[ "$FILE_PATH" == *.pem ]] || \
[[ "$FILE_PATH" == *id_rsa* ]]; then
echo "BLOCKED: Cannot write to sensitive file: $FILE_PATH" >&2
exit 1
fi
exit 0
Register it in settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/block-sensitive-writes.sh"
}
]
},
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/block-sensitive-writes.sh"
}
]
}
]
}
}
Now Claude physically cannot modify your .env file, regardless of what you ask it to do. The hook fires before the write and blocks it. No exceptions.
Example 2: Auto-lint after file edits
PostToolUse hooks run after an action completes. This makes them perfect for running linters, formatters, or validators on files Claude just modified:
#!/bin/bash
# .claude/hooks/post-edit-lint.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Only lint JS/TS files
case "$FILE_PATH" in
*.js|*.jsx|*.ts|*.tsx)
RESULT=$(npx eslint "$FILE_PATH" 2>&1)
if [ $? -ne 0 ]; then
echo "ESLint found issues in $FILE_PATH:"
echo "$RESULT"
# Exit 0 - don't block, just inform Claude
exit 0
fi
;;
esac
exit 0
Notice this exits with code 0 even when lint errors are found. The hook doesn't block, it prints the lint output, which flows back to Claude as context. Claude sees the warnings and can fix them in its next action. If you wanted lint errors to be hard blockers, change the exit code to 1.
Example 3: Guard destructive git commands
Some git commands are dangerous, reset --hard, push --force, branch -D. A PreToolUse hook on Bash can intercept these before they execute:
#!/bin/bash
# .claude/hooks/guard-destructive-git.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# Check for destructive git operations
if echo "$COMMAND" | grep -qE 'git\s+(reset\s+--hard|push\s+--force|push\s+-f|clean\s+-f|branch\s+-D|checkout\s+\.\s*$)'; then
echo "BLOCKED: Destructive git command detected: $COMMAND" >&2
echo "If you need to run this, ask the user to execute it manually." >&2
exit 1
fi
exit 0
Configuration:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash(git:*)",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/guard-destructive-git.sh"
}
]
}
]
}
}
The Bash(git:*) matcher ensures this only fires on git commands, not every Bash call. This keeps overhead minimal.
Example 4: Session logging with Stop hooks
Stop hooks fire every time Claude finishes a turn. This is the right place for audit logging, metrics collection, or session tracking:
#!/bin/bash
# .claude/hooks/log-session.sh
INPUT=$(cat)
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
STOP_REASON=$(echo "$INPUT" | jq -r '.stop_reason // "unknown"')
# Append to audit log
echo "[$TIMESTAMP] Session turn completed | Reason: $STOP_REASON" \
>> .claude/logs/audit-trail.log
exit 0
Over time, this gives you a complete timeline of Claude's activity. You can analyze patterns: how many turns per session, common stop reasons, time distribution across tasks.
Example 5: Notification hooks for alerts
Notification hooks let you pipe Claude Code events to external services. Here's a hook that sends a Slack message when Claude finishes a long task:
#!/bin/bash
# .claude/hooks/slack-notify.sh
INPUT=$(cat)
MESSAGE=$(echo "$INPUT" | jq -r '.message // "Task complete"')
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"text\": \"Claude Code: $MESSAGE\"}" \
> /dev/null
exit 0
This is especially useful when you kick off a long operation and switch to other work. The notification tells you when Claude needs attention without you having to watch the terminal.
Prompt-type hooks vs command-type hooks
So far, every example has used "type": "command", a shell command that runs and returns a result. The other option is "type": "prompt", which injects text directly into Claude's context.
Prompt hooks don't execute anything. They add instructions or reminders at specific lifecycle points. For example:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "prompt",
"prompt": "Before writing any file, verify: (1) the file path is correct, (2) you're not overwriting important content, (3) the content is complete - no TODOs or placeholders."
}
]
}
]
}
}
This is a soft gate, Claude receives the instruction and follows it, but there's no hard enforcement. Use prompt hooks for guidelines and command hooks for rules.
Common patterns worth adopting
Beyond the examples above, several claude code automation patterns show up repeatedly in well-configured setups:
Formatting enforcement. A PostToolUse hook that runs Prettier on any modified file. Claude writes code, the hook formats it, and the formatted version is what ends up on disk. Zero formatting debates.
Completeness validation. A PreToolUse hook on Write that scans for TODO, FIXME, PLACEHOLDER, or TBD in the content being written. Block incomplete work from reaching critical files while allowing it in drafts and scratchpads.
Test running. A PostToolUse hook that triggers relevant test suites after code changes. Claude edits a module, the hook runs that module's tests, and any failures feed back to Claude for immediate fixing.
Dependency checking. A PostToolUse hook on Bash that checks for newly added dependencies (package.json changes) and flags known-vulnerable packages.
If configuring all of this from scratch sounds tedious, that's because it is. Claudify ships with 21 pre-built hook configurations covering security, formatting, logging, and quality enforcement, ready to drop into any project.
Debugging hooks that don't work
Hooks are shell scripts. They break for the same reasons all shell scripts break. Here's a systematic debugging approach:
1. Check the basics. Is the script executable? (chmod +x script.sh). Does it have the right shebang? (#!/bin/bash). Does the path in settings.json match the actual file location?
2. Test in isolation. Run the script manually with sample input piped via stdin:
echo '{"tool_input": {"file_path": "test.env"}}' | .claude/hooks/block-sensitive-writes.sh
echo $? # Should be 1 for blocked
3. Add echo debugging. Temporarily add echo "DEBUG: got here" statements. Since stdout goes back to Claude, you'll see the debug output in the session.
4. Check settings.json validity. A malformed JSON file silently breaks all hooks. Validate with:
python3 -c "import json; json.load(open('.claude/settings.json'))"
5. Verify the matcher. If the hook never fires, the matcher might not match the tool you expect. Start with "*" to match everything, confirm the hook runs, then narrow down.
Most hook issues come down to file permissions, JSON syntax errors, or matcher mismatches. Rarely is the hook logic itself the problem.
How hooks complement skills and memory
Hooks, skills, and memory form a layered system. Each handles a different class of problem:
- Memory gives Claude context about what, current state, past decisions, preferences
- Skills give Claude procedures for how, step-by-step instructions for complex tasks
- Hooks enforce constraints, hard rules that apply regardless of context or instructions
Memory and skills are advisory. Claude reads them and follows them, but it's ultimately Claude making the decision. Hooks are deterministic. A PreToolUse hook that blocks .env writes cannot be overridden by Claude, no matter what instructions it receives.
This layering is important. You don't want hooks for everything (too rigid). You don't want skills for everything (too verbose). And you don't want memory for everything (too loose). The right approach uses all three: memory for context, skills for procedures, hooks for constraints.
If you haven't set up a Claude Code environment yet, our setup guide walks through the full process including hook configuration.
Start with security, then expand
If you're adding hooks for the first time, start with two:
- A PreToolUse hook blocking writes to sensitive files (
.env, credentials, keys) - A PreToolUse hook guarding destructive git commands
These two hooks alone prevent the most common ways Claude Code can cause damage. Everything else, linting, logging, notifications, is optimization. Security gates are foundational.
Once those are solid, add PostToolUse hooks for your linter and formatter. Then Stop hooks for logging. Then Notification hooks if you run long tasks. Build the system incrementally, test each hook in isolation, and keep your settings.json version-controlled so you can roll back.
Hooks turn Claude Code from a powerful but unpredictable tool into a reliable system with guardrails. The 30 minutes you spend configuring them pays back immediately, in prevented mistakes, enforced standards, and peace of mind.
Ready to skip the configuration work? Claudify includes 21 production-tested hook configs, pre-built skills, and a complete memory architecture, everything you need to run Claude Code like a professional engineering system.
More like this
Ready to upgrade your Claude Code setup?
Get Claudify