Skip to main content

Hooks

Automate actions in response to Claude Code events.

This page explains how to create hooks that run automatically when Claude Code performs actions. Hooks are one of the most powerful customization features — they let you integrate Claude Code into your existing workflow.

What are Hooks?

Hooks are shell commands that execute automatically when specific events occur. They're the glue between Claude Code and your other tools. Use them to:

  • Run formatters after file writes
  • Validate changes before commits
  • Inject context into prompts
  • Block dangerous operations
  • Send notifications

Hook Events

Each event fires at a specific point in Claude Code's operation. Understanding when they fire helps you choose the right one:

EventTimingCommon Use Cases
PreToolUseBefore tool executesBlock/modify tool calls
PostToolUseAfter successful executionRun linters, formatters
PermissionRequestWhen permission dialog appearsAuto-allow/deny
UserPromptSubmitWhen user submits promptInject context
SessionStartSession beginsInitialize environment
SessionEndSession endsCleanup tasks
StopAgent finishes responseForce continuation
NotificationAlert sentCustom notifications

Configuration

Hooks are defined in your settings file (.claude/settings.json or ~/.claude/settings.json). Here's the basic structure:

{
"hooks": {
"PostToolUse": [
{
"matcher": { "tool_name": "Write" },
"command": "prettier --write $FILE_PATH"
}
],
"PreToolUse": [
{
"matcher": { "tool_name": "Bash", "command": "rm *" },
"command": "echo 'Blocked dangerous delete' && exit 2"
}
]
}
}

Matchers

Matchers determine which tool invocations trigger your hook. Without matchers, hooks run on every tool use of that type:

{
"matcher": {
"tool_name": "Write", // Tool name
"file_path": "*.ts", // Glob pattern
"command": "npm *" // Command pattern (Bash)
}
}

Environment Variables

Hooks receive context via environment variables:

VariableDescription
$TOOL_NAMEName of the tool
$FILE_PATHFile being operated on
$COMMANDCommand being run (Bash)
$SESSION_IDCurrent session ID
$PROMPTUser's prompt (UserPromptSubmit)

Exit Codes

Your hook's exit code tells Claude Code how to proceed. This is how you can block dangerous operations:

Exit CodeEffect
0Success, continue
2Block the action
OtherLog warning, continue

JSON Responses

Hooks can return JSON for advanced control:

{
"decision": "allow", // or "block"
"reason": "Auto-approved", // Optional message
"continue": true, // Continue processing
"updatedInput": "..." // Modify input
}

Examples

These examples show common hook patterns. Copy and adapt them for your workflow:

Auto-format on Write

{
"hooks": {
"PostToolUse": [
{
"matcher": { "tool_name": "Write", "file_path": "*.{ts,tsx,js,jsx}" },
"command": "prettier --write $FILE_PATH && eslint --fix $FILE_PATH"
}
]
}
}

Block Production Commands

{
"hooks": {
"PreToolUse": [
{
"matcher": { "tool_name": "Bash", "command": "*production*" },
"command": "echo 'Production commands blocked' && exit 2"
}
]
}
}

Inject Git Context

{
"hooks": {
"UserPromptSubmit": [
{
"command": "echo '{\"updatedInput\": \"Current branch: '$(git branch --show-current)'. $PROMPT\"}'"
}
]
}
}

Run Tests After Changes

{
"hooks": {
"PostToolUse": [
{
"matcher": { "tool_name": "Write", "file_path": "src/**/*.ts" },
"command": "npm test -- --related $FILE_PATH"
}
]
}
}

Timeout

Hooks timeout after 60 seconds by default. Configure per-hook:

{
"matcher": { "tool_name": "Write" },
"command": "slow-linter $FILE_PATH",
"timeout": 120000
}

Managing Hooks

/hooks          # View and manage hooks
/hooks add # Add new hook
/hooks remove # Remove hook

Advanced Examples

These examples demonstrate more sophisticated hook patterns for real-world workflows.

Conditional Hook: Run Tests Only for Source Files

Only run tests when source files (not tests or configs) are modified:

{
"hooks": {
"PostToolUse": [
{
"matcher": { "tool_name": "Write", "file_path": "src/**/*.{ts,tsx}" },
"command": "npm test -- --related $FILE_PATH --passWithNoTests"
}
]
}
}

Language-Specific Formatters

Apply different formatters based on file extension:

{
"hooks": {
"PostToolUse": [
{
"matcher": { "tool_name": "Write", "file_path": "*.py" },
"command": "black $FILE_PATH && isort $FILE_PATH"
},
{
"matcher": { "tool_name": "Write", "file_path": "*.go" },
"command": "gofmt -w $FILE_PATH"
},
{
"matcher": { "tool_name": "Write", "file_path": "*.rs" },
"command": "rustfmt $FILE_PATH"
},
{
"matcher": { "tool_name": "Write", "file_path": "*.{ts,tsx,js,jsx}" },
"command": "prettier --write $FILE_PATH"
}
]
}
}

