Claude Code Hooks Tutorial: Automate Your AI Coding Workflow
Learn how to use Claude Code hooks to automate tasks, enforce coding standards, and create powerful custom workflows. Complete tutorial with practical examples for PreToolUse, PostToolUse, and Notification hooks.
Claude Code Hooks Tutorial: Automate Your AI Coding Workflow
Hooks are one of the most powerful features in Claude Code, allowing you to automate tasks, enforce coding standards, and create custom workflows. This comprehensive tutorial covers everything from basic setup to advanced use cases.
What Are Claude Code Hooks?
Hooks are shell commands that automatically execute at specific points during Claude Code's operation. They enable you to:
- Automate repetitive tasks - Run formatters, linters, or tests automatically
- Enforce project standards - Block certain actions or require specific patterns
- Extend functionality - Integrate with external tools and services
- Create guardrails - Add safety checks before destructive operations
Hook Types Overview
Claude Code supports four types of hooks:
| Hook Type | When It Runs | Use Cases |
|---|---|---|
PreToolUse | Before a tool executes | Validation, approval, blocking |
PostToolUse | After a tool completes | Formatting, notifications, logging |
Notification | When Claude sends notifications | Alerts, integrations, logging |
Stop | When Claude finishes a task | Cleanup, verification, reporting |
Setting Up Hooks
Hooks are configured in your .claude/settings.json file:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit",
"command": "echo 'About to edit: $CLAUDE_FILE_PATH'"
}
],
"PostToolUse": [
{
"matcher": "Edit",
"command": "npx prettier --write $CLAUDE_FILE_PATH"
}
]
}
}
Configuration Location
Hooks can be defined in:
- Project-level:
.claude/settings.jsonin your project root - User-level:
~/.claude/settings.jsonfor global hooks - CLAUDE.md: Inline hook definitions (limited)
PreToolUse Hooks
PreToolUse hooks run before Claude executes a tool. They can:
- Log what's about to happen
- Block operations by returning non-zero exit codes
- Modify behavior based on conditions
Basic Example: Logging All Edits
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit",
"command": "echo \"[$(date)] Editing: $CLAUDE_FILE_PATH\" >> ~/.claude/edit.log"
}
]
}
}
Blocking Dangerous Operations
Prevent edits to critical files:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit",
"command": "if [[ \"$CLAUDE_FILE_PATH\" == *\".env\"* ]]; then echo 'Cannot edit .env files!' && exit 1; fi"
}
]
}
}
Requiring Confirmation for Destructive Commands
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "if [[ \"$CLAUDE_COMMAND\" == *\"rm -rf\"* ]]; then echo 'Blocked: rm -rf is not allowed' && exit 1; fi"
}
]
}
}
Pattern Matching
The matcher field supports:
- Exact match:
"Edit","Bash","Write" - Wildcard:
"*"matches all tools - Regex:
"/Edit|Write/"matches Edit or Write
PostToolUse Hooks
PostToolUse hooks run after a tool completes successfully. Perfect for:
- Auto-formatting code
- Running linters
- Updating documentation
- Sending notifications
Auto-Format After Edits
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"command": "npx prettier --write \"$CLAUDE_FILE_PATH\" 2>/dev/null || true"
},
{
"matcher": "Write",
"command": "npx prettier --write \"$CLAUDE_FILE_PATH\" 2>/dev/null || true"
}
]
}
}
Run ESLint After JavaScript Changes
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"command": "if [[ \"$CLAUDE_FILE_PATH\" == *.js ]] || [[ \"$CLAUDE_FILE_PATH\" == *.ts ]]; then npx eslint --fix \"$CLAUDE_FILE_PATH\"; fi"
}
]
}
}
Auto-Run Tests
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"command": "if [[ \"$CLAUDE_FILE_PATH\" == *\"test\"* ]]; then npm test -- --findRelatedTests \"$CLAUDE_FILE_PATH\"; fi"
}
]
}
}
Notification Hooks
Notification hooks trigger when Claude sends a notification (task completion, errors, etc.).
Desktop Notifications (macOS)
{
"hooks": {
"Notification": [
{
"matcher": "*",
"command": "osascript -e 'display notification \"$CLAUDE_MESSAGE\" with title \"Claude Code\"'"
}
]
}
}
Desktop Notifications (Linux)
{
"hooks": {
"Notification": [
{
"matcher": "*",
"command": "notify-send 'Claude Code' \"$CLAUDE_MESSAGE\""
}
]
}
}
Slack Integration
{
"hooks": {
"Notification": [
{
"matcher": "*",
"command": "curl -X POST -H 'Content-type: application/json' --data '{\"text\":\"Claude Code: '$CLAUDE_MESSAGE'\"}' $SLACK_WEBHOOK_URL"
}
]
}
}
Stop Hooks
Stop hooks run when Claude completes a task or conversation turn.
Generate Summary Report
{
"hooks": {
"Stop": [
{
"matcher": "*",
"command": "echo \"Task completed at $(date)\" >> ~/.claude/completed.log"
}
]
}
}
Auto-Commit on Completion
{
"hooks": {
"Stop": [
{
"matcher": "*",
"command": "if [ -n \"$(git status --porcelain)\" ]; then git add -A && git commit -m 'Auto-commit: Claude Code changes'; fi"
}
]
}
}
Environment Variables
Hooks have access to these environment variables:
| Variable | Description | Available In |
|---|---|---|
CLAUDE_FILE_PATH | Path of file being edited | Edit, Write, Read |
CLAUDE_TOOL_NAME | Name of the current tool | All hooks |
CLAUDE_COMMAND | Bash command being run | Bash tool |
CLAUDE_MESSAGE | Notification message | Notification hooks |
CLAUDE_SESSION_ID | Current session ID | All hooks |
CLAUDE_WORKING_DIR | Current working directory | All hooks |
Advanced Hook Patterns
Conditional Hooks Based on File Type
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"command": "case \"$CLAUDE_FILE_PATH\" in *.py) black \"$CLAUDE_FILE_PATH\";; *.js|*.ts) npx prettier --write \"$CLAUDE_FILE_PATH\";; *.go) gofmt -w \"$CLAUDE_FILE_PATH\";; esac"
}
]
}
}
Chain Multiple Actions
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"command": "npx prettier --write \"$CLAUDE_FILE_PATH\" && npx eslint --fix \"$CLAUDE_FILE_PATH\" && echo 'Formatted and linted!'"
}
]
}
}
Using External Scripts
Create a hook script ~/.claude/hooks/post-edit.sh:
#!/bin/bash
FILE="$CLAUDE_FILE_PATH"
# Format based on extension
case "${FILE##*.}" in
py)
black "$FILE"
isort "$FILE"
;;
js|ts|jsx|tsx)
npx prettier --write "$FILE"
npx eslint --fix "$FILE"
;;
go)
gofmt -w "$FILE"
;;
esac
# Log the edit
echo "[$(date)] Edited: $FILE" >> ~/.claude/edits.log
Reference it in settings:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"command": "~/.claude/hooks/post-edit.sh"
}
]
}
}
Project-Specific Hooks
In your project's .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "if [[ \"$CLAUDE_COMMAND\" == *\"npm publish\"* ]]; then echo 'Run tests first!' && npm test || exit 1; fi"
}
],
"PostToolUse": [
{
"matcher": "Edit",
"command": "npm run lint:fix -- \"$CLAUDE_FILE_PATH\""
}
],
"Stop": [
{
"matcher": "*",
"command": "npm run typecheck"
}
]
}
}
Debugging Hooks
Enable Verbose Logging
{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"command": "echo \"[DEBUG] Tool: $CLAUDE_TOOL_NAME, File: $CLAUDE_FILE_PATH\" >> /tmp/claude-hooks.log"
}
]
}
}
Test Hooks Manually
# Simulate environment variables
export CLAUDE_FILE_PATH="/path/to/test.js"
export CLAUDE_TOOL_NAME="Edit"
# Run your hook command
npx prettier --write "$CLAUDE_FILE_PATH"
Common Hook Recipes
1. Enforce Branch Protection
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "BRANCH=$(git branch --show-current); if [[ \"$BRANCH\" == \"main\" || \"$BRANCH\" == \"master\" ]]; then echo 'Cannot run commands on protected branch!' && exit 1; fi"
}
]
}
}
2. Auto-Update Tests
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"command": "TEST_FILE=$(echo \"$CLAUDE_FILE_PATH\" | sed 's/\\.ts$/.test.ts/'); if [ -f \"$TEST_FILE\" ]; then echo \"Remember to update: $TEST_FILE\"; fi"
}
]
}
}
3. Security Scanning
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"command": "if grep -q 'password\\|secret\\|api_key' \"$CLAUDE_FILE_PATH\"; then echo 'WARNING: Possible sensitive data detected!'; fi"
}
]
}
}
4. Documentation Reminder
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"command": "if [[ \"$CLAUDE_FILE_PATH\" == *\"/src/\"* ]]; then echo 'Remember to update documentation if this is a public API!'; fi"
}
]
}
}
Best Practices
- Keep hooks fast - Slow hooks will slow down your entire workflow
- Handle errors gracefully - Use
|| truefor non-critical hooks - Use exit codes correctly - Return non-zero to block operations in PreToolUse
- Log important events - Create an audit trail for team projects
- Test hooks thoroughly - Bad hooks can break your workflow
- Document project hooks - Help team members understand custom behaviors
Troubleshooting
Hook Not Running
- Check the
matcherpattern matches the tool name exactly - Verify the command syntax is correct
- Check file permissions for external scripts
- Look for typos in settings.json
Hook Blocks Everything
- Check your PreToolUse exit codes
- Verify conditional logic is correct
- Test the command manually
Performance Issues
- Move heavy operations to PostToolUse or Stop hooks
- Use async operations where possible
- Consider using background processes:
command &
Related Resources
- Claude Code Commands Reference - All CLI commands
- CLAUDE.md Guide - Project configuration
- Best Practices Guide - Workflow optimization
Last updated: January 2025