Hooks
Intercept and control Claude Code's behavior at key lifecycle points with custom scripts or LLM evaluation.
Overview
Hooks allow you to intercept Claude Code's operations at specific points in the workflow. They can approve, deny, or modify tool invocations, add context to responses, and automate permission decisions.
Workflow Control
Approve, deny, or modify tool calls before execution
Context Enhancement
Add additional information to tool outputs
Automation
Auto-approve safe operations to reduce friction
Intelligent Decisions
Use LLM evaluation for complex approval logic
Configuration
Settings File Locations
Hooks are configured in your Claude Code settings files, checked in order of precedence:
| Location | Scope | Priority |
|---|---|---|
.claude/settings.local.json |
Project (gitignored) | Highest |
.claude/settings.json |
Project (version controlled) | Medium |
~/.claude/settings.json |
User-level (all projects) | Lowest |
Basic Structure
{
"hooks": [
{
"matcher": "Read",
"event": "PreToolUse",
"type": "command",
"command": "python ~/.claude/hooks/auto_approve_reads.py"
}
]
}
Hook Configuration Fields
| Field | Required | Description |
|---|---|---|
matcher |
Yes | Tool name or regex pattern to match (e.g., "Read", ".*") |
event |
Yes | When to trigger: PreToolUse, PostToolUse, etc. |
type |
Yes | "command" (bash script) or "prompt" (LLM evaluation) |
command |
If type=command | Shell command to execute |
prompt |
If type=prompt | Template for LLM evaluation |
Hook Types
Command Hooks
Execute custom bash commands or scripts to evaluate tool calls. Best for fast, deterministic validation logic.
{
"matcher": "Bash",
"event": "PreToolUse",
"type": "command",
"command": "python ~/.claude/hooks/validate_bash.py"
}
Use cases: File path validation, simple regex checks, environment variable checks
Prompt-Based Hooks
Use LLM evaluation for complex, context-aware decision making. The prompt has access to full hook context via template variables.
{
"matcher": "Write",
"event": "PreToolUse",
"type": "prompt",
"prompt": "Review this file write operation:\n{{toolCall}}\n\nApprove only if the change seems safe and follows project conventions."
}
Use cases: Code review, semantic validation, complex approval logic
Hook Events
Hooks can be triggered at different points in Claude Code's lifecycle:
PreToolUse
Invoked before a tool is executed. Can approve, deny, or modify the tool call.
Matcher: Tool name (e.g., Read, Write, Bash)
Actions: Approve, deny, modify via updatedInput
PostToolUse
Invoked after a tool completes. Can block with feedback or add context to the result.
Matcher: Tool name
Actions: Block with feedback, add context via additionalContext
UserPromptSubmit
Triggered when user submits a prompt. Can modify the prompt before it's sent to Claude.
Matcher: Not used (always triggers)
Actions: Modify prompt, add context
Stop
Triggered when the main agent conversation stops (completes or is interrupted).
Matcher: Not used
Use case: Cleanup, logging, follow-up actions
SubagentStop
Triggered when a subagent completes or is interrupted.
Matcher: Subagent name
Use case: Extract subagent results, logging
SessionStart
Triggered at the beginning of a Claude Code session.
Matcher: Not used
Use case: Initialization, environment setup
SessionEnd
Triggered when Claude Code session ends.
Matcher: Not used
Use case: Cleanup, save state, generate reports
Notification
Triggered on system notifications (errors, warnings, info).
Matcher: Notification type or pattern
Use case: Custom logging, alerts, error handling
PermissionRequest
Triggered when Claude requests permission for an action.
Matcher: Tool name being requested
Actions: Auto-approve or auto-deny based on rules
PreCompact
Triggered before conversation history is compacted (when context limit is reached).
Matcher: Not used
Use case: Extract information before compaction, save context
Hook Input/Output
Input Format (stdin)
Hooks receive JSON input via stdin with context about the event:
{
"event": "PreToolUse",
"toolName": "Write",
"toolCall": {
"file_path": "/path/to/file.txt",
"content": "Hello, world!"
},
"context": {
"projectDir": "/Users/user/project",
"conversationId": "abc123"
}
}
Exit Codes
| Exit Code | Meaning | Effect |
|---|---|---|
0 |
Success/Approve | Continue with the operation |
2 |
Blocking Error/Deny | Abort the operation with error message |
| Other | Non-blocking error | Log warning but continue operation |
Output Format (stdout)
Hooks return JSON to stdout to communicate decisions:
{
"decision": "approve",
"reason": "File path is safe and within project directory",
"updatedInput": {
"file_path": "/validated/path/file.txt",
"content": "Hello, world!"
},
"additionalContext": "This file was auto-approved by security hook",
"hookSpecificOutput": {
"validationPassed": true,
"checks": ["path", "content", "permissions"]
}
}
Response Fields
| Field | Type | Description |
|---|---|---|
decision |
string | "approve" or "deny" |
reason |
string | Human-readable explanation shown to user |
updatedInput |
object | Modified tool parameters (PreToolUse only) |
additionalContext |
string | Extra context added to Claude's view (PostToolUse) |
hookSpecificOutput |
object | Custom data for logging or chaining hooks |
Practical Examples
Bash Command Validation
Prevent dangerous commands from being executed:
{
"hooks": [
{
"matcher": "Bash",
"event": "PreToolUse",
"type": "command",
"command": "python ~/.claude/hooks/validate_bash.py"
}
]
}
#!/usr/bin/env python3
import sys
import json
# Read hook input
hook_input = json.loads(sys.stdin.read())
command = hook_input.get("toolCall", {}).get("command", "")
# Dangerous patterns
dangerous_patterns = [
"rm -rf /",
":(){ :|:& };:", # Fork bomb
"> /dev/sda",
"mkfs",
"dd if=/dev/random"
]
# Check for dangerous commands
for pattern in dangerous_patterns:
if pattern in command:
print(json.dumps({
"decision": "deny",
"reason": f"Blocked dangerous command pattern: {pattern}"
}))
sys.exit(2)
# Approve safe commands
print(json.dumps({
"decision": "approve",
"reason": "Command passed safety validation"
}))
sys.exit(0)
Auto-Approve Read Operations
Automatically approve safe read operations to reduce friction:
{
"hooks": [
{
"matcher": "Read",
"event": "PreToolUse",
"type": "command",
"command": "python ~/.claude/hooks/auto_approve_reads.py"
}
]
}
#!/usr/bin/env python3
import sys
import json
import os
hook_input = json.loads(sys.stdin.read())
file_path = hook_input.get("toolCall", {}).get("file_path", "")
project_dir = hook_input.get("context", {}).get("projectDir", "")
# Auto-approve if within project directory
if file_path.startswith(project_dir):
# Check file size to avoid reading huge files
if os.path.exists(file_path):
file_size = os.path.getsize(file_path)
if file_size > 10 * 1024 * 1024: # 10MB limit
print(json.dumps({
"decision": "deny",
"reason": f"File too large: {file_size / 1024 / 1024:.2f}MB"
}))
sys.exit(2)
print(json.dumps({
"decision": "approve",
"reason": "Auto-approved: file within project directory"
}))
sys.exit(0)
# Deny reads outside project
print(json.dumps({
"decision": "deny",
"reason": "File is outside project directory"
}))
sys.exit(2)
Intelligent Stop Hook
Extract action items when conversation ends:
{
"hooks": [
{
"event": "Stop",
"type": "prompt",
"prompt": "Review this conversation and extract any action items, TODOs, or follow-up tasks:\n\n{{conversation}}\n\nFormat as a markdown checklist."
}
]
}
Context Enhancement
Add architectural context after reading files:
{
"hooks": [
{
"matcher": "Read",
"event": "PostToolUse",
"type": "command",
"command": "python ~/.claude/hooks/add_architecture_context.py"
}
]
}
#!/usr/bin/env python3
import sys
import json
import os
hook_input = json.loads(sys.stdin.read())
file_path = hook_input.get("toolCall", {}).get("file_path", "")
# Add context based on file type
context = ""
if file_path.endswith(".py"):
context = "Note: This project uses FastAPI framework with async/await patterns."
elif file_path.endswith((".tsx", ".jsx")):
context = "Note: This is a React component using TypeScript and hooks."
elif "database" in file_path.lower():
context = "Note: This project uses PostgreSQL with SQLAlchemy ORM."
print(json.dumps({
"decision": "approve",
"additionalContext": context
}))
sys.exit(0)
Environment Variables
Hooks have access to special environment variables:
CLAUDE_PROJECT_DIR
Absolute path to the current project directory
#!/bin/bash
echo "Project: $CLAUDE_PROJECT_DIR"
CLAUDE_CODE_REMOTE
Set to "true" if running in remote mode (SSH, containers)
if [ "$CLAUDE_CODE_REMOTE" = "true" ]; then
echo "Running in remote environment"
fi
CLAUDE_ENV_FILE
Path to the project's .env file (if configured)
import os
env_file = os.environ.get("CLAUDE_ENV_FILE")
if env_file:
# Load environment variables from file
pass
MCP Tools Support
Hooks can intercept MCP (Model Context Protocol) tools using the naming pattern:
mcp__<server-name>__<tool-name>
Example: MCP Tool Hook
{
"hooks": [
{
"matcher": "mcp__filesystem__read_file",
"event": "PreToolUse",
"type": "command",
"command": "python ~/.claude/hooks/validate_mcp_read.py"
}
]
}
Match All MCP Tools
Use regex to match all tools from a specific MCP server:
{
"hooks": [
{
"matcher": "mcp__github__.*",
"event": "PreToolUse",
"type": "command",
"command": "python ~/.claude/hooks/audit_github_access.py"
}
]
}
Best Practices
Keep hooks fast
Hooks block Claude's execution. Optimize for speed and use caching when possible.
Prefer command hooks for simple logic
Use bash/python scripts for deterministic checks. Reserve prompt hooks for complex decisions.
Provide clear error messages
When denying operations, explain why in the "reason" field so users understand.
Use specific matchers
Target specific tools instead of matching everything to avoid unnecessary overhead.
Test hooks thoroughly
Broken hooks can block Claude entirely. Test edge cases and error handling.
Version control project hooks
Check hooks into .claude/settings.json to share with team members.
Use local settings for personal preferences
Put personal hooks in .claude/settings.local.json (gitignored).
Security Considerations
Important Security Notes
- Hooks execute with your user's permissions - validate all inputs carefully
- Never trust tool parameters without validation (path traversal, injection attacks)
- Be cautious with hooks from untrusted sources
- Avoid exposing sensitive data in hook output (it's visible to Claude)
Security Checklist
Validate file paths to prevent directory traversal attacks
Sanitize bash commands to prevent command injection
Use allowlists instead of denylists for validation
Set appropriate file permissions on hook scripts (chmod 700)
Review hooks before running in production environments
Avoid storing secrets in hook scripts - use environment variables
Example: Path Validation
#!/usr/bin/env python3
import sys
import json
import os
from pathlib import Path
hook_input = json.loads(sys.stdin.read())
file_path = hook_input.get("toolCall", {}).get("file_path", "")
project_dir = hook_input.get("context", {}).get("projectDir", "")
# Resolve to absolute path and check if within project
try:
abs_path = Path(file_path).resolve()
abs_project = Path(project_dir).resolve()
# Check if path is within project (prevents directory traversal)
if not str(abs_path).startswith(str(abs_project)):
print(json.dumps({
"decision": "deny",
"reason": "Path is outside project directory"
}))
sys.exit(2)
# Additional checks for sensitive files
sensitive_patterns = [".env", "secrets", "credentials", ".ssh", ".aws"]
if any(pattern in str(abs_path).lower() for pattern in sensitive_patterns):
print(json.dumps({
"decision": "deny",
"reason": "Access to sensitive files requires manual approval"
}))
sys.exit(2)
print(json.dumps({"decision": "approve"}))
sys.exit(0)
except Exception as e:
print(json.dumps({
"decision": "deny",
"reason": f"Path validation error: {str(e)}"
}))
sys.exit(2)