- Add auto-checkpoint.sh (Stop event, git stash automation) - Add typecheck-on-save.sh (PostToolUse, TypeScript validation) - Add test-on-change.sh (PostToolUse, smart test detection) - Add file-guard.sh (PreToolUse, unified file protection) - Add ClaudeKit evaluation (3/5, patterns extracted) - Version bump 3.21.0 → 3.21.1 (sync across all docs) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
113 lines
2.6 KiB
Bash
Executable file
113 lines
2.6 KiB
Bash
Executable file
#!/bin/bash
|
|
# .claude/hooks/file-guard.sh
|
|
# Event: PreToolUse
|
|
# Unified file protection with pattern matching and bash bypass detection
|
|
# Prevents Claude from reading/writing protected files
|
|
|
|
set -euo pipefail
|
|
|
|
INPUT=$(cat)
|
|
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
|
|
|
|
# Only check file operations
|
|
if [[ "$TOOL_NAME" != "Read" && "$TOOL_NAME" != "Write" && "$TOOL_NAME" != "Edit" ]]; then
|
|
exit 0
|
|
fi
|
|
|
|
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
|
|
# Load protection patterns from .agentignore or .aiignore
|
|
IGNORE_FILE=""
|
|
if [[ -f ".agentignore" ]]; then
|
|
IGNORE_FILE=".agentignore"
|
|
elif [[ -f ".aiignore" ]]; then
|
|
IGNORE_FILE=".aiignore"
|
|
fi
|
|
|
|
# Default critical patterns (if no ignore file)
|
|
CRITICAL_PATTERNS=(
|
|
".env"
|
|
".env.local"
|
|
".env.production"
|
|
"*.key"
|
|
"*.pem"
|
|
"*.p12"
|
|
"credentials.json"
|
|
"secrets.yaml"
|
|
"config/secrets/*"
|
|
".aws/credentials"
|
|
".ssh/id_*"
|
|
)
|
|
|
|
# Check against patterns
|
|
is_protected() {
|
|
local file="$1"
|
|
|
|
# Check ignore file patterns
|
|
if [[ -n "$IGNORE_FILE" ]]; then
|
|
while IFS= read -r pattern; do
|
|
# Skip comments and empty lines
|
|
[[ "$pattern" =~ ^#.*$ || -z "$pattern" ]] && continue
|
|
|
|
# Convert gitignore pattern to bash glob
|
|
if [[ "$file" == $pattern || "$file" =~ $pattern ]]; then
|
|
return 0
|
|
fi
|
|
done < "$IGNORE_FILE"
|
|
fi
|
|
|
|
# Check critical patterns
|
|
for pattern in "${CRITICAL_PATTERNS[@]}"; do
|
|
if [[ "$file" == $pattern || "$file" =~ $pattern ]]; then
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
# Detect bash variable expansion bypass attempts
|
|
detect_bypass() {
|
|
local file="$1"
|
|
|
|
# Check for variable expansion patterns
|
|
if [[ "$file" =~ \$\{?[A-Za-z_][A-Za-z0-9_]*\}? ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# Check for command substitution
|
|
if [[ "$file" =~ \$\( || "$file" =~ \` ]]; then
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# Validate file path
|
|
if [[ -z "$FILE_PATH" ]]; then
|
|
exit 0
|
|
fi
|
|
|
|
# Check for bypass attempts
|
|
if detect_bypass "$FILE_PATH"; then
|
|
cat << EOF
|
|
{
|
|
"block": true,
|
|
"systemMessage": "⛔ File access blocked: Variable expansion detected in path\n\nPath: $FILE_PATH\n\nThis looks like a bypass attempt. Use literal paths only."
|
|
}
|
|
EOF
|
|
exit 1
|
|
fi
|
|
|
|
# Check protection patterns
|
|
if is_protected "$FILE_PATH"; then
|
|
cat << EOF
|
|
{
|
|
"block": true,
|
|
"systemMessage": "⛔ File access blocked: Protected file\n\nPath: $FILE_PATH\n\nThis file is protected by .agentignore or security policy.\nTo access it, remove from ignore file and confirm manually."
|
|
}
|
|
EOF
|
|
exit 1
|
|
fi
|
|
|
|
exit 0
|