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.comSecure 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 here3. 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
- Principle of Least Privilege: Only grant minimum required permissions
- Defense in Depth: Layer multiple security controls
- Fail Secure: Default to denying operations
- Input Validation: Never trust user input
- Output Encoding: Sanitize output to prevent injection
- Error Handling: Don’t leak sensitive information in errors
- Logging: Maintain audit trails for security events
- Regular Updates: Keep hooks and dependencies updated
Emergency Response
If you suspect a security breach:
-
Immediately disable affected hooks:
mv .claude/settings.json .claude/settings.json.disabled -
Review recent hook activity:
grep -r "ERROR\|WARN\|FAIL" ~/.claude/logs/ -
Check for unauthorized changes:
git status git diff -
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:
- Apply Your Knowledge: Secure Hooks Workshop Solutions
- Review the Core Hooks Architecture
- Context: See How Secure Hooks Fit into Common Development Workflows
Related Resources
- Debugging Guide - Secure debugging
- Advanced Examples - Security patterns
- Workshop Overview - Start here
Remember: Security is not optional. Always prioritize safety over convenience.