534 lines
11 KiB
Markdown
534 lines
11 KiB
Markdown
# Security Best Practices
|
|
|
|
Comprehensive security guidance for Claude Code hooks.
|
|
|
|
## Input Validation
|
|
|
|
### Validate All Input
|
|
|
|
Always validate and sanitize hook input before use:
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
INPUT=$(cat)
|
|
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
|
|
|
# Validate input exists
|
|
if [[ -z "$FILE_PATH" ]]; then
|
|
echo "Error: file_path missing" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Validate format
|
|
if [[ ! "$FILE_PATH" =~ ^[a-zA-Z0-9_./-]+$ ]]; then
|
|
echo "Error: invalid characters in file path" >&2
|
|
exit 2
|
|
fi
|
|
```
|
|
|
|
### Check for Path Traversal
|
|
|
|
Block directory traversal attacks:
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
INPUT=$(cat)
|
|
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
|
|
|
# Block path traversal
|
|
if echo "$FILE_PATH" | grep -qE '\.\./'; then
|
|
cat << EOF >&2
|
|
Path traversal detected: $FILE_PATH
|
|
Paths containing '..' are not allowed.
|
|
EOF
|
|
exit 2
|
|
fi
|
|
|
|
# Block absolute paths outside project
|
|
if [[ "$FILE_PATH" == /* ]] && [[ ! "$FILE_PATH" == "$CLAUDE_PROJECT_DIR"* ]]; then
|
|
echo "Access outside project directory blocked: $FILE_PATH" >&2
|
|
exit 2
|
|
fi
|
|
```
|
|
|
|
### Block Sensitive System Paths
|
|
|
|
Prevent access to system files:
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
INPUT=$(cat)
|
|
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
|
|
|
# Blocked system paths
|
|
BLOCKED_PATHS=(
|
|
'^/etc/'
|
|
'^/root/'
|
|
'^/home/[^/]+/\.ssh/'
|
|
'^/var/log/'
|
|
'^/sys/'
|
|
'^/proc/'
|
|
'^/boot/'
|
|
'^/usr/bin/'
|
|
'^/usr/sbin/'
|
|
)
|
|
|
|
for pattern in "${BLOCKED_PATHS[@]}"; do
|
|
if echo "$FILE_PATH" | grep -qE "$pattern"; then
|
|
cat << EOF >&2
|
|
Access to sensitive system path blocked: $FILE_PATH
|
|
This path is restricted for security reasons.
|
|
EOF
|
|
exit 2
|
|
fi
|
|
done
|
|
```
|
|
|
|
### Detect Sensitive Files
|
|
|
|
Warn or block access to sensitive project files:
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
INPUT=$(cat)
|
|
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
|
|
|
# Sensitive file patterns
|
|
SENSITIVE_PATTERNS=(
|
|
'\.env$'
|
|
'\.env\.'
|
|
'id_rsa'
|
|
'id_ed25519'
|
|
'\.pem$'
|
|
'\.key$'
|
|
'\.p12$'
|
|
'credentials'
|
|
'password'
|
|
'token'
|
|
'secret'
|
|
'\.git/config$'
|
|
'\.npmrc$'
|
|
'\.pypirc$'
|
|
)
|
|
|
|
for pattern in "${SENSITIVE_PATTERNS[@]}"; do
|
|
if echo "$FILE_PATH" | grep -qiE "$pattern"; then
|
|
cat << EOF >&2
|
|
Warning: Accessing sensitive file: $FILE_PATH
|
|
This file may contain sensitive information.
|
|
EOF
|
|
# Could exit 2 to block, or continue with warning
|
|
fi
|
|
done
|
|
```
|
|
|
|
## Command Injection Prevention
|
|
|
|
### Always Quote Variables
|
|
|
|
```bash
|
|
# WRONG - vulnerable to injection
|
|
rm $FILE_PATH
|
|
cd $DIRECTORY
|
|
echo $CONTENT
|
|
|
|
# CORRECT - properly quoted
|
|
rm "$FILE_PATH"
|
|
cd "$DIRECTORY"
|
|
echo "$CONTENT"
|
|
```
|
|
|
|
### Avoid eval
|
|
|
|
```bash
|
|
# WRONG - dangerous
|
|
eval "$USER_COMMAND"
|
|
|
|
# CORRECT - use specific commands
|
|
if [[ "$USER_COMMAND" == "format" ]]; then
|
|
black "$FILE_PATH"
|
|
fi
|
|
```
|
|
|
|
### Validate Command Patterns
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
INPUT=$(cat)
|
|
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
|
|
|
|
# Block dangerous command patterns
|
|
DANGEROUS_PATTERNS=(
|
|
'\brm\s+-rf\s+/' # rm -rf /
|
|
'\brm\s+--no-preserve-root' # rm --no-preserve-root
|
|
'\bmkfs\b' # filesystem format
|
|
'\bdd\s+if=' # disk destruction
|
|
'\bformat\s+[cC]:' # Windows format
|
|
'>\s*/dev/sd[a-z]' # overwrite disk
|
|
':()\{\s*:\|\:&\s*\};:' # Fork bomb
|
|
'\bchmod\s+777\s+/' # Dangerous permissions
|
|
'\bchown\s+.*\s+/' # System ownership change
|
|
'\bcurl\s+.*\|\s*bash' # Pipe to bash
|
|
'\bwget\s+.*\|\s*bash' # Pipe to bash
|
|
'\bsudo\s+rm' # Sudo rm
|
|
'\bgit\s+push\s+--force\s+origin\s+main' # Force push main
|
|
)
|
|
|
|
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
|
|
if echo "$COMMAND" | grep -qE "$pattern"; then
|
|
cat << EOF >&2
|
|
Dangerous command blocked: $COMMAND
|
|
Pattern matched: $pattern
|
|
EOF
|
|
exit 2
|
|
fi
|
|
done
|
|
```
|
|
|
|
## Path Security
|
|
|
|
### Use Absolute Paths
|
|
|
|
Always construct paths from known roots:
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
# Use CLAUDE_PROJECT_DIR for project paths
|
|
SCRIPT_PATH="$CLAUDE_PROJECT_DIR/.claude/hooks/helper.sh"
|
|
|
|
# Use CLAUDE_PLUGIN_ROOT for plugin paths
|
|
PLUGIN_SCRIPT="${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh"
|
|
|
|
# Never rely on relative paths
|
|
# BAD: ./scripts/validate.sh
|
|
# GOOD: "$CLAUDE_PROJECT_DIR/.claude/scripts/validate.sh"
|
|
```
|
|
|
|
### Validate Script Existence
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
SCRIPT_PATH="$CLAUDE_PROJECT_DIR/.claude/hooks/helper.sh"
|
|
|
|
# Check exists
|
|
if [[ ! -f "$SCRIPT_PATH" ]]; then
|
|
echo "Error: script not found: $SCRIPT_PATH" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Check executable
|
|
if [[ ! -x "$SCRIPT_PATH" ]]; then
|
|
echo "Error: script not executable: $SCRIPT_PATH" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Execute safely
|
|
"$SCRIPT_PATH" "$@"
|
|
```
|
|
|
|
### Resolve Symlinks
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')
|
|
|
|
# Resolve symlinks to check actual destination
|
|
REAL_PATH=$(realpath "$FILE_PATH" 2>/dev/null || echo "$FILE_PATH")
|
|
|
|
# Check the real path is within project
|
|
if [[ ! "$REAL_PATH" == "$CLAUDE_PROJECT_DIR"* ]]; then
|
|
echo "Symlink points outside project: $FILE_PATH -> $REAL_PATH" >&2
|
|
exit 2
|
|
fi
|
|
```
|
|
|
|
## Sensitive Data Protection
|
|
|
|
### Never Log Sensitive Data
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
INPUT=$(cat)
|
|
|
|
# WRONG - logs everything including secrets
|
|
echo "Input: $INPUT" >> /tmp/debug.log
|
|
|
|
# CORRECT - log only safe fields
|
|
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
|
|
echo "Tool: $TOOL_NAME" >> /tmp/debug.log
|
|
|
|
# CORRECT - filter sensitive fields before logging
|
|
echo "$INPUT" | jq 'del(.tool_input.password, .tool_input.api_key, .tool_input.token)' >> /tmp/debug.log
|
|
```
|
|
|
|
### Sanitize Output
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
INPUT=$(cat)
|
|
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty')
|
|
|
|
# Check for secrets in content
|
|
if echo "$CONTENT" | grep -qiE '(password|api_key|secret|token)\s*[=:]\s*\S+'; then
|
|
echo "Warning: Potential secret detected in content" >&2
|
|
# Could block or just warn
|
|
fi
|
|
```
|
|
|
|
### Protect Environment Variables
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
# Don't expose sensitive env vars
|
|
|
|
# WRONG
|
|
echo "API_KEY=$API_KEY"
|
|
env | grep -i secret
|
|
|
|
# CORRECT - never print secrets
|
|
echo "API key configured: $([ -n "$API_KEY" ] && echo "yes" || echo "no")"
|
|
```
|
|
|
|
## Timeout Protection
|
|
|
|
### Set Appropriate Timeouts
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"PreToolUse": [{
|
|
"matcher": "Bash",
|
|
"hooks": [{
|
|
"type": "command",
|
|
"command": "./.claude/hooks/validate.sh",
|
|
"timeout": 5
|
|
}]
|
|
}],
|
|
"PostToolUse": [{
|
|
"matcher": "Write|Edit",
|
|
"hooks": [{
|
|
"type": "command",
|
|
"command": "./.claude/hooks/format.sh",
|
|
"timeout": 30
|
|
}]
|
|
}]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Guidelines**:
|
|
- Quick validation: 3-5 seconds
|
|
- Formatting: 10-30 seconds
|
|
- Network operations: 30-60 seconds
|
|
- Default: 60 seconds for command, 30 seconds for prompt
|
|
|
|
### Handle Timeouts Gracefully
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Set internal timeout for network operations
|
|
timeout 10 curl -s https://api.example.com/validate || {
|
|
echo "API validation skipped (timeout)" >&2
|
|
exit 0 # Don't block on timeout
|
|
}
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Use Strict Mode
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail # Exit on error, undefined vars, pipe failures
|
|
|
|
# Also consider:
|
|
set -E # Inherit ERR trap in functions
|
|
trap 'echo "Error on line $LINENO" >&2' ERR
|
|
```
|
|
|
|
### Validate Dependencies
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Check required tools exist
|
|
for cmd in jq git curl; do
|
|
if ! command -v "$cmd" &>/dev/null; then
|
|
echo "Error: $cmd not installed" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
```
|
|
|
|
### Handle JSON Parsing Errors
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Read input with error handling
|
|
INPUT=$(cat) || {
|
|
echo "Error: failed to read stdin" >&2
|
|
exit 1
|
|
}
|
|
|
|
# Parse with validation
|
|
if ! echo "$INPUT" | jq empty 2>/dev/null; then
|
|
echo "Error: invalid JSON input" >&2
|
|
exit 1
|
|
fi
|
|
|
|
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
|
|
if [[ -z "$TOOL_NAME" ]]; then
|
|
echo "Error: tool_name missing" >&2
|
|
exit 1
|
|
fi
|
|
```
|
|
|
|
## Permission Control
|
|
|
|
### PreToolUse Permission Decisions
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
INPUT=$(cat)
|
|
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
|
|
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
|
|
|
# Auto-approve reads of non-sensitive files
|
|
if [[ "$TOOL_NAME" == "Read" ]] && [[ ! "$FILE_PATH" =~ \.(env|key|pem)$ ]]; then
|
|
cat << EOF
|
|
{
|
|
"continue": true,
|
|
"hookSpecificOutput": {
|
|
"hookEventName": "PreToolUse",
|
|
"permissionDecision": "allow"
|
|
}
|
|
}
|
|
EOF
|
|
exit 0
|
|
fi
|
|
|
|
# Ask for writes to core files
|
|
if [[ "$FILE_PATH" =~ src/core/ ]]; then
|
|
cat << EOF
|
|
{
|
|
"continue": true,
|
|
"hookSpecificOutput": {
|
|
"hookEventName": "PreToolUse",
|
|
"permissionDecision": "ask",
|
|
"permissionDecisionReason": "Write to core module requires confirmation"
|
|
}
|
|
}
|
|
EOF
|
|
exit 0
|
|
fi
|
|
|
|
# Default: allow
|
|
exit 0
|
|
```
|
|
|
|
### PermissionRequest Hook
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
INPUT=$(cat)
|
|
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
|
|
|
|
# Auto-deny certain operations
|
|
if [[ "$TOOL_NAME" =~ (delete|destroy|remove) ]]; then
|
|
cat << EOF
|
|
{
|
|
"hookSpecificOutput": {
|
|
"permissionDecision": "deny",
|
|
"permissionDecisionReason": "Destructive operations require manual approval"
|
|
}
|
|
}
|
|
EOF
|
|
exit 0
|
|
fi
|
|
```
|
|
|
|
## Audit Trail
|
|
|
|
### Log All Operations
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
AUDIT_FILE="$CLAUDE_PROJECT_DIR/.claude/audit.log"
|
|
|
|
INPUT=$(cat)
|
|
TIMESTAMP=$(date -Iseconds)
|
|
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name')
|
|
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "N/A"')
|
|
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // "N/A"')
|
|
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
|
|
|
|
# Create audit entry (filter sensitive data)
|
|
AUDIT_ENTRY=$(jq -n \
|
|
--arg ts "$TIMESTAMP" \
|
|
--arg event "$HOOK_EVENT" \
|
|
--arg tool "$TOOL_NAME" \
|
|
--arg file "$FILE_PATH" \
|
|
--arg session "$SESSION_ID" \
|
|
--arg user "$USER" \
|
|
'{
|
|
timestamp: $ts,
|
|
event: $event,
|
|
tool: $tool,
|
|
file: $file,
|
|
session: $session,
|
|
user: $user
|
|
}')
|
|
|
|
echo "$AUDIT_ENTRY" >> "$AUDIT_FILE"
|
|
|
|
# Rotate: keep only last 10000 entries
|
|
tail -n 10000 "$AUDIT_FILE" > "$AUDIT_FILE.tmp" && mv "$AUDIT_FILE.tmp" "$AUDIT_FILE"
|
|
|
|
exit 0
|
|
```
|
|
|
|
## Security Checklist
|
|
|
|
### Before Deploying Hooks
|
|
|
|
- [ ] All input validated and sanitized
|
|
- [ ] Path traversal attacks blocked
|
|
- [ ] Sensitive system paths protected
|
|
- [ ] All shell variables quoted
|
|
- [ ] No eval or command injection vectors
|
|
- [ ] Sensitive data not logged
|
|
- [ ] Appropriate timeouts set
|
|
- [ ] Dependencies validated
|
|
- [ ] Error handling robust
|
|
- [ ] Audit trail enabled
|
|
|
|
### Regular Security Review
|
|
|
|
- [ ] Review hook scripts for vulnerabilities
|
|
- [ ] Check for hardcoded secrets
|
|
- [ ] Verify timeout values are appropriate
|
|
- [ ] Audit logged data for sensitive info leaks
|
|
- [ ] Update blocked patterns for new threats
|
|
- [ ] Test hooks with malicious input
|