Hook Configuration Reference
Complete reference for configuring hooks in .
Configuration File Location
Hooks are configured in .claude/settings.json or .claude/settings.local.json:
/your-project/
├── .claude/
│ └── settings.json ← Add hook configuration here
└── hooks/
├── security/ ← secret-detection.py, secret-file-scanner.py, file-protection.py
├── quality/ ← frontmatter-validator.py, tag-taxonomy-enforcer.py, wiki-link-checker.py, filename-convention-checker.py
├── ux/ ← code-formatter.py, context-loader.sh, search-hint.sh
├── safety/ ← bash-safety.py
└── notification/ ← desktop-notify.sh
Project vs Global Settings
- Project settings:
.claude/settings.json(in project root) - Local settings:
.claude/settings.local.json(gitignored, per-machine overrides) - Global settings:
~/.claude/settings.json(applies to all projects)
Project settings override global settings for the same event/matcher.
Basic Structure
{
"hooks": {
"<EventType>": [
{
"matcher": "<ToolNamePattern>",
"hooks": [
{"type": "command", "command": "<command>"}
]
}
]
}
}
Event Types
| Event | When It Fires |
|---|---|
PreToolUse |
Before tool executes |
PostToolUse |
After tool executes |
UserPromptSubmit |
User submits prompt |
Stop |
Conversation ends |
Start |
Conversation begins |
PermissionRequest |
Claude requests permission |
Most common: PreToolUse, PostToolUse
Matcher Syntax
Matchers use regex patterns to match tool names:
"matcher": "Edit|Write" // Matches Edit OR Write
"matcher": ".*" // Matches all tools
"matcher": "Bash" // Matches Bash only
"matcher": "Edit|Write|NotebookEdit" // Multiple tools
Common Matchers
| Pattern | Matches |
|---|---|
| `Edit | Write` |
Bash |
Bash commands |
.* |
All tools |
Read |
File reading |
| `Grep | Glob` |
Hook Object
Each hook in the hooks array:
{
"type": "command",
"command": "python3 hooks/security/file-protection.py"
}
Fields
type: Always"command"(only type supported)command: Shell command to execute
Command requirements:
- Must read JSON from stdin
- Must write JSON to stdout
- Must exit with appropriate code (0/1/2)
Complete Example
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": ".*",
"hooks": [
{"type": "command", "command": "python3 hooks/security/secret-detection.py", "timeout": 10},
{"type": "command", "command": "hooks/ux/context-loader.sh", "timeout": 5}
]
}
],
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{"type": "command", "command": "python3 hooks/security/file-protection.py", "timeout": 5},
{"type": "command", "command": "python3 hooks/security/secret-file-scanner.py", "timeout": 10}
]
},
{
"matcher": "Grep",
"hooks": [
{"type": "command", "command": "hooks/ux/search-hint.sh", "timeout": 5}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{"type": "command", "command": "python3 hooks/quality/frontmatter-validator.py", "timeout": 10},
{"type": "command", "command": "python3 hooks/quality/tag-taxonomy-enforcer.py", "timeout": 10},
{"type": "command", "command": "python3 hooks/quality/wiki-link-checker.py", "timeout": 15},
{"type": "command", "command": "python3 hooks/quality/filename-convention-checker.py", "timeout": 10},
{"type": "command", "command": "python3 hooks/ux/code-formatter.py", "timeout": 10}
]
}
],
"PermissionRequest": [
{
"matcher": "Bash",
"hooks": [
{"type": "command", "command": "python3 hooks/safety/bash-safety.py", "timeout": 5}
]
}
],
"Notification": [
{
"matcher": "Stop",
"hooks": [
{"type": "command", "command": "hooks/notification/desktop-notify.sh"}
]
}
]
}
}
Hook Execution Order
Hooks execute sequentially in the order defined:
"hooks": [
{"type": "command", "command": "hook1.py"}, // Runs first
{"type": "command", "command": "hook2.py"}, // Runs second
{"type": "command", "command": "hook3.py"} // Runs third
]
Blocking behaviour:
- If hook1 exits 2 (block), hook2 and hook3 don't run
- If hook1 exits 1 (warn), hook2 and hook3 still run
- If hook1 exits 0 (allow), hook2 and hook3 run
Order matters for:
- Security checks (run first to block early)
- Formatters (run after validation)
- Backups (run last)
Environment Variables
Hooks receive environment variables from Claude Code:
CLAUDE_PROJECT_ROOT=/path/to/project
CLAUDE_CONVERSATION_ID=abc123
CLAUDE_USER_NAME=username
Access in Python:
import os
project_root = os.getenv("CLAUDE_PROJECT_ROOT")
conversation_id = os.getenv("CLAUDE_CONVERSATION_ID")
Custom environment variables:
Set in shell before running Claude Code:
export VAULT_ROOT=/path/to/vault
export BACKUP_DIR=/path/to/backups
Hooks can read these for configuration.
Path Resolution
Hook commands are executed with CWD = project root.
Relative paths:
"command": "python3 hooks/security/file-protection.py"
Resolves to: /your-project/hooks/security/file-protection.py
Absolute paths:
"command": "/usr/local/bin/custom-hook"
Use absolute paths for hooks outside project directory.
Conditional Configuration
Use environment variables to conditionally enable hooks:
# .bashrc or .zshrc
export CLAUDE_ENABLE_BACKUP_HOOKS=true
Hook checks variable:
import os
if not os.getenv("CLAUDE_ENABLE_BACKUP_HOOKS"):
sys.exit(0) # Skip backup if not enabled
Testing Configuration
1. Validate JSON
cat .claude/settings.json | jq .
2. Test individual hook
echo '{
"event": "PreToolUse",
"tool_name": "Edit",
"tool_input": {"file_path": "test.md"}
}' | python3 hooks/security/file-protection.py
echo "Exit code: 127"
3. Verify hook loads
Enable debug mode:
export CLAUDE_DEBUG=1
Claude Code will log hook execution in conversation.
Common Configurations
Minimal (Security Only)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{"type": "command", "command": "python3 hooks/security/file-protection.py"}
]
}
]
}
}
Standard (Security + Quality)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{"type": "command", "command": "python3 hooks/security/secret-file-scanner.py"},
{"type": "command", "command": "python3 hooks/security/file-protection.py"}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{"type": "command", "command": "python3 hooks/quality/frontmatter-validator.py"}
]
}
]
}
}
Full (All Features)
See examples/obsidian-vault.json
Troubleshooting
Hooks not running
- Check JSON syntax:
cat .claude/settings.json | jq . - Verify matcher pattern matches tool name
- Test hook manually:
echo '{}' | python3 hooks/your-hook.py - Check file permissions:
chmod +x hooks/**/*.py
Hook exits but doesn't block
- PreToolUse hooks must exit 2 to block
- PostToolUse hooks cannot block (exit 1 for warning)
- Check exit code:
echo 127after running hook
Hook crashes
- Add error handling: wrap in try/except
- Check stdin input: ensure it's valid JSON
- Add logging: write to stderr for debugging
Performance issues
- Check hook execution time
- Use caching for expensive operations
- Consider moving slow operations to PostToolUse
Further Reading
- Hook Lifecycle — Events and exit codes
- Hook Patterns — Implementation patterns
- README — Hook overview
- Installation Guide — Setup instructions