Block Dangerous Patterns

Prevent accidental destructive commands:

{
"hooks": {
"PreToolUse": [
{
"matcher": { "tool_name": "Bash", "command": "rm -rf *" },
"command": "echo 'Blocked: recursive force delete' && exit 2"
},
{
"matcher": { "tool_name": "Bash", "command": "*DROP TABLE*" },
"command": "echo 'Blocked: DROP TABLE commands' && exit 2"
},
{
"matcher": { "tool_name": "Bash", "command": "*DELETE FROM*WHERE*" },
"command": "echo 'Blocked: DELETE without review' && exit 2"
},
{
"matcher": { "tool_name": "Bash", "command": "git push --force*" },
"command": "echo 'Blocked: force push requires manual execution' && exit 2"
},
{
"matcher": { "tool_name": "Bash", "command": "*main*" },
"command": "if echo \"$COMMAND\" | grep -qE 'checkout|merge|push.*main'; then echo 'Blocked: main branch operations' && exit 2; fi"
}
]
}
}

Inject Project Context

Add relevant context to every prompt automatically:

{
"hooks": {
"UserPromptSubmit": [
{
"command": "echo '{\"updatedInput\": \"[Context: Branch=$(git branch --show-current 2>/dev/null || echo none), Last commit=$(git log -1 --oneline 2>/dev/null || echo none)] $PROMPT\"}'"
}
]
}
}

Type Checking After TypeScript Changes

Run type checker when TypeScript files change:

{
"hooks": {
"PostToolUse": [
{
"matcher": { "tool_name": "Write", "file_path": "*.{ts,tsx}" },
"command": "npx tsc --noEmit --skipLibCheck 2>&1 | head -20 || true"
}
]
}
}

Lint Staged Files Before Commit

When Claude runs git commit, lint the staged files first:

{
"hooks": {
"PreToolUse": [
{
"matcher": { "tool_name": "Bash", "command": "git commit*" },
"command": "npx lint-staged || (echo 'Lint failed - fix issues first' && exit 2)"
}
]
}
}

Notification on Session End

Send a notification when a long session completes:

{
"hooks": {
"SessionEnd": [
{
"command": "osascript -e 'display notification \"Claude Code session ended\" with title \"Session Complete\"'"
}
]
}
}

On Linux with notify-send:

{
"hooks": {
"SessionEnd": [
{
"command": "notify-send 'Claude Code' 'Session ended'"
}
]
}
}

Auto-Stage Written Files

Automatically stage files that Claude writes:

{
"hooks": {
"PostToolUse": [
{
"matcher": { "tool_name": "Write" },
"command": "git add $FILE_PATH 2>/dev/null || true"
}
]
}
}

Debugging Hooks

When hooks don't work as expected, use these techniques:

Test Hook Commands Manually

Run your hook command directly to verify it works:

# Set the environment variable manually
export FILE_PATH="src/app.ts"

# Run your hook command
prettier --write $FILE_PATH && eslint --fix $FILE_PATH

Add Logging to Hooks

Temporarily add logging to understand what's happening:

{
"hooks": {
"PostToolUse": [
{
"matcher": { "tool_name": "Write" },
"command": "echo \"Hook triggered for: $FILE_PATH\" >> /tmp/claude-hooks.log && prettier --write $FILE_PATH"
}
]
}
}

Check Exit Codes

Verify your commands return correct exit codes:

# This should exit 0 (success)
prettier --write test.ts; echo "Exit code: $?"

# This should exit 2 (block) when you want to prevent an action
echo "Blocked" && exit 2; echo "Exit code: $?"

Common Hook Issues

ProblemLikely CauseSolution
Hook never runsMatcher pattern doesn't matchCheck glob patterns with `echo "src/app.ts"
Hook runs but fails silentlyCommand not foundUse full paths or ensure tools are in PATH
Hook blocks unexpectedlyExit code not 0Add `
Variables emptyWrong variable nameCheck exact variable names: $FILE_PATH, not $FILEPATH

Security Notes

Hooks are powerful, which means they can also be dangerous. Keep these in mind:

  • Hooks run with your user permissions — they can do anything you can do
  • Review hooks before enabling, especially from third parties
  • Be careful with hooks that modify updatedInput — they change what Claude sees
  • Direct edits to hook configs require /hooks review as a safety measure

For comprehensive security configuration, see Security Best Practices.