claude-code hooks security best-practices validation safety

Security Considerations - Claude Code Hooks

Learn essential security practices to keep your hooks and system safe.

Hook Security Checklist

Use this checklist every time you write or review a custom hook to ensure it is secure:

  • Input Validation: Is all incoming data validated for type, length, and format?
  • Output Sanitization: Is any data that will be rendered in the UI sanitized to prevent XSS attacks?
  • Least Privilege: Does the hook only have access to the permissions and data it absolutely needs?
  • Graceful Error Handling: Are errors caught properly without leaking sensitive information like stack traces?
  • External Call Security: Are all external API calls properly authenticated and protected against request forgery (CSRF)?

Module Overview

  • Duration: 30 minutes
  • Prerequisites: Understanding of hooks
  • Goal: Implement secure hook patterns

⚠️ Security Warning

Claude Code hooks execute arbitrary shell commands with your user privileges. This means:

  • They can read, modify, or delete ANY file you have access to
  • They can execute system commands
  • They can access network resources
  • They can interact with other processes

Always review and understand hooks before enabling them!

Core Security Principles

1. Input Validation

Never trust external input. Always validate and sanitize.

Unsafe Example ❌

#!/bin/bash
# DANGEROUS - Command injection vulnerability
FILE_PATH="$CLAUDE_FILE_PATHS"
eval "cat $FILE_PATH"  # Never use eval with user input!

Safe Example ✅

#!/bin/bash
# Safe - Properly quoted and validated
FILE_PATH="$CLAUDE_FILE_PATHS"
 
# Validate path is within project
if [[ ! "$FILE_PATH" =~ ^/home/user/project/ ]]; then
    echo "Access denied: Path outside project" >&2
    exit 2
fi
 
# Use quotes to prevent injection
cat "$FILE_PATH"

2. Path Traversal Prevention

Prevent access to files outside intended directories:

#!/usr/bin/env python3
import json
import sys
import os
from pathlib import Path
 
def validate_path(file_path):
    """Ensure path is within allowed directory"""
    # Resolve to absolute path
    abs_path = Path(file_path).resolve()
    
    # Define allowed base directory
    allowed_base = Path.cwd()
    
    # Check if path is within allowed directory
    try:
        abs_path.relative_to(allowed_base)
        return True
    except ValueError:
        return False
 
def main():
    data = json.load(sys.stdin)
    file_path = data.get('tool_input', {}).get('file_path', '')
    
    if not validate_path(file_path):
        print(json.dumps({
            'continue': False,
            'stopReason': 'Path traversal attempt detected'
        }))
        return 2
    
    return 0
 
if __name__ == '__main__':
    sys.exit(main())

3. Command Injection Prevention

Using Shell Safely

#!/usr/bin/env python3
import subprocess
import shlex
 
# UNSAFE - Command injection possible
def unsafe_command(user_input):
    subprocess.run(f"echo {user_input}", shell=True)  # ❌
 
# SAFE - Properly escaped
def safe_command(user_input):
    # Option 1: Use shlex for escaping
    cmd = f"echo {shlex.quote(user_input)}"
    subprocess.run(cmd, shell=True)  # ✅
    
    # Option 2: Avoid shell entirely (preferred)
    subprocess.run(['echo', user_input])  # ✅✅

4. Secrets Management

Never hardcode secrets in hooks!

Unsafe Example ❌

{
  "hooks": {
    "PostToolUse": [{
      "matcher": ".*",
      "hooks": [{
        "type": "command",
        "command": "curl -H 'Authorization: Bearer sk-abc123...' https://api.example.com"
      }]
    }]
  }
}

Safe Example ✅

#!/bin/bash
# Read secrets from environment or secure storage
API_KEY="${CLAUDE_API_KEY:-$(cat ~/.secrets/api_key 2>/dev/null)}"
 
if [ -z "$API_KEY" ]; then
    echo "Error: API key not found" >&2
    exit 1
fi
 
curl -H "Authorization: Bearer $API_KEY" https://api.example.com

Secure Hook Patterns

1. Whitelist-Based Validation

Always use whitelists, not blacklists. The following script is a powerful example of a PreToolUse hook that acts as a security gateway, validating every tool call against a predefined set of rules before it executes.

#!/usr/bin/env python3
import json
import sys
import re
 
# Define allowed patterns
ALLOWED_TOOLS = ['Edit', 'Write', 'Read']
ALLOWED_FILE_EXTENSIONS = ['.py', '.js', '.ts', '.json', '.md']
ALLOWED_PATHS = [
    r'^/home/user/project/src/.*',
    r'^/home/user/project/tests/.*'
]
 
def validate_operation(data):
    tool_name = data.get('tool_name', '')
    file_path = data.get('tool_input', {}).get('file_path', '')
    
    # Check tool whitelist
    if tool_name not in ALLOWED_TOOLS:
        return False, f"Tool '{tool_name}' not allowed"
    
    # Check file extension
    if not any(file_path.endswith(ext) for ext in ALLOWED_FILE_EXTENSIONS):
        return False, f"File type not allowed: {file_path}"
    
    # Check path patterns
    if not any(re.match(pattern, file_path) for pattern in ALLOWED_PATHS):
        return False, f"Path not allowed: {file_path}"
    
    return True, "Validation passed"
 
