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

11 KiB

Security Best Practices

Comprehensive security guidance for Claude Code hooks.

Input Validation

Validate All Input

Always validate and sanitize hook input before use:

#!/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:

#!/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:

#!/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:

#!/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

# WRONG - vulnerable to injection
rm $FILE_PATH
cd $DIRECTORY
echo $CONTENT

# CORRECT - properly quoted
rm "$FILE_PATH"
cd "$DIRECTORY"
echo "$CONTENT"

Avoid eval

# WRONG - dangerous
eval "$USER_COMMAND"

# CORRECT - use specific commands
if [[ "$USER_COMMAND" == "format" ]]; then
  black "$FILE_PATH"
fi

Validate Command Patterns

#!/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:

#!/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

#!/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" "$@"
#!/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

#!/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

#!/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

#!/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

{
  "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

#!/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

#!/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

#!/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

#!/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

#!/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

#!/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

#!/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