Secret & Identifier Gates¶
Two complementary gates prevent secrets and operator-identifying literals from landing in the public repo:
| Gate | Enforcement point | Authoritative? |
|---|---|---|
| gitleaks scan | pre-commit hook + CI (secret-scan.yml) |
CI is authoritative |
.env guard |
pre-commit hook + CI | CI is authoritative |
| Operator identifier deny-list | pre-commit hook (local) + CI | CI is authoritative |
Gate 1 — gitleaks¶
What it catches¶
gitleaks v8 uses the built-in ruleset (useDefault = true) plus any rules in
.gitleaks.toml at the repo root. It detects token shapes for HuggingFace,
OpenAI, Anthropic, GitHub PATs, AWS access keys, generic API key assignments,
and more.
Where it runs¶
Pre-commit hook (.github/hooks/pre-commit): if gitleaks is on PATH,
runs gitleaks protect --staged -c .gitleaks.toml --no-banner against the
staged index. If gitleaks is absent, the hook prints a warning and continues —
dev machines without it are not hard-blocked. Install with:
brew install gitleaks
CI (secret-scan.yml → job gitleaks): downloads gitleaks v8.30.1,
runs it against the entire PR commit range
(origin/<base>..HEAD). Fails the PR on any finding.
The CI job also explicitly checks that no bare .env file (files whose name
IS .env, not .env.example or .env.test) is tracked by git.
Allowlist policy¶
.gitleaks.toml extends the default ruleset and narrows false-positive scope:
-
Excluded paths —
.venv/,node_modules/,.cache/,.test_outputs/,test-results/,htmlcov/,*.ipynb,data/eval/runs/are skipped. These are vendored, generated, or historically-frozen trees; real secrets there are handled at the source, not the scan. -
Allowed regex patterns — narrow patterns for:
max_tokens=4096/max_tokens: 4096— LLM config, never a secret.- Model fingerprint digests (SHA-256/MD5 after
hash:/checksum:etc.). - RFC/ADR prose titles containing "key" or "secret" as English words.
- Shell
${VAR}/$VARreferences — variable names are not values. -
Explicit placeholder strings (
changeme,hf_xxx,<redacted>, etc.). -
NOT disabled —
generic-api-keyand all provider token rules remain active. Allowlist entries must be narrow; never add a blanket disable.
To add a new allowlist entry: edit .gitleaks.toml, keep the regex tight,
add a comment explaining why it is a false positive, verify with
gitleaks git -c .gitleaks.toml --log-opts="-3".
Gate 2 — Operator identifier deny-list¶
The operator's real name, email, domain, or other identifying literals must never appear in a commit. The deny-list of those literals is itself sensitive — committing it would publish the values. The solution:
CI (authoritative)¶
The literal list lives in a GitHub Actions secret named
SECURITY_IDENTIFIER_DENYLIST (newline-separated). The operator populates it
via Settings → Secrets and variables → Actions → New repository secret.
The secret-scan.yml job identifier-denylist:
- Reads the secret into the
DENYLISTenv var. - If empty/unset: prints a warning and passes (so the gate never blocks before the secret is populated).
- If set: builds the full PR diff, then
grep -Feach literal against the diff. On a match it prints the diff FILE path — never the matched literal — and fails the job.
Pre-commit (best-effort, local)¶
The hook checks for .git/identifier-denylist (one literal per line). This
file is:
- Inside
.git/— not tracked, never committed, not visible to anyone who clones the repo. - Populated by the operator manually on each machine:
cat > .git/identifier-denylist <<'EOF'
your.real@email.com
your-real-domain.com
EOF
If the file is absent the hook skips silently. If present, it grep -Fs each
literal against the staged diff and fails, printing only that a match was found
(never the literal value).
Checklist when the gate fires¶
-
gitleaks finding: remove the secret from staged files. Replace with an environment variable reference (
${SECRET_NAME}). If it is a documented false positive, add a narrow entry to.gitleaks.tomlallowlist with an explanation, then re-verify. -
.envguard: rename to.env.examplewith placeholder values. Ensure.envis in.gitignore. -
Identifier finding: remove or redact the literal from the staged file. Do not put the literal in a code comment, log line, or fixture — even redacted history is history.