Claude Code hooks are the deterministic layer underneath a probabilistic agent. The model reasons and decides. The hook executes the rule you wrote, every time, with no exceptions.
If you want a rule that Claude Code must follow regardless of context window length or conversation history, a hook is the only way to enforce it reliably.
Key Takeaways
- Hooks fire on lifecycle events, not on prompts: They are shell commands, not LLM instructions.
- Six hook events exist: PreToolUse, PostToolUse, SessionStart, Stop, Notification, and SubagentStop.
- Exit codes control behavior: Exit 0 allows, exit 1 blocks (PreToolUse) or reports error (PostToolUse), exit 2 silently ignores.
- PreToolUse is for policy; PostToolUse is for verification: Never run expensive operations in PreToolUse.
- Hooks live in
.claude/settings.json(shared) or.claude/settings.local.json(personal): Shared hooks apply to the whole team. - Test every hook independently in the terminal before adding it to config: A buggy PreToolUse hook blocks legitimate work.
What Are Claude Code Hooks?
Hooks are like Git hooks, but for AI-assisted development. They attach shell commands to specific lifecycle events and run automatically every time that event fires, regardless of what Claude decides.
CLAUDE.md is for guidance the model should consider. Hooks are for behavior the system must guarantee.
The practical difference:
| Instruction type | Where it goes | How reliable |
|---|---|---|
| ”Prefer functional patterns” | CLAUDE.md | Claude considers it |
”Never rm -rf outside the project root” | PreToolUse hook | Enforced every time |
| ”Format files after every edit” | PostToolUse hook | Runs every time |
If the consequence of the model ignoring an instruction is “minor annoyance,” put it in CLAUDE.md. If the consequence is a production incident or data loss, put it in a hook.
Where Hooks Live
Hooks are configured in JSON settings files inside the .claude/ directory at your project root.
Two scopes:
.claude/settings.json # Team-shared: committed to the repo, applies to everyone
.claude/settings.local.json # Personal: gitignored, applies only to your machine
Use settings.json for security policies and shared automation. Use settings.local.json for personal preferences that should not affect teammates.
Basic structure:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/your-hook.sh"
}
]
}
]
}
}
The matcher field is a string matched against the tool name. Use "Bash" for shell commands, "Write" for file writes, "Edit" for file edits, or ".*" to match all tools.
The Six Hook Events
| Event | When it fires | Can block? | Best used for |
|---|---|---|---|
| PreToolUse | Before a tool call executes | Yes (exit 1) | Policy enforcement, blocking dangerous commands |
| PostToolUse | After a tool call succeeds | No (reports error) | Formatting, linting, logging, validation |
| SessionStart | When a Claude Code session begins | No | Context prep, environment checks, printing reminders |
| Stop | When Claude finishes a task or session | No | Final validation, test runs, summaries |
| Notification | When Claude needs user attention | No | Desktop alerts, Slack notifications |
| SubagentStop | When a sub-agent finishes | No | Sub-agent result validation, orchestrator signaling |
How Exit Codes Work
Exit codes control what happens after your hook runs.
For PreToolUse:
exit 0 → Allow the tool call to proceed
exit 1 → Block the tool call; Claude sees your stderr output and can adjust
exit 2 → Silently continue (hook failure is ignored)
For PostToolUse:
exit 0 → Success, continue normally
exit 1 → Report the error to Claude as feedback; Claude can respond
exit 2 → Silently continue (hook failure is ignored)
Always write your block reason to stderr when exiting 1 from a PreToolUse hook. Claude reads stderr and uses it to understand why the action was blocked.
echo "BLOCKED: command matches danger pattern" >&2
exit 1
PreToolUse Hooks: Policy Enforcement
PreToolUse hooks run before a tool call. They are your safety net. Use them to block dangerous commands, require confirmation for high-risk operations, or enforce access boundaries.
Example 1: Danger Guard (Block Irreversible Commands)
This is the first hook every team should have. It blocks the most destructive bash patterns before they execute.
Create .claude/hooks/danger-guard.sh:
#!/usr/bin/env bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# Patterns that should never run autonomously
DANGER='(rm -rf /|rm -rf ~|rm -rf \$HOME|git reset --hard origin|git push --force.*main|git push --force.*master|DROP TABLE|TRUNCATE TABLE)'
if echo "$CMD" | grep -qE "$DANGER"; then
echo "[danger-guard] BLOCKED: $CMD" >&2
echo "Run this manually outside Claude Code if you are sure." >&2
exit 1
fi
exit 0
Make it executable:
chmod +x .claude/hooks/danger-guard.sh
Register it in .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/danger-guard.sh"
}
]
}
]
}
}
Example 2: Block Writes Outside the Project
Prevents Claude from writing files outside your project directory:
#!/usr/bin/env bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROJECT=$(pwd)
if [[ -n "$FILE" && "$FILE" != "$PROJECT"* ]]; then
echo "[scope-guard] BLOCKED: write outside project: $FILE" >&2
exit 1
fi
exit 0
Register with matcher "Write" and "Edit" to cover both file operations.
Example 3: Require Confirmation for Production Paths
Asks for explicit confirmation before any file in a production config directory is modified:
#!/usr/bin/env bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if echo "$FILE" | grep -qE '(config/prod|deploy/|\.env\.production)'; then
echo "[prod-guard] BLOCKED: production path requires manual confirmation." >&2
echo "Review the change and apply it manually: $FILE" >&2
exit 1
fi
exit 0
PostToolUse Hooks: Verification and Automation
PostToolUse hooks run after a tool call succeeds. They cannot block, but they can run formatters, linters, tests, and loggers automatically.
The model cannot forget to run them. The model cannot skip them because the conversation is long.
Example 4: Auto-Format on File Edit
Runs Prettier on any JavaScript or TypeScript file Claude edits:
#!/usr/bin/env bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if echo "$FILE" | grep -qE '\.(js|ts|jsx|tsx|json|css)$'; then
npx prettier --write "$FILE" 2>/dev/null
fi
exit 0
Register with matcher "Edit" and "Write".
Example 5: Timestamped Command Logger
Logs every Bash command Claude runs with a timestamp to an audit file:
#!/usr/bin/env bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
LOG="${CLAUDE_PROJECT_DIR:-.}/.claude/command_log.txt"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $CMD" >> "$LOG"
exit 0
Add .claude/command_log.txt to your .gitignore. This hook creates a full audit trail of every shell command Claude Code ran in your project. Register with matcher "Bash".
Example 6: Run Tests After File Edits
Runs your test suite after Claude edits a source file. Use the Stop hook (not PostToolUse) for full test runs to avoid running tests after every individual file edit.
#!/usr/bin/env bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Only run on source files, not config or docs
if echo "$FILE" | grep -qE '^src/.*\.(js|ts|py)$'; then
npm test -- --testPathPattern="$(basename "$FILE")" 2>&1 | tail -5
fi
exit 0
Example 7: Secret Scanner on File Writes
Scans newly written files for common credential patterns before they are committed. See also the Claude Code security audit checklist for the full secrets handling domain.
#!/usr/bin/env bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ -f "$FILE" ]]; then
SECRETS='(AKIA[0-9A-Z]{16}|sk-[a-zA-Z0-9]{32,}|ghp_[a-zA-Z0-9]{36}|-----BEGIN.*PRIVATE KEY)'
if grep -qE "$SECRETS" "$FILE" 2>/dev/null; then
echo "[secret-scan] WARNING: possible credential detected in $FILE" >&2
echo "Review before committing." >&2
exit 1
fi
fi
exit 0
SessionStart Hooks
SessionStart hooks fire when a new Claude Code session opens. Use them to prepare context, print reminders, or run environment checks.
Example 8: Print Session Reminders
#!/usr/bin/env bash
echo "=== Session started: $(date) ==="
echo "Project: $CLAUDE_PROJECT_DIR"
echo "Reminder: always commit before large refactors."
exit 0
Stop Hooks
Stop hooks fire when Claude finishes a task. Use them for full test runs, final validation, and session summaries.
Example 9: Run Full Test Suite on Task Completion
#!/usr/bin/env bash
echo "Running test suite..."
npm test 2>&1 | tail -10
exit 0
Stop hooks are the right place for expensive operations like full test runs, builds, or deployment checks. Never put these in PostToolUse, which fires after every individual tool call.
Notification Hooks
Notification hooks fire when Claude needs your attention. Useful for long-running tasks where you want to be alerted without watching the terminal.
Example 10: Desktop Notification (macOS)
#!/usr/bin/env bash
osascript -e 'display notification "Claude Code needs your attention" with title "Claude Code"'
exit 0
Hook Configuration with Multiple Matchers
You can register multiple hooks for multiple tool events in a single settings file:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": ".claude/hooks/danger-guard.sh" }]
},
{
"matcher": "Write",
"hooks": [{ "type": "command", "command": ".claude/hooks/scope-guard.sh" }]
}
],
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [{ "type": "command", "command": ".claude/hooks/format.sh" }]
},
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": ".claude/hooks/command-logger.sh" }]
}
],
"Stop": [
{
"matcher": ".*",
"hooks": [{ "type": "command", "command": ".claude/hooks/run-tests.sh" }]
}
]
}
}
Common Mistakes and How to Avoid Them
| Mistake | Consequence | Fix |
|---|---|---|
| Running expensive operations in PostToolUse | Tests run after every file edit; session becomes unusably slow | Move full test runs to Stop hooks |
| PreToolUse hook exits 1 on an error in the hook itself | Legitimate tool calls get blocked | Always exit 0 as a fallback at the end of PreToolUse hooks |
| Not writing to stderr when blocking | Claude does not know why the action was blocked | Always echo "reason" >&2 before exit 1 |
| Putting secrets in hook script bodies | Credentials exposed in version control | Use environment variables for any secrets hooks need |
| Not testing hooks independently first | Buggy hooks silently break workflows | Run .claude/hooks/your-hook.sh in the terminal with test input before registering |
Need Help Setting Up Production-Grade Claude Code Hooks?
Individual hooks are straightforward. Team-wide hooks with shared conventions, security policies, and MCP server integration are a different scope of work.
Phos AI Labs is an embedded AI consulting firm for small and mid-market businesses.
We identify the right AI problems, build the strategy, handle implementation, and train your team until AI is how the business actually runs.
- Strategy before systems: We establish which workflows Claude Code should own before any hook configuration begins.
- AI Foundations that hold: We install the operating context, decision rules, and configuration standards your team runs on for years.
- Real team training: We build fluency inside your actual workflows, not in staged demos disconnected from your operations.
- Private AI Workspace: We design a company-wide AI environment built around your knowledge base and existing stack.
- AI Implementation: We rebuild the workflows that matter most so AI compounds across your business.
- Honest judgment, every time: We tell you which hooks matter for your specific environment and which are unnecessary overhead.
- We stay until it compounds: We are not done when the configuration is delivered. We are done when the team runs it reliably.
400+ engagements. Clients include Zapier, Coca-Cola, Medtronic, Dataiku, and American Express.
For certified Claude Code development with security-first hook configuration from day one, LOW/CODE Agency is one of the first Anthropic partners worldwide with 10+ CCA-F certified developers on staff.
If you want your Claude Code deployment configured correctly, talk to the team at Phos AI Labs.
Frequently Asked Questions
What are Claude Code hooks?
Hooks are shell commands that fire automatically on Claude Code lifecycle events (PreToolUse, PostToolUse, SessionStart, Stop, Notification, SubagentStop). They enforce rules deterministically, unlike CLAUDE.md instructions which Claude considers but can overlook.
Where do I put Claude Code hooks?
Hooks are configured in .claude/settings.json (team-shared, committed to the repo) or .claude/settings.local.json (personal, gitignored). Hook scripts themselves live anywhere, but .claude/hooks/ is the standard location.
What is the difference between PreToolUse and PostToolUse?
PreToolUse fires before a tool call and can block it (exit 1). PostToolUse fires after and cannot block, but runs formatters, linters, loggers, and tests. Use pre for policy, post for verification.
How do I block a command with a PreToolUse hook?
Exit with code 1 and write your reason to stderr. Claude reads stderr to understand why the block occurred. Exit 0 allows the command. Exit 2 silently ignores the hook result.
Will hooks slow down my Claude Code sessions?
PreToolUse hooks add latency to every matched tool call, so keep them fast (under 100ms for pattern matching). PostToolUse hooks run after the call, so moderate latency is fine. Never run full test suites in PostToolUse; use Stop hooks instead.
Can I use hooks with MCP servers?
Yes. Use the matcher pattern mcp__servername__toolname to match specific MCP server tool calls. For example, mcp__memory__.* matches all tools from an MCP server named “memory.”