Appearance
Hook Examples
Bash Command Validation (PreToolUse)
Block unsafe commands and nudge Claude toward better tools:
python
#!/usr/bin/env python3
import json
import re
import sys
VALIDATION_RULES = [
(
r"\bgrep\b(?!.*\|)",
"Use 'rg' (ripgrep) instead of 'grep' for better performance",
),
(
r"\bfind\s+\S+\s+-name\b",
"Use 'rg --files | rg pattern' instead of 'find -name'",
),
(
r"rm\s+-rf\s+/",
"Dangerous deletion blocked — path starts with /",
),
]
def validate_command(command: str) -> list[str]:
issues = []
for pattern, message in VALIDATION_RULES:
if re.search(pattern, command):
issues.append(message)
return issues
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
command = tool_input.get("command", "")
if tool_name != "Bash" or not command:
sys.exit(0)
issues = validate_command(command)
if issues:
for message in issues:
print(f"- {message}", file=sys.stderr)
sys.exit(2) # Exit code 2: block and show stderr to Claudesettings.json:
json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.py" }]
}
]
}
}Protect Sensitive Files (PreToolUse)
Block any attempt to read or write .env files:
python
#!/usr/bin/env python3
import json
import sys
try:
data = json.load(sys.stdin)
except:
sys.exit(0)
tool_name = data.get("tool_name", "")
tool_input = data.get("tool_input", {})
# Block access to .env files
sensitive_patterns = [".env", ".env.local", ".env.production", "secrets"]
file_path = tool_input.get("file_path", "") or tool_input.get("command", "")
for pattern in sensitive_patterns:
if pattern in file_path:
output = {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": f"Access to '{pattern}' files is blocked. Use environment variables via os.environ."
}
}
print(json.dumps(output))
sys.exit(0)
sys.exit(0)Log All Tool Calls (PostToolUse)
Write a session log file for observability:
bash
#!/bin/bash
# .claude/hooks/log-tool-use.sh
# Appends each tool call to a session log
INPUT=$(cat)
TOOL=$(echo "$INPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('tool_name','unknown'))")
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
SESSION=$(echo "$INPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('session_id','unknown')[:8])")
LOG_DIR="$CLAUDE_PROJECT_DIR/logs"
mkdir -p "$LOG_DIR"
echo "$TIMESTAMP [$SESSION] $TOOL" >> "$LOG_DIR/tool-use.log"Inject Context at Session Start (SessionStart)
Load recent git activity and current branch:
python
#!/usr/bin/env python3
import json
import subprocess
import sys
try:
data = json.load(sys.stdin)
except:
sys.exit(0)
# Only inject on startup (not resume or clear)
if data.get("source") != "startup":
sys.exit(0)
context_parts = []
# Recent commits
try:
git_log = subprocess.run(
["git", "log", "--oneline", "-5"],
capture_output=True, text=True, timeout=5
).stdout.strip()
if git_log:
context_parts.append(f"Recent commits:\n{git_log}")
except:
pass
# Current branch
try:
branch = subprocess.run(
["git", "branch", "--show-current"],
capture_output=True, text=True, timeout=5
).stdout.strip()
if branch:
context_parts.append(f"Current branch: {branch}")
except:
pass
if context_parts:
output = {
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "\n\n".join(context_parts)
}
}
print(json.dumps(output))
sys.exit(0)Auto-Approve Documentation Files (PreToolUse)
Skip permission prompts for safe file types:
python
#!/usr/bin/env python3
import json
import sys
try:
data = json.load(sys.stdin)
except:
sys.exit(0)
tool_name = data.get("tool_name", "")
tool_input = data.get("tool_input", {})
if tool_name == "Read":
file_path = tool_input.get("file_path", "")
safe_extensions = (".md", ".mdx", ".txt", ".json", ".yaml", ".yml")
if file_path.endswith(safe_extensions):
output = {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Documentation file auto-approved"
},
"suppressOutput": True
}
print(json.dumps(output))
sys.exit(0)
sys.exit(0)Validate Prompts for Security (UserPromptSubmit)
Block prompts that contain credentials:
python
#!/usr/bin/env python3
import json
import re
import sys
try:
data = json.load(sys.stdin)
except:
sys.exit(0)
prompt = data.get("prompt", "")
sensitive_patterns = [
(r"(?i)\b(password|secret|api.?key|token)\s*[:=]\s*\S+", "Potential credentials detected"),
(r"sk-ant-[a-zA-Z0-9]+", "Anthropic API key detected"),
(r"ghp_[a-zA-Z0-9]+", "GitHub token detected"),
]
for pattern, message in sensitive_patterns:
if re.search(pattern, prompt):
output = {
"decision": "block",
"reason": f"Security policy: {message}. Please remove credentials from your prompt."
}
print(json.dumps(output))
sys.exit(0)
sys.exit(0)