Claude Code hooks are powerful automation triggers that execute shell commands at key points during your coding sessions. They provide deterministic control over Claude's behavior, ensuring specific actions always happen rather than relying on the AI to choose them.
Understanding Hook Events
Claude Code supports multiple hook events that fire at different points in the session lifecycle:
| Hook Event | When It Fires | Common Use Cases |
|---|---|---|
| SessionStart | When session begins or resumes | Set environment variables, load configs |
| UserPromptSubmit | When you submit a prompt, before processing | Input validation, context injection |
| PreToolUse | Before Claude executes a tool | Block dangerous commands, validate inputs |
| PermissionRequest | When permission dialog appears | Auto-approve/deny based on rules |
| PostToolUse | After a tool completes successfully | Run linters, formatters, tests |
| PostToolUseFailure | After a tool fails | Error logging, notifications |
| Stop | When Claude finishes responding | Notifications, cleanup tasks |
| SubagentStop | When a subagent finishes | Subagent-specific cleanup |
| Notification | When Claude sends notifications | Custom notification routing |
| SessionEnd | When session terminates | Final cleanup, logging |
Configuration Basics
Configuration File Locations
Hooks can be configured at three levels:
- User settings:
~/.claude/settings.json- applies globally to all projects - Project settings:
.claude/settings.json- version-controlled, shared with team - Local project:
.claude/settings.local.json- local overrides, not committed
Basic Configuration Structure
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npm run lint --fix",
"timeout": 60
}
]
}
]
}
}
Using the Interactive Configuration
The easiest way to configure hooks is through Claude Code's built-in interface:
- Start Claude Code in your project directory
- Type
/hooksto open the hook configuration menu - Select an event type (e.g., PostToolUse)
- Add a matcher pattern (e.g.,
Write|Edit) - Enter your command
This interactive method is recommended for beginners as it validates your configuration automatically.
Matcher Syntax
Matchers determine which tools trigger your hooks. They apply to PreToolUse, PermissionRequest, and PostToolUse events.
Matching Patterns
// Exact tool match
"matcher": "Write"
// Multiple tools with pipe syntax
"matcher": "Edit|Write|MultiEdit"
// Regex pattern
"matcher": "Notebook.*"
// Match all tools
"matcher": "*"
Common Tool Names
| Tool | Description |
|---|---|
Bash | Shell commands |
Read | Reading files |
Write | Creating new files |
Edit | Modifying existing files |
MultiEdit | Multiple file edits |
Glob | File pattern matching |
Grep | Content searching |
WebFetch | Fetching web content |
Task | Subagent tasks |
mcp__* | MCP server tools |
Practical Hook Examples
Automatic Code Formatting
Run Prettier on JavaScript/TypeScript files after every edit:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$(echo $CLAUDE_FILE_PATHS | tr ',' ' ')\" 2>/dev/null || true"
}
]
}
]
}
}
Run Tests on File Changes
Automatically run related tests when test files are modified:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "if [[ \"$CLAUDE_FILE_PATHS\" == *\".test.\"* ]]; then npm test -- --related; fi"
}
]
}
]
}
}
TypeScript Type Checking
Run type checking after editing TypeScript files:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "if [[ \"$CLAUDE_FILE_PATHS\" == *\".ts\"* ]]; then npx tsc --noEmit; fi"
}
]
}
]
}
}
Block Dangerous Commands
Prevent accidental destructive operations:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -qE 'rm -rf /|DROP DATABASE|format c:'; then echo 'Dangerous command blocked!' >&2; exit 2; fi"
}
]
}
]
}
}
Protect Sensitive Files
Block modifications to critical files:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "python3 -c \"import json, sys; data=json.load(sys.stdin); path=data.get('tool_input',{}).get('file_path',''); sys.exit(2 if any(p in path for p in ['.env', 'package-lock.json', '.git/', 'secrets']) else 0)\""
}
]
}
]
}
}
macOS Desktop Notifications
Get notified when Claude finishes a task (macOS):
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude has finished the task\" with title \"Claude Code\"'"
}
]
}
]
}
}
Windows Desktop Notifications
PowerShell notification for Windows:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "powershell -Command \"[System.Windows.Forms.MessageBox]::Show('Claude has finished the task', 'Claude Code')\""
}
]
}
]
}
}
Linux Desktop Notifications
Using notify-send on Linux:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "notify-send 'Claude Code' 'Claude has finished the task'"
}
]
}
]
}
}
Command Logging
Log all bash commands for compliance or debugging:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '\"[\\(now | strftime(\"%Y-%m-%d %H:%M:%S\"))] \\(.tool_input.command)\"' >> ~/.claude/command-log.txt"
}
]
}
]
}
}
Environment Variables
Hooks have access to several environment variables:
| Variable | Description |
|---|---|
CLAUDE_PROJECT_DIR | Absolute path to the project root |
CLAUDE_ENV_FILE | File path for persisting environment variables (SessionStart only) |
CLAUDE_CODE_REMOTE | "true" if running in remote/web environment |
CLAUDE_PLUGIN_ROOT | Path to plugin directory (for plugin hooks) |
Persisting Environment Variables
Use SessionStart hooks to set environment variables for the entire session:
#!/bin/bash
# ~/.claude/hooks/session-init.sh
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo 'export NODE_ENV=development' >> "$CLAUDE_ENV_FILE"
echo 'export DEBUG=true' >> "$CLAUDE_ENV_FILE"
fi
exit 0
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-init.sh"
}
]
}
]
}
}
Hook Input and Output
Input Format
Hooks receive JSON data via stdin containing context about the operation:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/current/working/directory",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm install",
"description": "Install dependencies"
}
}
Exit Codes
| Exit Code | Behavior |
|---|---|
| 0 | Success - continue execution |
| 2 | Blocking error - stop the action (PreToolUse/PermissionRequest only) |
| Other | Non-blocking error - show warning, continue |
JSON Output for Advanced Control
Hooks can return JSON to control behavior:
{
"continue": true,
"suppressOutput": false,
"systemMessage": "Warning: Consider using pnpm instead of npm",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"additionalContext": "Approved for development environment"
}
}
CI/CD Integration
Headless Mode
For CI/CD pipelines, use Claude Code's headless mode with hooks:
# Run Claude Code non-interactively
claude -p "Fix all linting errors in src/" --output-format stream-json
GitHub Actions Example
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Claude Code
run: curl -fsSL https://claude.ai/install.sh | bash
- name: Run Claude Review
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
claude -p "Review the changes in this PR and check for:
1. Security vulnerabilities
2. Performance issues
3. Code style violations
Report findings in markdown format."
Pre-commit Hook Integration
Create a pre-commit hook that uses Claude Code:
#!/bin/bash
# .git/hooks/pre-commit
# Get staged files
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|ts|tsx)$')
if [ -n "$STAGED_FILES" ]; then
# Run Claude Code to check for issues
echo "$STAGED_FILES" | claude -p "Review these staged files for obvious bugs or security issues. Exit with code 1 if critical issues found."
if [ $? -ne 0 ]; then
echo "Claude found issues. Please fix before committing."
exit 1
fi
fi
Detecting Remote Environments
Use the CLAUDE_CODE_REMOTE variable to run different logic in CI:
#!/bin/bash
if [ "$CLAUDE_CODE_REMOTE" = "true" ]; then
# CI/CD environment - use stricter checks
npm run lint -- --max-warnings 0
npm run test -- --coverage --ci
else
# Local development - more relaxed
npm run lint
fi
Platform-Specific Notes
macOS
- Use
osascriptfor native notifications - Homebrew-installed tools are typically in
/opt/homebrew/bin(Apple Silicon) or/usr/local/bin(Intel) - File paths are case-insensitive by default
Windows
- Native Windows support available since Claude Code 2.x
- Use PowerShell for complex scripts
- WSL 2 provides sandboxing support if needed
- File paths use backslashes but Claude Code handles both
Linux
- Use
notify-sendfor desktop notifications (requires libnotify) - Ensure hook scripts have execute permissions:
chmod +x script.sh - SELinux/AppArmor may affect hook execution
Security Best Practices
Hooks execute arbitrary shell commands with your user permissions. Follow these guidelines:
- Validate inputs: Never trust data from stdin without validation
- Quote variables: Always use
"$VAR"not$VARto prevent word splitting - Block path traversal: Check for
..in file paths - Use absolute paths: Reference
$CLAUDE_PROJECT_DIRfor project files - Skip sensitive files: Don't process
.env,.git/, or key files - Review changes: Hooks require manual review in
/hooksmenu to take effect
Debugging Hooks
Enable Debug Output
claude --debug
This shows detailed hook execution information.
Verbose Mode
Press Ctrl+O in Claude Code to toggle verbose mode, which displays hook stdout.
Test Hooks Manually
Test your hook commands outside Claude Code first:
# Simulate hook input
echo '{"tool_name":"Bash","tool_input":{"command":"npm test"}}' | your-hook-script.sh
echo $? # Check exit code
Troubleshooting
Hook Not Executing
- Verify the hook is registered with
/hookscommand - Check the matcher pattern matches the tool name exactly
- Ensure the hook command is executable
- Review debug output with
claude --debug
Hook Times Out
Default timeout is 60 seconds. Increase it in your configuration:
{
"type": "command",
"command": "npm run build",
"timeout": 300
}
Permission Denied
Ensure hook scripts have execute permissions:
chmod +x ~/.claude/hooks/my-hook.sh
Additional Resources
- Claude Code Hooks Reference
- Claude Code Documentation
- GitHub: awesome-claude-code - Community hooks and skills
- Install Claude Code CLI - Get started with Claude Code
Need help automating your development workflow? Inventive HQ specializes in AI-powered development tooling and DevOps automation. Contact us to discuss how we can streamline your engineering processes.