Push protection is a good control. It matches a push against known credential formats and rejects it before the secret reaches the remote, which turns a leaked key into a five-second annoyance instead of an incident.
It also does nothing about the commits already in your repository, and that gap is where most organisations actually live.
Two problems that look like one
Prospective: stop new secrets entering. Push protection, pre-commit hooks, developer education. Mostly solved, and cheaply.
Retrospective: deal with the secrets already committed. Not solved by any of the above, because they operate on new pushes and every historical secret arrived before you turned them on.
The retrospective problem is the larger one for any repository older than the control. A repo with five years of history has been touched by people who joined and left, during incidents when someone hardcoded a key to ship a fix, and in the early days before anyone thought about it.
Push protection's real limits
Signatures only. Detection works by matching known formats — provider prefixes, structured tokens, high-confidence patterns. That covers AWS keys, cloud provider tokens, and a long list of SaaS credentials, because those vendors publish their formats.
It does not reliably catch your own internal API keys, a database password in a connection string, a private key pasted into a YAML file, or a shared secret that happens to look like a random string. Anything without a distinctive shape is a coin flip.
New pushes only. Force-pushing an old branch, importing a repository, or restoring from a mirror can bring historical secrets past a control that only inspects the delta.
Bypassable by design. Push protection includes an override, because false positives on test fixtures would otherwise block work entirely. Overrides are logged. Somebody has to read the log.
Why deleting the commit does not work
The instinct on finding a historical secret is to remove it — rewrite history with git filter-repo, force-push, done.
It is not done, for four reasons.
Forks and clones keep it. Every clone taken before the rewrite still has the object. On a public repository that is an unbounded set of copies you cannot reach.
Hosting platforms cache unreachable objects. An orphaned commit can remain retrievable by SHA on some platforms long after the branch is rewritten, and the SHA is in the pull request history.
Mirrors, CI caches and backups. Build caches, artefact stores, backup snapshots and internal mirrors all hold copies on their own retention schedules.
Rewriting is disruptive. Every open branch and pull request needs rebasing, every clone needs re-cloning, and the pain is directly proportional to team size — which biases teams toward not doing it, and toward feeling like they have addressed the problem when they decide not to.
The correct first action is rotation. Invalidate the credential at the provider. Once it is revoked, the copies do not matter — the secret in a thousand clones is a string with no privileges attached. Rewriting history afterwards is tidying, and it is optional.
Doing the historical scan
Scan the full history, not the working tree:
gitleaks detect --source . --log-opts="--all"
trufflehog git file://. --since-commit HEAD~10000
Two things to plan for.
Verification matters more than detection. Tools that check whether a found credential is still live turn a list of two thousand hits into a list of nine that need action today. Without that, the output is unactionable and gets ignored, which is the same as not running it.
Triage by exposure, not by count. A live production key in a public repository is an incident. A revoked test credential in a private repo from 2019 is a cleanup task. Sorting by severity of the credential type alone will put those in the wrong order.
The control that actually reduces the rate
All of the above manages a problem rather than preventing it. The prevention is architectural: applications get credentials from an identity-based mechanism rather than a stored string.
Workload identity federation, instance roles, OIDC-based CI authentication to cloud providers, short-lived tokens from a secrets manager. In each case there is no long-lived credential to commit, because the credential does not exist as a value a human ever holds.
That migration is real work and it is not uniform across every system. But it is the only intervention that changes the shape of the problem instead of adding another filter to it — and the teams that have done it stop appearing in their own secret-scanning reports, which no amount of push protection achieves.