Hooks System in Claude Code
Table of content
Hooks are shell commands that fire automatically at specific points in Claude Code’s lifecycle. Unlike prompts (which are suggestions), hooks are deterministic. They run every time, no exceptions.
Why Hooks Matter
Your AI assistant can read and write files. That’s powerful and dangerous. Hooks let you:
- Auto-format code after every edit
- Block writes to sensitive files
- Inject context at session start
- Log all tool usage
- Enforce project rules automatically
Hook Events
Five events trigger hooks:
| Event | When it fires |
|---|---|
PreToolUse | Before any tool executes |
PostToolUse | After any tool completes |
SessionStart | When Claude Code launches |
SessionEnd | When session terminates |
Notification | When Claude sends a notification |
Configuration
Add hooks to .claude/settings.json in your project:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "prettier --write \"$CLAUDE_FILE_PATH\""
}
]
}
}
The matcher field accepts regex patterns to filter which tools trigger the hook.
Environment Variables
Hooks receive context through environment variables:
CLAUDE_TOOL_NAME- The tool being invokedCLAUDE_FILE_PATH- File path (for file operations)CLAUDE_SESSION_ID- Current session identifier
Practical Examples
Auto-format After Edits
Run Prettier on JavaScript/TypeScript files after every edit:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "if [[ \"$CLAUDE_FILE_PATH\" =~ \\.(js|ts|jsx|tsx)$ ]]; then prettier --write \"$CLAUDE_FILE_PATH\"; fi"
}
]
}
}
Block Sensitive File Writes
Prevent writes to .env and production configs:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"command": "if [[ \"$CLAUDE_FILE_PATH\" =~ (\\.env|production\\.config) ]]; then echo 'Blocked: sensitive file' >&2; exit 1; fi"
}
]
}
}
Exit code 1 aborts the operation.
Inject Context at Session Start
Load project-specific context when starting a session:
{
"hooks": {
"SessionStart": [
{
"command": "cat .claude/project-context.md"
}
]
}
}
Run Tests After Code Changes
Automatically run relevant tests:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "if [[ \"$CLAUDE_FILE_PATH\" =~ \\.py$ ]]; then pytest \"${CLAUDE_FILE_PATH%.*}_test.py\" 2>/dev/null || true; fi"
}
]
}
}
Go Format on Save
Auto-format Go files:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "if [[ \"$CLAUDE_FILE_PATH\" =~ \\.go$ ]]; then gofmt -w \"$CLAUDE_FILE_PATH\"; fi"
}
]
}
}
Debugging Hooks
Hooks fail silently by default. To debug:
- Add
echostatements to trace execution - Check exit codes explicitly
- Redirect stderr to a log file
{
"command": "echo \"Hook fired: $CLAUDE_TOOL_NAME on $CLAUDE_FILE_PATH\" >> /tmp/claude-hooks.log"
}
Best Practices
- Keep hooks fast. They block tool execution.
- Use exit code 1 to abort operations in
PreToolUse. - Test hooks manually before relying on them.
- Don’t put secrets in hook commands (they’re stored in plain text).
- Use
|| truefor non-critical hooks that shouldn’t break the flow.
Hooks vs MCP vs CLAUDE.md
| Mechanism | Purpose | Determinism |
|---|---|---|
| Hooks | Automated shell commands | Always runs |
| MCP | External tool integration | Called when needed |
| CLAUDE.md | Project instructions | Suggestion only |
Hooks enforce rules. CLAUDE.md suggests them.
Next: Building Custom MCP Servers
Get updates
New guides, workflows, and AI patterns. No spam.
Thank you! You're on the list.