#!/usr/bin/env bash # Pre-commit hook to: # 1. Warn about large deletions in critical files (prevents clobbering during merges) # 2. Block commits containing secrets/credentials (prevents leaks) RED='\033[0;31m' YELLOW='\033[1;33m' GREEN='\033[0;32m' NC='\033[0m' # No Color NODE_BIN="${NODE_BIN:-node}" ############################################################################### # PART 1: SECRET DETECTION ############################################################################### echo -e "${GREEN}๐Ÿ”’ Scanning for secrets...${NC}" # Patterns to detect secrets (add new patterns here as needed) # Format: "PATTERN_NAME:REGEX" # NOTE: Use POSIX extended regex compatible with grep -E SECRET_PATTERNS=( # SMTP/Email credentials - hardcoded passwords "SMTP_PASSWORD:(^|[[:space:](;])SMTP_PASS[[:space:]]*=[[:space:]]*['\"][^'\"[:space:]]{8,}['\"]" # Google App Passwords (4 groups of 4 lowercase letters) # Only match when it looks like a credential assignment, not prose "GOOGLE_APP_PASSWORD:Pass(word)?[[:space:]]*[:=][[:space:]]*.*[a-z]{4} [a-z]{4} [a-z]{4} [a-z]{4}" # API Keys and Tokens (generic patterns) "AWS_KEY:AKIA[0-9A-Z]{16}" "GITHUB_TOKEN:gh[pousr]_[A-Za-z0-9_]{36,}" "PRIVATE_KEY:BEGIN.*PRIVATE KEY" "JWT_SECRET:jwt[_-]?secret[[:space:]]*[=:][[:space:]]*['\"][^'\"]{16,}['\"]" # Database connection strings with passwords "MONGODB_URI:mongodb.*://[^:]+:[^@]+@" "POSTGRES_URI:postgres.*://[^:]+:[^@]+@" # Generic secret patterns (hardcoded, not env var references) "PASSWORD_HARDCODE:pass(word)?[[:space:]]*[:=][[:space:]]*['\"][^'\"$]{8,}['\"]" "SECRET_HARDCODE:secret[[:space:]]*[:=][[:space:]]*['\"][^'\"$]{16,}['\"]" "API_KEY_HARDCODE:api[_-]?key[[:space:]]*[:=][[:space:]]*['\"][^'\"$]{16,}['\"]" ) # Files/paths to exclude from secret scanning EXCLUDE_PATTERNS=( "*.lock" "package-lock.json" "yarn.lock" ".githooks/pre-commit" # Don't flag this file's patterns "*.md" # Be more lenient with docs (still scan but warn only) ) # Known false positives to ignore (exact strings that trigger patterns but aren't secrets) FALSE_POSITIVE_ALLOWLIST=( "Styles" # UI label, triggers PASSWORD_HARDCODE via case-insensitive "s****s" "STYLES" # Same as above "styles" # Same as above "Second pass" # Code comment in chat.mjs "class=" # HTML class attributes "className=" # React className "AC_PASS" # Default wifi AP password (public, not a secret) "enter password" # UI label in wifi.mjs password prompt "Seed pass" # GPU shader comment in gpu-effects.mjs ) secret_errors=0 secret_warnings=0 # Get list of staged files (excluding deletions) staged_files=$(git diff --cached --name-only --diff-filter=d) for file in $staged_files; do # Skip binary files if file "$file" 2>/dev/null | grep -q "binary"; then continue fi # Check if file should be excluded skip_file=false warn_only=false for exclude in "${EXCLUDE_PATTERNS[@]}"; do if [[ "$file" == $exclude ]] || [[ "$file" == *"$exclude" ]]; then if [[ "$exclude" == "*.md" ]]; then warn_only=true else skip_file=true fi break fi done if $skip_file; then continue fi # Get the staged content of the file staged_content=$(git show ":$file" 2>/dev/null) for pattern_entry in "${SECRET_PATTERNS[@]}"; do pattern_name="${pattern_entry%%:*}" pattern_regex="${pattern_entry#*:}" # Search for pattern in staged content if echo "$staged_content" | grep -qiE "$pattern_regex"; then matching_lines=$(echo "$staged_content" | grep -niE "$pattern_regex" | head -3) # Check if all matches are in the allowlist (false positives) is_false_positive=true while IFS= read -r line; do line_is_allowed=false for allowed in "${FALSE_POSITIVE_ALLOWLIST[@]}"; do if echo "$line" | grep -qi "$allowed"; then line_is_allowed=true break fi done if ! $line_is_allowed; then is_false_positive=false break fi done <<< "$matching_lines" # Skip if this is a known false positive if $is_false_positive; then continue fi if $warn_only; then echo -e "${YELLOW}โš ๏ธ Possible secret in ${file} (${pattern_name})${NC}" echo "$matching_lines" | while read -r line; do echo -e " ${YELLOW}$line${NC}" done secret_warnings=$((secret_warnings + 1)) else echo -e "${RED}๐Ÿšจ SECRET DETECTED in ${file} (${pattern_name})${NC}" echo "$matching_lines" | while read -r line; do # Mask the actual secret value masked=$(echo "$line" | sed -E 's/([=:][[:space:]]*['"'"'"]?)([^'"'"'"]{4})[^'"'"'"]*([^'"'"'"]{2})(['"'"'"]?)/\1\2****\3\4/g') echo -e " ${RED}$masked${NC}" done secret_errors=$((secret_errors + 1)) fi fi done done if [ "$secret_errors" -gt 0 ]; then echo "" echo -e "${RED}โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•${NC}" echo -e "${RED} ๐Ÿšจ $secret_errors SECRET(S) DETECTED - COMMIT BLOCKED${NC}" echo -e "${RED}โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•${NC}" echo "" echo "To fix:" echo " 1. Remove the secrets from the staged files" echo " 2. Use environment variables instead" echo " 3. Store secrets in vault (aesthetic-computer-vault/)" echo "" echo "To bypass (NOT RECOMMENDED): git commit --no-verify" echo "" exit 1 fi if [ "$secret_warnings" -gt 0 ]; then echo -e "${YELLOW}โš ๏ธ $secret_warnings possible secret(s) in docs - review carefully${NC}" fi echo -e "${GREEN}โœ“ No secrets detected${NC}" echo "" ############################################################################### # PART 2: CRITICAL FILE PROTECTION (prevents merge clobbering) ############################################################################### # Critical files that should never lose significant content CRITICAL_FILES=( "artery/artery-tui.mjs" "dotfiles/dot_config/emacs.el" "system/public/aesthetic.computer/lib/udp.mjs" "system/public/aesthetic.computer/lib/logs.mjs" "system/public/aesthetic.computer/lib/speaker.mjs" "system/public/aesthetic.computer/bios.mjs" ) # Minimum expected line counts for critical files (set conservatively) declare -A MIN_LINES MIN_LINES["artery/artery-tui.mjs"]=5000 MIN_LINES["dotfiles/dot_config/emacs.el"]=1000 MIN_LINES["system/public/aesthetic.computer/lib/udp.mjs"]=150 MIN_LINES["system/public/aesthetic.computer/lib/logs.mjs"]=100 MIN_LINES["system/public/aesthetic.computer/lib/speaker.mjs"]=800 MIN_LINES["system/public/aesthetic.computer/bios.mjs"]=200 # Threshold for warning about large deletions (percentage) DELETION_THRESHOLD=30 RED='\033[0;31m' YELLOW='\033[1;33m' NC='\033[0m' # No Color warnings=0 for file in "${CRITICAL_FILES[@]}"; do # Check if file is being modified in this commit if git diff --cached --name-only | grep -q "^${file}$"; then # Get current staged line count staged_lines=$(git show :${file} 2>/dev/null | wc -l) # Get HEAD line count (before this commit) head_lines=$(git show HEAD:${file} 2>/dev/null | wc -l) if [ "$head_lines" -gt 0 ] && [ "$staged_lines" -gt 0 ]; then # Calculate deletion percentage deleted=$((head_lines - staged_lines)) if [ "$deleted" -gt 0 ]; then pct=$((deleted * 100 / head_lines)) if [ "$pct" -ge "$DELETION_THRESHOLD" ]; then echo -e "${RED}โš ๏ธ WARNING: Large deletion detected in ${file}${NC}" echo -e " ${YELLOW}Before: ${head_lines} lines โ†’ After: ${staged_lines} lines (${pct}% deleted)${NC}" warnings=$((warnings + 1)) fi fi fi # Check minimum line count min=${MIN_LINES[$file]} if [ -n "$min" ] && [ "$staged_lines" -lt "$min" ]; then echo -e "${RED}โš ๏ธ WARNING: ${file} is below minimum expected size${NC}" echo -e " ${YELLOW}Current: ${staged_lines} lines, Expected minimum: ${min} lines${NC}" warnings=$((warnings + 1)) fi fi done if [ "$warnings" -gt 0 ]; then echo "" echo -e "${YELLOW}โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•${NC}" echo -e "${YELLOW} $warnings file(s) may have been accidentally clobbered!${NC}" echo -e "${YELLOW} This often happens during merge conflict resolution.${NC}" echo -e "${YELLOW}โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•${NC}" echo "" echo "To proceed anyway, use: git commit --no-verify" echo "To abort and fix, use: git reset HEAD" echo "" exit 1 fi ############################################################################### # PART 3: PAPERS โ€” oven builds PDFs automatically on push to main # (see papers/SCORE.md ยง Building ยง Automatic) # No local rebuild needed in pre-commit. ############################################################################### exit 0