Claude Code Hooks Guide: Automate Custom Workflows with Pre and Post Execution Hooks
What Are Claude Code Hooks and Why They Matter
Claude Code hooks are automated shell commands that execute in response to specific events during a Claude Code session. When Claude calls a tool (like Edit, Write, or Bash), hooks can run commands before or after the tool execution. This turns Claude Code from a standalone AI assistant into an integrated development workflow engine.
Think of hooks as CI/CD for your AI coding sessions. Just as a pre-commit hook runs linting before allowing a commit, a Claude Code PreToolUse hook can run validation before Claude writes a file. Just as a post-deploy hook sends a Slack notification, a PostToolUse hook can notify your team when Claude completes a significant change.
Common hook use cases:
- Auto-linting: run Prettier or ESLint after every file write
- Type checking: run tsc after TypeScript file modifications
- Test execution: run affected tests after code changes
- Notifications: send Slack/Discord messages when tasks complete
- Logging: track what Claude modifies for audit purposes
- Guardrails: block changes to protected files or directories
Hook Architecture
Hook Types
Claude Code supports three hook types:
PreToolUse: fires before a tool executes. Can block the tool if the hook fails (non-zero exit code).
PostToolUse: fires after a tool executes. Cannot block the tool (it already ran) but can trigger follow-up actions.
Notification: fires when Claude produces specific output or completes a task.
Hook Configuration Structure
Hooks are defined in settings.json (project-level at .claude/settings.json or global at ~/.claude/settings.json):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"command": "echo 'File modification detected'",
"timeout": 10000
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "/path/to/your/script.sh",
"timeout": 30000
}
]
}
}
Matcher Patterns
The matcher field determines which tool calls trigger the hook:
"Edit"— matches only the Edit tool"Write"— matches only the Write tool"Edit|Write"— matches Edit or Write"Bash"— matches Bash tool calls".*"— matches all tool calls (use with caution)
Environment Variables Available to Hooks
When a hook runs, Claude Code passes context through environment variables:
CLAUDE_TOOL_NAME— which tool was called (Edit, Write, Bash, etc.)CLAUDE_FILE_PATH— the file being modified (for Edit/Write)CLAUDE_TOOL_INPUT— JSON string of the tool’s input parametersCLAUDE_SESSION_ID— unique identifier for the current session
Setting Up Common Hook Workflows
Auto-Format on File Write
Run Prettier after every file modification:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "npx prettier --write \"$CLAUDE_FILE_PATH\" 2>/dev/null || true",
"timeout": 10000
}
]
}
}
The || true ensures the hook does not fail if Prettier encounters an unsupported file type.
Type Check After TypeScript Changes
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "if [[ \"$CLAUDE_FILE_PATH\" == *.ts ]] || [[ \"$CLAUDE_FILE_PATH\" == *.tsx ]]; then npx tsc --noEmit --pretty 2>&1 | head -20; fi",
"timeout": 30000
}
]
}
}
This only runs tsc when TypeScript files are modified, and limits output to 20 lines to avoid flooding the conversation.
Run Affected Tests
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "if [[ \"$CLAUDE_FILE_PATH\" == *.ts ]] || [[ \"$CLAUDE_FILE_PATH\" == *.tsx ]]; then npx vitest related \"$CLAUDE_FILE_PATH\" --run 2>&1 | tail -10; fi",
"timeout": 60000
}
]
}
}
vitest related runs only the tests that import the modified file — fast and targeted.
Protect Sensitive Files
Block Claude from modifying certain files:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"command": "if echo \"$CLAUDE_FILE_PATH\" | grep -qE '\\.(env|pem|key)$|secrets/|credentials'; then echo 'BLOCKED: Cannot modify sensitive files' >&2; exit 1; fi",
"timeout": 5000
}
]
}
}
A non-zero exit code from a PreToolUse hook blocks the tool execution. Claude will see the error message and adjust its approach.
Slack Notification on Task Completion
{
"hooks": {
"Notification": [
{
"matcher": ".*",
"command": "curl -s -X POST -H 'Content-Type: application/json' -d '{\"text\":\"Claude Code completed a task in '\"$PWD\"'\"}' $SLACK_WEBHOOK_URL",
"timeout": 5000
}
]
}
}
Git Auto-Stage After Changes
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "git add \"$CLAUDE_FILE_PATH\" 2>/dev/null || true",
"timeout": 5000
}
]
}
}
This auto-stages every file Claude modifies, making it easy to see all changes with git diff --staged.
Advanced Hook Patterns
Conditional Hooks Based on File Type
Create a hook script that handles different file types:
#!/bin/bash
# hooks/post-edit.sh
FILE="$CLAUDE_FILE_PATH"
EXT="${FILE##*.}"
case "$EXT" in
ts|tsx)
npx prettier --write "$FILE" 2>/dev/null
npx tsc --noEmit --pretty 2>&1 | head -10
;;
py)
python -m black "$FILE" 2>/dev/null
python -m mypy "$FILE" --no-error-summary 2>&1 | head -10
;;
css|scss)
npx prettier --write "$FILE" 2>/dev/null
npx stylelint "$FILE" 2>&1 | head -5
;;
sql)
# Validate SQL syntax
echo "$FILE modified - manual review recommended for SQL changes"
;;
esac
Reference it in settings:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "bash .claude/hooks/post-edit.sh",
"timeout": 30000
}
]
}
}
Audit Logging
Track every modification Claude makes:
#!/bin/bash # hooks/audit-log.sh LOG_FILE=".claude/audit.log" TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") echo "$TIMESTAMP | $CLAUDE_TOOL_NAME | $CLAUDE_FILE_PATH | session:$CLAUDE_SESSION_ID" >> "$LOG_FILE"
Dependency Check After Package Changes
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "if [[ \"$CLAUDE_FILE_PATH\" == *package.json ]]; then npm ls --depth=0 2>&1 | grep -i 'ERR\\|WARN' | head -5; fi",
"timeout": 30000
}
]
}
}
Hook Configuration Best Practices
Keep Hooks Fast
Hooks run synchronously — a slow hook blocks Claude. Keep hooks under 10 seconds for PreToolUse and under 30 seconds for PostToolUse. If a process takes longer, run it asynchronously:
"command": "nohup bash .claude/hooks/slow-check.sh &>/dev/null &"
Handle Errors Gracefully
Always add || true or proper error handling to prevent hooks from crashing the session:
"command": "npx prettier --write \"$CLAUDE_FILE_PATH\" 2>/dev/null || true"
Use Project-Level Hooks
Store hooks in .claude/settings.json at the project root so they are:
- Version-controlled with the project
- Shared automatically with all team members
- Specific to the project’s technology stack
Test Hooks Independently
Before adding a hook to settings, test it manually:
CLAUDE_FILE_PATH="src/app.ts" CLAUDE_TOOL_NAME="Edit" bash .claude/hooks/post-edit.sh
Document Your Hooks
Add a comment section to your settings.json or create a .claude/HOOKS.md explaining what each hook does and why.
Troubleshooting Common Hook Issues
Hook Does Not Fire
- Verify the
matcherpattern matches the tool name exactly - Check that the settings.json is in the correct location
- Restart Claude Code after modifying settings.json
Hook Blocks Tool Execution Unexpectedly
- PreToolUse hooks block on non-zero exit codes
- Add
|| trueif the hook should warn but not block - Check that error output goes to stderr, not stdout
Hook Output Clutters the Conversation
- Redirect verbose output:
command 2>/dev/null - Limit output:
command | head -5 - Send output to a log file instead:
command >> .claude/hook.log 2>&1
Hook Timeout
- Increase the
timeoutvalue (in milliseconds) - For slow operations, run asynchronously with
nohup ... & - Optimize the hook script (avoid unnecessary operations)
Frequently Asked Questions
Can hooks modify Claude’s behavior?
PreToolUse hooks can block tool calls by returning a non-zero exit code. The error message (stderr) is shown to Claude, which can adjust its approach. PostToolUse hooks cannot modify what Claude already did but can trigger side effects.
Are hooks shared when pairing with Claude Code?
Hooks in .claude/settings.json (project-level) are shared with anyone who clones the repository. Hooks in ~/.claude/settings.json (global) are personal to each user.
Can I use hooks with Claude Code in VS Code?
Yes. The VS Code extension reads the same settings.json files. Hooks work identically in the terminal CLI and VS Code extension.
Do hooks work with all Claude Code tools?
Yes. The matcher can target any tool: Edit, Write, Bash, Read, Glob, Grep, and any MCP tools. Use the tool name as the matcher string.
Can hooks access the content Claude is writing?
The CLAUDE_TOOL_INPUT environment variable contains the JSON input parameters, which includes the content for Write/Edit operations. Parse it with jq or your preferred JSON tool.
Is there a performance impact from running hooks?
Yes. Each hook adds execution time to every matching tool call. Keep hooks fast (under 5 seconds) and use specific matchers to avoid running hooks unnecessarily.