Hooks¶
Automate actions with lifecycle hooks that run before or after Claude operations.
What You'll Learn¶
- What hooks are and when to use them
- Available hook types
- Creating hook scripts
- Real-world hook examples
What Are Hooks?¶
Hooks are shell commands that run automatically at specific points in Claude Code's lifecycle. They let you:
- Run linting before commits
- Notify when Claude modifies files
- Validate changes before they're applied
- Log operations for auditing
Available Hooks¶
| Hook | When It Runs |
|---|---|
pre-tool-use |
Before any tool runs |
post-tool-use |
After any tool runs |
notification |
When Claude has a notification |
user-prompt-submit |
When you submit a prompt |
stop |
When Claude stops or crashes |
Configuration¶
Hooks are configured in Claude settings. Access with:
Or edit the settings file directly:
- Project: .claude/settings.json
- User: ~/.claude/settings.json
Basic Structure¶
Hook Matchers¶
Matchers filter which operations trigger the hook:
By Tool Name¶
Matches only Edit operations.
By Pattern¶
Matches Python file edits.
All Operations¶
Environment Variables¶
Hooks receive context via environment variables:
| Variable | Content |
|---|---|
CLAUDE_TOOL_NAME |
Name of the tool |
CLAUDE_TOOL_ARGS |
JSON of arguments |
CLAUDE_TOOL_RESULT |
Result (post-tool only) |
CLAUDE_SESSION_ID |
Current session ID |
CLAUDE_WORKING_DIR |
Working directory |
Real-World Examples¶
Example 1: Lint on Edit¶
Run linting when Python files are modified:
{
"hooks": {
"post-tool-use": [
{
"matcher": "Edit:*.py",
"command": "ruff check ${CLAUDE_TOOL_ARGS} --fix"
}
]
}
}
Example 2: Format on Save¶
Auto-format edited files:
{
"hooks": {
"post-tool-use": [
{
"matcher": "Edit:*.ts",
"command": "prettier --write $(echo $CLAUDE_TOOL_ARGS | jq -r '.file_path')"
},
{
"matcher": "Edit:*.tsx",
"command": "prettier --write $(echo $CLAUDE_TOOL_ARGS | jq -r '.file_path')"
}
]
}
}
Example 3: Security Check¶
Block editing of sensitive files:
Create a script ~/.claude/scripts/security-check.sh:
#!/bin/bash
FILE_PATH=$(echo "$CLAUDE_TOOL_ARGS" | jq -r '.file_path')
# Block sensitive files
if [[ "$FILE_PATH" == *.env* ]] || [[ "$FILE_PATH" == *secret* ]]; then
echo "BLOCKED: Cannot edit sensitive file: $FILE_PATH"
exit 1
fi
exit 0
Configure:
{
"hooks": {
"pre-tool-use": [
{
"matcher": "Edit",
"command": "~/.claude/scripts/security-check.sh"
}
]
}
}
Example 4: Logging¶
Log all Claude operations:
#!/bin/bash
# ~/.claude/scripts/log-operation.sh
LOG_FILE="$HOME/.claude/operations.log"
TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M:%S")
echo "[$TIMESTAMP] $CLAUDE_TOOL_NAME: $CLAUDE_TOOL_ARGS" >> "$LOG_FILE"
{
"hooks": {
"pre-tool-use": [
{
"matcher": "*",
"command": "~/.claude/scripts/log-operation.sh"
}
]
}
}
Example 5: Notification¶
Send desktop notification on completion:
{
"hooks": {
"notification": [
{
"command": "notify-send 'Claude Code' \"$CLAUDE_NOTIFICATION\""
}
]
}
}
Hook Script Best Practices¶
Make Them Fast¶
Hooks run synchronously. Slow hooks = slow Claude.
# Bad: Slow operation
npm test # Could take minutes
# Good: Quick check
npm run lint:quick # Just syntax check
Handle Errors Gracefully¶
#!/bin/bash
if ! command -v prettier &> /dev/null; then
echo "prettier not found, skipping format"
exit 0 # Don't fail the operation
fi
prettier --write "$FILE_PATH"
Return Appropriate Exit Codes¶
exit 0- Success, continueexit 1- Failure, block operation (for pre-hooks)
Log for Debugging¶
#!/bin/bash
DEBUG_LOG="/tmp/claude-hook-debug.log"
echo "Hook called at $(date)" >> "$DEBUG_LOG"
echo "Tool: $CLAUDE_TOOL_NAME" >> "$DEBUG_LOG"
echo "Args: $CLAUDE_TOOL_ARGS" >> "$DEBUG_LOG"
Blocking Operations¶
Pre-hooks can block operations by returning non-zero:
#!/bin/bash
# Block edits after 6pm
HOUR=$(date +%H)
if [ "$HOUR" -ge 18 ]; then
echo "BLOCKED: No edits after 6pm"
exit 1
fi
exit 0
When blocked, Claude sees the message and can adjust.
Complex Hook Patterns¶
Chain Multiple Actions¶
{
"hooks": {
"post-tool-use": [
{
"matcher": "Edit:*.ts",
"command": "npm run format && npm run lint && npm run typecheck"
}
]
}
}
Conditional Hooks¶
#!/bin/bash
# Only run on main branch
BRANCH=$(git branch --show-current)
if [ "$BRANCH" = "main" ]; then
npm run strict-lint
else
npm run lint
fi
Tool-Specific Logic¶
#!/bin/bash
case "$CLAUDE_TOOL_NAME" in
"Edit")
run_linter
;;
"Write")
validate_new_file
;;
"Bash")
log_command
;;
esac
Try It Yourself¶
Exercise: Create a Formatting Hook¶
-
Create a hook script:
-
Configure the hook:
-
Test it:
Watch for the hook output.
Exercise: Operation Logger¶
Create a hook that logs all operations to a file, including: - Timestamp - Tool used - Files affected - Success/failure
What's Next?¶
Extend Claude with external services using MCP in 03-mcp-servers.
Summary: - Hooks run at lifecycle points: pre-tool, post-tool, notification, prompt submit - Matchers filter which operations trigger hooks - Environment variables provide context to hook scripts - Pre-hooks can block operations by returning non-zero - Keep hooks fast and handle errors gracefully
Learning Resources¶
Featured Video¶
Edmund Yong: 800+ Hours Learning Claude Code - Hooks (Popular tech channel)
Configure lifecycle hooks for automated linting, formatting, and security checks.
Additional Resources¶
| Type | Resource | Description |
|---|---|---|
| 🎬 Video | Git Hooks Tutorial | Git hook automation basics |
| 📚 Official Docs | Hooks Documentation | Official hooks reference |
| 📖 Tutorial | Plugins & Hooks | Anthropic plugin guide |
| 🎓 Free Course | Awesome Claude Code | Community hooks collection |
| 💼 Commercial | Git Complete | Advanced hook patterns |