Almost every code review I run finds at least one credential in git history. Usually it is old, usually someone noticed and removed it in a later commit, and usually nobody rotated it — because removing it felt like fixing it.
It is not. Every clone of the repository still has it. Every fork, every CI cache, every laptop that pulled that branch, and any mirror a service made while it was there.
Scan the history, not the checkout
A grep of the current tree finds nothing, which is exactly why teams believe they are clean. Scan every commit on every branch.
# Full-history scan, not just HEAD
gitleaks detect --source . --log-opts="--all --full-history"
trufflehog git file://. --json --only-verified
# Quick sanity check on what a scanner might miss
git log --all --full-history -p | grep -nE "BEGIN (RSA|OPENSSH|EC) PRIVATE KEY"
- Run it against every branch, including ones nobody has merged.
- Include submodules and any vendored directories.
- Check the CI configuration and its logs — build output is a common secondary leak.
- Look in test fixtures. Real credentials end up in them constantly, on the theory that test data does not matter.
Rotate first, argue about history later
The response order matters, because the window between discovery and rotation is the only part you control.
- Rotate the credential. Immediately, before writing the incident note.
- Check what it could reach and review the logs for that period — the useful question is not whether it leaked, but whether it was used.
- Then decide whether to rewrite history. For a public repository, yes. For a private one with rotation done, it is hygiene rather than remediation.
- If you do rewrite, coordinate it: everyone re-clones, and any fork that does not is still holding the old objects.
Stop the next one
Detection after the fact is a losing game played forever. Two gates end most of it.
- Pre-commit hooks that block obvious secrets locally. Fast feedback, and developers stop fighting it once it never false-positives on their code.
- CI scanning on every pull request, failing the build on a verified secret. The build failure is what makes it real.
- A .env.example convention plus a secret manager, so nobody needs a real value in the repository to run the app.
- Scoped, short-lived credentials wherever the platform offers them — OIDC federation for CI beats any static key you have to protect.
Tune it or lose it
A scanner that fires on every random hex string gets disabled within a month, and then you have neither scanning nor the illusion of it. Spend the hour: allowlist the known false positives, prefer verified-only detection where the tool supports it, and make the failure message say exactly which file, which line, and what to do next.