Skip to content

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 Claude

settings.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)

Agentics SOP — AI Developer Workflow & Agentic Coding Reference