Context
Secrets management is solved at the storage layer. Doppler holds runtime
secrets (ADR 035), GitHub
environments hold CI credentials
(ADR 018). The unsolved
layer is accidental commits: a .env pasted into a script, a connection
string in a test fixture, an API key in a debugging commit.
The existing net for that is GitHub secret scanning, and it has two structural limits:
- It fires after the push. On a public repo that is too late by definition: the secret is in clone caches, mirrors, and scrapers the moment it lands. History rewrites un-publish nothing; the only real remediation is rotation. Post-push detection reports an incident after the guard has already failed.
- It matches known provider patterns. Tokens with vendor-recognisable shapes are caught; a generic high-entropy string, a database URL with an inline password, or a private key in an unusual wrapper may not be.
On the determinism axis of ADR 048: AI reviewers might notice a pasted credential, non-deterministically, at PR time, also after the secret has already left the laptop.
Decision
Adopt gitleaks, mise-pinned like every other scanner, at two points in the pipeline:
- Pre-commit (the layer that actually prevents leaks): a
//:security:secretstask runsgitleaks git --pre-commit --stagedafter lint-staged in the Husky hook, so a staged secret blocks the commit while it is still only on the laptop.--redactkeeps the candidate value out of terminal scrollback. - CI, Security-tab posture (ADR 047 /
ADR 049 pattern): a workflow scans
the full history on PRs,
mainpushes, and a weekly Wednesday schedule, uploading SARIF with--exit-code 0. Findings gate via Code Scanning's new-alert behaviour rather than failing the job. A historical finding predating the scan cannot be fixed by whichever PR happens to trip over it, and historical findings require rotation. A red build cannot remediate them. A genuine tool failure still fails the job, so the scan cannot silently skip itself.
GitHub secret scanning stays on; the two overlap on known provider patterns (harmless redundancy, per the ADR 048/049 precedent) and differ everywhere that matters: timing (pre-commit vs post-push) and breadth (generic entropy and custom rules vs vendor patterns).
Why It Adds Something New
Every other scanner in the stack examines code after it is shared. The pre-commit hook is the only place in the entire pipeline where detection precedes disclosure, and on a public repo that distinction is the whole game. The CI scan adds the complementary posture view: the complete history audited by a ruleset broader than vendor patterns, with findings in the same Security tab as Trivy, zizmor, and Scorecard.
Alternatives Considered
| Tool | Role | Pros | Cons | Verdict |
|---|---|---|---|---|
| gitleaks (accepted) | Secret detection | De-facto standard; single binary in mise; pre-commit + full-history modes; SARIF; TOML allowlist config when needed. | Pattern/entropy-based → occasional false positives to allowlist. | Accepted. |
| GitHub push protection | Push-time block | Native, free on public repos, zero config. | Known provider patterns only; fires at push, later than pre-commit; not a substitute for history auditing. | Complementary, worth enabling in repo settings, not a replacement. |
| TruffleHog | Detection + verification | Verifies candidates against live APIs (fewer false positives). | Verification means sending candidates to providers; heavier tool; OSS/paid split has churned. | Rejected: verification isn't worth the weight here. |
| detect-secrets (Yelp) | Baseline-driven detection | Mature baseline workflow. | Python toolchain in a Node monorepo; slower-moving project. | Rejected: gitleaks covers it in one binary. |
| gitleaks-action | Same engine, packaged | Turnkey. | Org licensing terms; less control than running the mise-pinned binary the hook already uses. | Rejected: run the same binary both places instead. |
| Status quo (GitHub secret scanning only) | n/a | Zero effort. | Post-push detection only, vendor patterns only. | Rejected: detection after publication requires rotation and cannot prevent exposure. |
Where It Sits On The Adoption Curve
Late majority. Gitleaks has been the default open-source secret scanner for years, with a stable CLI, huge deployment base, deep LLM corpus (LLM-Optimized). No Goldilocks trade-off to accept this time.
Relation To Building Philosophy
- Short Feedback Loops. The loop cannot get shorter: the finding lands before the commit exists.
- Less Is More. One binary serving both the hook and CI; findings in the existing Security tab; no platform, no account.
- LLM-Optimized. Agents commit autonomously in this repo; a deterministic pre-commit gate on secrets is the control that scales with that, rather than hoping each agent notices.
- Respect Goodhart's Law. Historical findings surface for triage instead of failing unrelated PRs. Pressure lands on rotating the credential, not on silencing the scanner.
Gaps & Risks
- False positives (test fixtures, example configs) will need
.gitleaks.tomlallowlist entries over time; the config is committed and reviewable, so exceptions are visible rather than silent. - Pattern-based detection is not proof of absence. A secret in a shape no rule anticipates still gets through; pre-commit reduces the risk without guaranteeing absence.
- The hook can be bypassed (
--no-verify); the CI history scan is the backstop that still catches what skipped the hook.
Consequences
Positive
- Secret detection moves from post-push (irreversible on a public repo) to pre-commit (still private, still preventable).
- Full-history coverage with generic/entropy rules, surfaced in the Security tab alongside Trivy/zizmor/Scorecard.
- Free, one binary, mise/CI parity like every other scanner in the stack.
Negative
- Pre-commit gains a scan step (sub-second on staged changes).
- Occasional false positives require allowlist maintenance in
.gitleaks.toml.