def main():
    data = json.load(sys.stdin)
    valid, message = validate_operation(data)
    
    if not valid:
        print(json.dumps({
            'continue': False,
            'stopReason': message
        }))
        return 2
    
    return 0
 
if __name__ == '__main__':
    sys.exit(main())

This script provides a robust foundation for a centralized security validation hook. Learn more about implementing PreToolUse hooks in the Hooks Architecture guide.

2. Privilege Limitation

Run hooks with minimal required privileges:

#!/bin/bash
# Drop privileges if running as root (shouldn't happen, but safety first)
if [ "$EUID" -eq 0 ]; then
    echo "Error: Hooks should not run as root" >&2
    exit 2
fi
 
# Create restricted environment
export PATH="/usr/local/bin:/usr/bin:/bin"
unset LD_PRELOAD
unset LD_LIBRARY_PATH
 
# Your hook logic here

3. Audit Logging

Log all security-relevant events:

#!/usr/bin/env python3
import json
import sys
import logging
import hashlib
from datetime import datetime
 
# Setup secure logging
logging.basicConfig(
    filename='/var/log/claude-hooks-audit.log',
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)
 
def audit_log(event_type, data, result):
    """Create audit log entry"""
    entry = {
        'timestamp': datetime.utcnow().isoformat(),
        'event_type': event_type,
        'tool_name': data.get('tool_name'),
        'file_path': data.get('tool_input', {}).get('file_path'),
        'session_id': data.get('session_id'),
        'result': result,
        'checksum': hashlib.sha256(
            json.dumps(data, sort_keys=True).encode()
        ).hexdigest()
    }
    
    logging.info(json.dumps(entry))
 
def main():
    data = json.load(sys.stdin)
    
    # Log attempt
    audit_log('hook_execution', data, 'started')
    
    try:
        # Your hook logic here
        result = process_hook(data)
        audit_log('hook_execution', data, 'success')
        return result
    except Exception as e:
        audit_log('hook_execution', data, f'failed: {str(e)}')
        raise
 
if __name__ == '__main__':
    sys.exit(main())

4. Sandboxing

Use containers or restricted environments:

#!/bin/bash
# Run hook in Docker container with limited permissions
docker run --rm \
    --read-only \
    --user 1000:1000 \
    --network none \
    --memory 100m \
    --cpus 0.5 \
    -v "$PWD:/workspace:ro" \
    -w /workspace \
    alpine:latest \
    /bin/sh -c "your-hook-command"

Common Security Vulnerabilities

1. Shell Injection

# VULNERABLE
echo "Processing file: $CLAUDE_FILE_PATHS" | sh
 
# SECURE
echo "Processing file: $CLAUDE_FILE_PATHS"

2. Path Traversal

# VULNERABLE
cat "/tmp/uploads/$USER_INPUT"
 
# SECURE
SAFE_INPUT=$(basename "$USER_INPUT")
cat "/tmp/uploads/$SAFE_INPUT"

3. Regex DoS

# VULNERABLE - Catastrophic backtracking
pattern = r'(a+)+$'
 
# SECURE - Limit complexity
pattern = r'a{1,100}$'

4. Resource Exhaustion

# VULNERABLE - No limits
find / -name "*.log"
 
# SECURE - Limited scope and timeout
timeout 5s find ./logs -maxdepth 3 -name "*.log"

Security Checklist

Before Writing Hooks

  • Understand the security implications
  • Define clear security boundaries
  • Plan input validation strategy
  • Identify sensitive resources

While Writing Hooks

  • Validate all inputs
  • Use absolute paths
  • Avoid shell=True when possible
  • Quote all variables
  • Limit resource usage
  • Log security events

Before Deploying Hooks

  • Review code for vulnerabilities
  • Test with malicious inputs
  • Check file permissions
  • Verify no hardcoded secrets
  • Test in isolated environment

After Deployment

  • Monitor logs regularly
  • Review audit trails
  • Update hooks promptly
  • Respond to security alerts

Best Practices Summary

  1. Principle of Least Privilege: Only grant minimum required permissions
  2. Defense in Depth: Layer multiple security controls
  3. Fail Secure: Default to denying operations
  4. Input Validation: Never trust user input
  5. Output Encoding: Sanitize output to prevent injection
  6. Error Handling: Don’t leak sensitive information in errors
  7. Logging: Maintain audit trails for security events
  8. Regular Updates: Keep hooks and dependencies updated

Emergency Response

If you suspect a security breach:

  1. Immediately disable affected hooks:

    mv .claude/settings.json .claude/settings.json.disabled
  2. Review recent hook activity:

    grep -r "ERROR\|WARN\|FAIL" ~/.claude/logs/
  3. Check for unauthorized changes:

    git status
    git diff
  4. Audit system logs:

    sudo journalctl -u claude-code --since "1 hour ago"

Additional Resources

Learning Path: Secure Hook Development

You’ve completed the security module! Review:

Remember: Security is not optional. Always prioritize safety over convenience.