The Claude Code hook system provides an extensible event-driven architecture that allows custom validation, transformation, and control of tool executions. Hooks can intercept tool calls before execution to validate commands, modify inputs, or block dangerous operations.
Configure hooks through JSON configuration that defines when and how hooks should execute.
{
"hooks": {
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo 'Session started in $CLAUDE_PROJECT_DIR'",
"timeout": 5000
}
]
}
],
"SessionEnd": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo 'Session ended at $(date)' >> ~/.claude/session.log"
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 /path/to/hook-script.py",
"timeout": 3000
}
]
}
],
"UserPromptSubmit": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo 'User prompt logged' >> ~/.claude/prompts.log"
}
]
}
],
"PreCompact": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/backup-conversation.sh"
}
]
}
],
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo 'Task completed successfully'"
}
]
}
],
"SubagentStop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo 'Subagent task completed'"
}
]
}
]
}
}Configuration Structure:
hooks: Root configuration objectSessionStart, SessionEnd, PreToolUse, UserPromptSubmit, PreCompact, Stop, SubagentStopmatcher: Tool name to match (e.g., "Bash", "Read", "Write", "*" for all)hooks: Array of hook actions to execute when matcher triggerstype: Hook action type (currently supports "command")command: Shell command to execute as the hooktimeout: Optional timeout in milliseconds for hook executionClaude Code supports multiple hook types for different lifecycle events.
Executed when a new Claude Code session begins.
{
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "git status",
"timeout": 5000
}
]
}
]
}Environment Variables Available:
CLAUDE_PROJECT_DIR: Current project directoryCLAUDE_SESSION_ID: Unique session identifierCLAUDE_USER: Current user (if available)Executed when a Claude Code session terminates.
{
"SessionEnd": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo 'Session ended at $(date)' >> ~/.claude/session.log"
}
]
}
]
}Executed before any tool is used (most common hook type).
{
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/bash-validator.py",
"timeout": 3000
}
]
}
]
}Executed when a user submits input to Claude.
{
"UserPromptSubmit": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo 'User prompt: $CLAUDE_USER_PROMPT' >> ~/.claude/prompts.log"
}
]
}
]
}Environment Variables Available:
CLAUDE_USER_PROMPT: The user's input textCLAUDE_SESSION_ID: Current session identifierExecuted before conversation compaction occurs.
{
"PreCompact": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/backup-conversation.sh"
}
]
}
]
}Executed when a main task completes successfully.
{
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo 'Task completed successfully' | notify-send 'Claude Code'"
}
]
}
]
}Executed when a subagent task completes.
{
"SubagentStop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo 'Subagent task completed'"
}
]
}
]
}Hook scripts receive JSON input via stdin containing information about the tool being executed.
{
"tool_name": "Bash",
"tool_input": {
"command": "git status",
"description": "Show working tree status"
},
"environment": {
"CLAUDE_PROJECT_DIR": "/path/to/project",
"CLAUDE_SESSION_ID": "session-123"
}
}Input Structure:
tool_name: Name of the tool being executedtool_input: Object containing the tool's input parameters
command (string), description (string), optional parametersfile_path (string), optional limit and offsetfile_path (string), content (string)Hook scripts communicate results through exit codes and stderr output.
# Exit code meanings:
# 0: Success - Allow tool execution to proceed
# 1: Error - Show stderr to user but not to Claude, allow execution
# 2: Block - Show stderr to Claude and block tool executionExit Code Behaviors:
Example hook script structure for bash command validation:
#!/usr/bin/env python3
import json
import sys
import re
def validate_command(command):
"""
Validate bash command against security rules
Returns list of validation issues
"""
issues = []
# Example validation rules
if re.search(r"^rm\s+-rf\s+/", command):
issues.append("Dangerous recursive delete of root paths blocked")
if re.search(r"^grep\b(?!.*\|)", command):
issues.append("Use 'rg' (ripgrep) instead of 'grep' for better performance")
return issues
def main():
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
sys.exit(1)
if input_data.get("tool_name") != "Bash":
sys.exit(0)
command = input_data.get("tool_input", {}).get("command", "")
if not command:
sys.exit(0)
issues = validate_command(command)
if issues:
for message in issues:
print(f"• {message}", file=sys.stderr)
sys.exit(2) # Block execution
if __name__ == "__main__":
main()The most common hook type that executes before any tool is used.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 /path/to/bash-validator.py"
}
]
},
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "/usr/local/bin/file-security-check"
}
]
}
]
}
}Common Use Cases:
Command Validation Hook:
# Pattern for validating bash commands
def validate_bash_command(command):
validation_rules = [
(r"^dangerous_pattern", "Reason why this is dangerous"),
(r"^deprecated_command", "Use modern_alternative instead")
]
issues = []
for pattern, message in validation_rules:
if re.search(pattern, command):
issues.append(message)
return issuesFile Operation Hook:
# Pattern for validating file operations
def validate_file_operation(tool_input):
file_path = tool_input.get("file_path", "")
# Check for sensitive file patterns
sensitive_patterns = [
r"\.env$",
r"\.key$",
r"\.pem$",
r"password",
r"secret"
]
for pattern in sensitive_patterns:
if re.search(pattern, file_path, re.IGNORECASE):
return ["Sensitive file operation detected"]
return []Hooks are typically configured in user or project-specific configuration files:
.claude/ directoryConfiguration Loading Order:
# Robust error handling pattern for hooks
def main():
try:
input_data = json.load(sys.stdin)
result = process_hook(input_data)
if result.should_block:
for error in result.errors:
print(f"• {error}", file=sys.stderr)
sys.exit(2)
elif result.warnings:
for warning in result.warnings:
print(f"Warning: {warning}", file=sys.stderr)
sys.exit(1)
else:
sys.exit(0)
except Exception as e:
print(f"Hook execution error: {e}", file=sys.stderr)
sys.exit(1) # Allow execution on hook failure# Test hook script directly
echo '{"tool_name": "Bash", "tool_input": {"command": "rm -rf /"}}' | python3 hook-script.py
# Check exit code
echo $?
# Test with various inputs
echo '{"tool_name": "Read", "tool_input": {"file_path": ".env"}}' | python3 hook-script.py