|
#!/usr/bin/env bash |
|
# git-secret-guard — Global pre-commit hook |
|
# Blocks commits containing API keys, tokens, and .env files. |
|
# https://gist.github.com/brennerspear/... |
|
# |
|
# Install globally: |
|
# mkdir -p ~/.config/git/hooks |
|
# cp pre-commit ~/.config/git/hooks/pre-commit |
|
# chmod +x ~/.config/git/hooks/pre-commit |
|
# git config --global core.hooksPath ~/.config/git/hooks |
|
# |
|
# Override for a single commit: |
|
# git commit --no-verify |
|
|
|
set -euo pipefail |
|
|
|
FAILED=0 |
|
|
|
# ─── Part 1: Block .env files ──────────────────────────────────────────────── |
|
|
|
bad_files=$(git diff --cached --name-only --diff-filter=ACR | grep -E '(^|/)\.env($|\..*)' | grep -v '\.env\.example$' || true) |
|
|
|
if [ -n "$bad_files" ]; then |
|
echo "🚫 BLOCKED: .env file(s) with potential secrets:" |
|
echo "$bad_files" | sed 's/^/ /' |
|
echo "" |
|
|
|
# Auto-fix .gitignore |
|
root=$(git rev-parse --show-toplevel 2>/dev/null || true) |
|
if [ -n "$root" ]; then |
|
touch "$root/.gitignore" |
|
changed=false |
|
for pattern in '.env' '.env.*' '!.env.example'; do |
|
if ! grep -qxF "$pattern" "$root/.gitignore" 2>/dev/null; then |
|
echo "$pattern" >> "$root/.gitignore" |
|
changed=true |
|
fi |
|
done |
|
if [ "$changed" = true ]; then |
|
echo " Auto-added .env patterns to .gitignore" |
|
git add "$root/.gitignore" |
|
fi |
|
|
|
# Unstage the bad files |
|
echo "$bad_files" | while read -r f; do |
|
git reset HEAD -- "$f" 2>/dev/null || true |
|
done |
|
echo " .env files unstaged automatically." |
|
echo "" |
|
fi |
|
|
|
FAILED=1 |
|
fi |
|
|
|
# ─── Part 2: Scan diffs for secret patterns ────────────────────────────────── |
|
|
|
# Each line: "pattern_regex ## human-readable label" |
|
SECRET_PATTERNS=( |
|
'sk-proj-[a-zA-Z0-9]{20,} ## OpenAI API key' |
|
'sk-or-v1-[a-zA-Z0-9]{20,} ## OpenRouter API key' |
|
'sk-ant-api[a-zA-Z0-9]{20,} ## Anthropic API key' |
|
'AIzaSy[a-zA-Z0-9_-]{33} ## Google API key' |
|
'GOCSPX-[a-zA-Z0-9_-]{20,} ## Google OAuth client secret' |
|
'ghp_[a-zA-Z0-9]{36} ## GitHub personal access token' |
|
'gho_[a-zA-Z0-9]{36} ## GitHub OAuth token' |
|
'github_pat_[a-zA-Z0-9_]{20,} ## GitHub fine-grained PAT' |
|
'glpat-[a-zA-Z0-9_-]{20,} ## GitLab personal access token' |
|
'xoxb-[0-9]{10,} ## Slack bot token' |
|
'xoxp-[0-9]{10,} ## Slack user token' |
|
'xoxs-[0-9]{10,} ## Slack session token' |
|
'AKIA[0-9A-Z]{16} ## AWS access key ID' |
|
'sk_live_[a-zA-Z0-9]{20,} ## Stripe live secret key' |
|
'sk_test_[a-zA-Z0-9]{20,} ## Stripe test secret key' |
|
'rk_live_[a-zA-Z0-9]{20,} ## Stripe restricted key' |
|
'pk_live_[a-zA-Z0-9]{20,} ## Stripe live publishable key' |
|
'SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43} ## SendGrid API key' |
|
'sq0csp-[a-zA-Z0-9_-]{43} ## Square OAuth secret' |
|
'eyJ[a-zA-Z0-9_-]{20,}\.eyJ[a-zA-Z0-9_-]{20,}\. ## JWT token' |
|
'npm_[a-zA-Z0-9]{36} ## npm access token' |
|
'pypi-[a-zA-Z0-9]{50,} ## PyPI API token' |
|
'hf_[a-zA-Z0-9]{34} ## Hugging Face token' |
|
'r8_[a-zA-Z0-9]{38} ## Replicate API token' |
|
'whsec_[a-zA-Z0-9]{20,} ## Webhook signing secret' |
|
) |
|
|
|
# Get only added lines from staged changes |
|
added_lines=$(git diff --cached -U0 --diff-filter=ACM 2>/dev/null | grep -E '^\+' | grep -v '^+++' || true) |
|
|
|
if [ -n "$added_lines" ]; then |
|
for entry in "${SECRET_PATTERNS[@]}"; do |
|
pattern=$(echo "$entry" | sed 's/ *## .*//') |
|
label=$(echo "$entry" | sed 's/.*## //') |
|
|
|
matches=$(echo "$added_lines" | grep -E "$pattern" 2>/dev/null || true) |
|
if [ -n "$matches" ]; then |
|
if [ "$FAILED" -eq 0 ]; then |
|
echo "🚫 BLOCKED: Potential secrets detected in staged changes:" |
|
echo "" |
|
fi |
|
echo " $label" |
|
echo "$matches" | head -3 | sed 's/^/ /' |
|
echo "" |
|
FAILED=1 |
|
fi |
|
done |
|
fi |
|
|
|
# ─── Result ─────────────────────────────────────────────────────────────────── |
|
|
|
if [ "$FAILED" -ne 0 ]; then |
|
echo "Commit blocked. Remove the secrets and try again." |
|
echo "Override: git commit --no-verify" |
|
exit 1 |
|
fi |
|
|
|
# ─── Chain to repo-local hook ──────────────────────────────────────────────── |
|
|
|
# If this repo has its own pre-commit hook, run it too. |
|
local_hook="$(git rev-parse --show-toplevel 2>/dev/null)/.git/hooks/pre-commit" |
|
if [ -x "$local_hook" ]; then |
|
exec "$local_hook" |
|
fi |