ADR 024: Gitleaks (Secret Scanning)

Accepted
2026-07-20

This legacy URL now resolves to the canonical ADR.

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:

  1. 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.
  2. 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:secrets task runs gitleaks git --pre-commit --staged after lint-staged in the Husky hook, so a staged secret blocks the commit while it is still only on the laptop. --redact keeps the candidate value out of terminal scrollback.
  • CI, Security-tab posture (ADR 047 / ADR 049 pattern): a workflow scans the full history on PRs, main pushes, 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

ToolRoleProsConsVerdict
gitleaks (accepted)Secret detectionDe-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 protectionPush-time blockNative, 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.
TruffleHogDetection + verificationVerifies 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 detectionMature baseline workflow.Python toolchain in a Node monorepo; slower-moving project.Rejected: gitleaks covers it in one binary.
gitleaks-actionSame engine, packagedTurnkey.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/aZero 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.toml allowlist 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.