Version: 1.0.0 Based on: Anthropic Official Documentation
- Hooks Overview
- Hook Events
- Hook Configuration
- Hook Types
- Input/Output Formats
- Practical Examples
- Best Practices
Hooks are user-defined shell commands that execute at specific points in Claude Code's lifecycle. They provide deterministic and predictable behavior control.
| Use Case | Description |
|---|---|
| Validation | Block dangerous commands before execution |
| Automation | Auto-format/lint after file save |
| Auditing | Log all tool usage |
| Context | Automatically add information to prompts |
| Cleanup | Clean up temporary files on session end |
┌─────────────────────────────────────────────────────────────┐
│ Deterministic Validation │
│ Hook: "Block any command containing rm -rf" │
│ → Always same result, 100% predictable │
└─────────────────────────────────────────────────────────────┘
vs
┌─────────────────────────────────────────────────────────────┐
│ LLM Judgment │
│ System prompt: "Avoid dangerous commands" │
│ → Different judgments per situation, bypass possible │
└─────────────────────────────────────────────────────────────┘
| Event | Timing | Blockable | Primary Purpose |
|---|---|---|---|
PreToolUse |
Before tool execution | ✅ | Command validation, permission checks, input modification |
PostToolUse |
After tool execution | ❌ | Auto-formatting, logging, result processing |
UserPromptSubmit |
On prompt submission | ✅ | Input validation, context addition |
PermissionRequest |
On permission dialog display | ✅ | Auto-approve/deny |
SessionStart |
On session start/resume | ❌ | Environment initialization, context loading |
SessionEnd |
On session end | ❌ | Cleanup, logging, state saving |
Notification |
On notification dispatch | ❌ | Custom notifications |
Stop |
On response completion | ✅* | Decide whether to continue |
SubagentStop |
On subagent completion | ✅* | Evaluate subagent |
PreCompact |
Before context compaction | ❌ | Pre-compact processing |
*Stop/SubagentStop can send "continue" signals
SessionStart
↓
UserPromptSubmit ─────────────────────────────────┐
↓ │
PreToolUse ─────→ [Tool Execution] ─────→ PostToolUse │
↓ ↓ │
[Blocked?] [Repeat?] ───┘
↓ No ↓ No
[Tool Execution] Stop
↓ ↓
PostToolUse [Continue?] ─→ UserPromptSubmit
↓ ↓ No
... SessionEnd
| Scope | File | Shared | Priority |
|---|---|---|---|
| User | ~/.claude/settings.json |
Personal | Low |
| Project | .claude/settings.json |
Git shared | Medium |
| Local | .claude/settings.local.json |
Personal | High |
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Edit|Write",
"hooks": [
{
"type": "command",
"command": "/path/to/validator.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$CLAUDE_FILE_PATH\" 2>/dev/null || true"
}
]
}
]
}
}// Match specific tool
"matcher": "Bash"
// OR condition (pipe)
"matcher": "Edit|Write"
// All tools (omit matcher)
{
"hooks": [{ "type": "command", "command": "..." }]
}
// Regex pattern
"matcher": "Bash\\(npm.*\\)"Executes shell commands.
{
"type": "command",
"command": "/path/to/script.sh"
}Features:
- Fast and deterministic
- Receives JSON via stdin
- Returns result via exit code
- Access to environment variables
Sends context to LLM for evaluation.
{
"type": "prompt",
"prompt": "Evaluate whether this command might affect the production environment"
}Supported Events:
Stop,SubagentStopUserPromptSubmitPreToolUsePermissionRequest
Features:
- Context-aware decisions
- Slower than command hooks
- Can handle complex rules
Hooks receive JSON via stdin:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.json",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm run build"
}
}| Variable | Description |
|---|---|
CLAUDE_PROJECT_DIR |
Project root directory |
CLAUDE_FILE_PATH |
Related file path (for Edit/Write) |
CLAUDE_SESSION_ID |
Current session ID |
Exit Codes:
| Code | Meaning |
|---|---|
| 0 | Success, continue |
| 2 | Block (PreToolUse, PermissionRequest) |
| Other | Error (logged, continue) |
JSON Output (optional):
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Dangerous command detected"
}
}// Block
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Reason"
}
}
// Allow (with input modification)
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {
"command": "npm run build -- --safe-mode"
}
}
}
// Tool replacement
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "replace",
"tool": {
"name": "SafeBash",
"input": { "command": "..." }
}
}
}validate_bash.sh:
#!/bin/bash
# Read JSON from stdin
input=$(cat)
# Extract command
command=$(echo "$input" | jq -r '.tool_input.command // ""')
# Check dangerous patterns
dangerous_patterns=(
"rm -rf /"
"rm -rf ~"
"dd if=/dev/zero"
":(){:|:&};:"
"mkfs"
"> /dev/sd"
)
for pattern in "${dangerous_patterns[@]}"; do
if [[ "$command" == *"$pattern"* ]]; then
echo "{
\"hookSpecificOutput\": {
\"hookEventName\": \"PreToolUse\",
\"permissionDecision\": \"deny\",
\"permissionDecisionReason\": \"Dangerous command blocked: $pattern\"
}
}"
exit 2
fi
done
# Allow
exit 0Configuration:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate_bash.sh"
}
]
}
]
}
}{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$CLAUDE_FILE_PATH\" 2>/dev/null || true"
}
]
}
]
}
}audit_logger.py:
#!/usr/bin/env python3
import sys
import json
from datetime import datetime
# Read JSON from stdin
input_data = json.load(sys.stdin)
# Create log entry
log_entry = {
"timestamp": datetime.now().isoformat(),
"session_id": input_data.get("session_id"),
"event": input_data.get("hook_event_name"),
"tool": input_data.get("tool_name"),
"input": input_data.get("tool_input")
}
# Append to file
with open("/var/log/claude-audit.jsonl", "a") as f:
f.write(json.dumps(log_entry) + "\n")
# Continue
sys.exit(0)protect_files.sh:
#!/bin/bash
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path // ""')
# Protected file/directory patterns
protected_patterns=(
".env"
".env.*"
"**/secrets/**"
"**/credentials/**"
"**/*.pem"
"**/*.key"
)
for pattern in "${protected_patterns[@]}"; do
if [[ "$file_path" == $pattern ]]; then
echo "{
\"hookSpecificOutput\": {
\"hookEventName\": \"PreToolUse\",
\"permissionDecision\": \"deny\",
\"permissionDecisionReason\": \"Protected file: $file_path\"
}
}"
exit 2
fi
done
exit 0from claude_agent_sdk import query, ClaudeAgentOptions, HookMatcher, HookContext
from typing import Any
async def validate_bash_command(
input_data: dict[str, Any],
tool_use_id: str | None,
context: HookContext
) -> dict[str, Any]:
"""Bash command validation hook"""
if input_data.get('tool_name') == 'Bash':
command = input_data['tool_input'].get('command', '')
# Check dangerous patterns
dangerous = ['rm -rf /', 'dd if=/dev/zero', ':(){']
for pattern in dangerous:
if pattern in command:
return {
'hookSpecificOutput': {
'hookEventName': 'PreToolUse',
'permissionDecision': 'deny',
'permissionDecisionReason': f'Dangerous command: {pattern}'
}
}
return {}
async def log_tool_usage(
input_data: dict[str, Any],
tool_use_id: str | None,
context: HookContext
) -> dict[str, Any]:
"""Tool usage logging hook"""
import logging
logging.info(f"Tool used: {input_data.get('tool_name')}")
return {}
# Configure hooks in options
options = ClaudeAgentOptions(
allowed_tools=["Read", "Edit", "Bash"],
hooks={
'PreToolUse': [
HookMatcher(matcher='Bash', hooks=[validate_bash_command])
],
'PostToolUse': [
HookMatcher(hooks=[log_tool_usage]) # All tools
]
}
)# Restrict hook script permissions
chmod 700 .claude/hooks/*.sh
# Never log sensitive information
# BAD: echo "$input" >> log.txt (may contain API keys)
# GOOD: Extract only needed fields with jq# Run heavy tasks in background
heavy_task &
# Only synchronous for fast validation
exit 0#!/bin/bash
set -e # Exit immediately on error
# Error handler
trap 'echo "Hook execution error: $?" >&2' ERR
# Logic...# Write for standalone execution
if [[ -z "$1" ]]; then
input=$(cat)
else
input=$(cat "$1") # Input from file
fi#!/bin/bash
# =============================================================================
# validate_bash.sh
#
# Purpose: Check dangerous patterns before Bash command execution
# Event: PreToolUse
# Matcher: Bash
#
# Blocked commands:
# - rm -rf /
# - dd if=/dev/zero
# - fork bomb
# ============================================================================={
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "./1_validate.sh" },
{ "type": "command", "command": "./2_log.sh" },
{ "type": "command", "command": "./3_modify.sh" }
]
}
]
}
}Hooks execute in order; if any returns exit 2, execution stops.
Previous: Tools Reference | Next: MCP Integration