fix(verify-no-secrets): resolve repo root via git, allow scaffolding under secrets/

Two defects found by testing the guard against itself:

1. When invoked through the .git/hooks/pre-commit symlink, deriving the repo
   root from dirname(BASH_SOURCE)/.. resolved to .git/ instead of the work
   tree, so the hook scanned nothing and never blocked. Use
   'git rev-parse --show-toplevel' instead.

2. The blanket secrets/ rule rejected secrets/.gitkeep. Replaced with an
   explicit allowlist: .gitkeep, README.md, *.example, *.template.

Verified: a staged file containing a Telegram bot token now aborts the commit
and leaves HEAD unchanged.
This commit is contained in:
Kai
2026-08-26 22:48:54 -07:00
parent cbba8faabc
commit 734e63aa28
+16 -3
View File
@@ -16,7 +16,13 @@
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
set -uo pipefail set -uo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # Resolve the repository root via git, not via BASH_SOURCE: when this script is
# invoked through the .git/hooks/pre-commit symlink, dirname(BASH_SOURCE)/..
# resolves to .git/ rather than the work tree.
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || {
echo "verify-no-secrets: not inside a git work tree" >&2
exit 1
}
cd "$REPO_ROOT" || exit 1 cd "$REPO_ROOT" || exit 1
MODE="${1:-staged}" MODE="${1:-staged}"
@@ -40,14 +46,21 @@ fail() {
} }
# --- Rule 1: filenames that must never be committed ------------------------ # --- Rule 1: filenames that must never be committed ------------------------
FORBIDDEN_NAMES='(^|/)(models\.json|auth\.json|trust\.json|models-store\.json)$|(^|/)\.env$|\.env\.[^/]*$|(^|/)id_(ed25519|rsa)|\.(pem|p12)$|\.rendered(\..*)?$|(^|/)secrets/(?!.*\.(example|template)$)' FORBIDDEN_NAMES='(^|/)(models\.json|auth\.json|trust\.json|models-store\.json)$|(^|/)\.env$|\.env\.[^/]*$|(^|/)id_(ed25519|rsa)|\.(pem|p12)$|\.rendered(\..*)?$'
# Inside secrets/ only documentation and placeholder scaffolding may be tracked.
SECRETS_ALLOWED='(^|/)secrets/(\.gitkeep|README\.md|.*\.(example|template))$'
for f in "${FILES[@]}"; do for f in "${FILES[@]}"; do
# .env.example / .env.template are allowed # Templates and examples are the intended way to track credential-shaped files.
case "$f" in case "$f" in
*.env.example|*.env.template|*.example|*.template) continue ;; *.env.example|*.env.template|*.example|*.template) continue ;;
esac esac
if printf '%s' "$f" | grep -qP "$FORBIDDEN_NAMES"; then if printf '%s' "$f" | grep -qP "$FORBIDDEN_NAMES"; then
fail "$f" "filename is on the never-commit list" fail "$f" "filename is on the never-commit list"
continue
fi
if printf '%s' "$f" | grep -qP '(^|/)secrets/' \
&& ! printf '%s' "$f" | grep -qP "$SECRETS_ALLOWED"; then
fail "$f" "files under secrets/ may only be .gitkeep, README.md, *.example or *.template"
fi fi
done done