#!/bin/sh # pre-commit-secret-scan — blocks commits that would add real credentials. # # Deployed via core.hooksPath (e.g. ~/.config/git/hooks/pre-commit) so it runs # for EVERY repository on the machine. Fails OPEN (never blocks) if git itself # is unavailable. Override only when you're sure: SKIP_SECRET_SCAN=1 git commit # # Pattern set covers the key formats used in this house: OpenAI sk-proj-, # OpenRouter sk-or-v1-, GitHub ghp_, Tavily tvly-, Gemini AIza*, ntfy-style # AQ.Ab*, JWTs (eyJ...), generic sk-... and Bearer tokens, private key blocks. # Lines that only REFERENCE keys (nix builtins.getEnv, literal '...' # placeholders) are ignored. [ -n "$SKIP_SECRET_SCAN" ] && exit 0 # fail-open: not a git repo, or git unavailable git rev-parse --git-dir >/dev/null 2>&1 || exit 0 # nothing staged -> nothing to check [ -n "$(git diff --cached --name-only 2>/dev/null)" ] || exit 0 hits=$(git diff --cached -U0 --no-color 2>/dev/null | awk ' /^diff --git/ { f=$3; sub(/^a\//,"",f) } /^\+/ && $0 !~ /^\+\+\+/ { l = substr($0, 2) if (l ~ /getEnv|\.\.\./) next if (l ~ /sk-proj-[A-Za-z0-9._-]{20,}|sk-or-v1-[A-Za-z0-9._-]{20,}|ghp_[A-Za-z0-9]{20,}|tvly-[A-Za-z0-9]{20,}|AIza[A-Za-z0-9_-]{30,}|AQ\.Ab[A-Za-z0-9._-]{20,}|eyJ[A-Za-z0-9._-]{30,}|sk-[A-Za-z0-9]{25,}|Bearer [A-Za-z0-9._-]{25,}|BEGIN [A-Z ]*PRIVATE KEY/) print f ": " l }') [ -z "$hits" ] && exit 0 echo "⛔ pre-commit secret scan BLOCKED — possible credentials in staged changes:" >&2 echo "$hits" | sed -E ' s/(sk-proj-[A-Za-z0-9._-]{6})[A-Za-z0-9._-]+/\1…/g s/(sk-or-v1-[A-Za-z0-9._-]{6})[A-Za-z0-9._-]+/\1…/g s/(ghp_[A-Za-z0-9]{4})[A-Za-z0-9]+/\1…/g s/(tvly-[A-Za-z0-9]{4})[A-Za-z0-9]+/\1…/g s/(AIza[A-Za-z0-9_-]{4})[A-Za-z0-9_-]+/\1…/g s/(AQ\.Ab[A-Za-z0-9._-]{4})[A-Za-z0-9._-]+/\1…/g s/(eyJ[A-Za-z0-9._-]{8})[A-Za-z0-9._-]+/\1…/g s/(sk-[A-Za-z0-9]{6})[A-Za-z0-9]+/\1…/g s/(Bearer [A-Za-z0-9._-]{6})[A-Za-z0-9._-]+/\1…/g' >&2 echo "→ Remove the secret and re-commit. If it's a false positive: SKIP_SECRET_SCAN=1 git commit" >&2 exit 1