playbook/outfitter-agents/plugins/outfitter/skills/claude-hooks/references/security.md

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