Create hooks that run shell commands on specific events to add guardrails, automations, and policy enforcement.
Quick Reference
You MUST read the reference files for detailed schemas and examples:
- Hook Events Reference - All events with input/output schemas
- Examples: Firewall - Block dangerous commands
- Examples: Quality Checks - Lint/format after edits
- Examples: Pre-Push Tests - Run tests before git push
Core Concepts
Hook Types
- Command hooks - Run bash scripts
- Prompt hooks - Query LLM for context-aware decisions
Exit Codes
| Code | Meaning | Behavior |
|---|---|---|
| 0 | Success | Action proceeds; stdout shown in verbose mode |
| 2 | Block | Action blocked; stderr fed to Claude |
| Other | Error | Non-blocking; stderr shown to user |
File Locations
- Settings:
.claude/settings.json - Scripts:
.claude/hooks/(mark executable)
Settings Structure
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/my-script.sh",
"timeout": 60
}
]
}
]
}
}
Hook Events Summary
| Event | When | Can Block? |
|---|---|---|
| PreToolUse | Before tool executes | Yes (exit 2) |
| PostToolUse | After tool completes | Feedback only |
| PermissionRequest | User sees permission dialog | Yes |
| UserPromptSubmit | User submits prompt | Yes |
| Stop | Main agent finishes | Yes (continue) |
| SubagentStop | Subagent finishes | Yes (continue) |
| SessionStart | Session begins | Add context |
| SessionEnd | Session ends | Cleanup only |
| Notification | Notifications sent | No |
| PreCompact | Before compact | No |
Common Matchers
For PreToolUse/PostToolUse/PermissionRequest:
Bash- Shell commandsEdit,Write,Read- File operationsGlob,Grep- Search operationsTask- Subagent tasksmcp__<server>__<tool>- MCP tools- Regex patterns supported
Wildcard Permissions
Use wildcards for flexible matching patterns:
Bash(npm *)- Match any npm commandBash(*-h*)- Match commands containing-hBash(git:*)- Match any git subcommand
This reduces configuration overhead and avoids mismatched permissions blocking legitimate workflows.
Script Template
#!/usr/bin/env bash
set -euo pipefail
# Read JSON input
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name // ""')
command=$(echo "$input" | jq -r '.tool_input.command // ""')
# Your validation logic here
if [[ "$command" =~ dangerous_pattern ]]; then
echo "Blocked: reason here" >&2
exit 2
fi
exit 0
Critical: Activating Hook Changes
Hooks are snapshotted at startup. After creating or modifying hooks:
⚠️ Changes won't take effect until you either:
- Restart Claude Code (exit and re-run
claude), OR- Run
/hooksto review and apply the updated configurationThis is a security feature - it prevents malicious hook modifications from affecting your current session.
Verifying Hooks Are Loaded
After restart, run /hooks to confirm your hook appears in the list. If it doesn't show up:
- Check JSON syntax in settings file
- Verify file is in correct location (
.claude/settings.json) - Look for
disableAllHooks: truein any settings file
Troubleshooting
Hook Not Triggering
- Did you restart? Hooks are snapshotted at startup - run
/hooksor restart Claude Code - Check
/hooksoutput - Your hook should be listed with correct matcher - Validate JSON - Run
cat .claude/settings.json | jq .to check syntax - Check matcher - Tool names are case-sensitive (
Bashnotbash)
Testing Hooks Safely
When creating hooks that block operations (like preventing push to main):
- Test on a safe branch first - Modify the hook to block a test branch
- Verify the block works - Attempt the blocked operation
- Update to production config - Change to block the actual target (e.g., main)
- Restart and verify - Run
/hooksto confirm the updated hook is loaded
Debugging
Use claude --debug to see hook execution details, or add logging to your hook:
echo "[DEBUG] Hook triggered: $cmd" >> /tmp/hook-debug.log
Attribution
Examples adapted from Steve Kinney's Claude Code Hook Examples.