Secrets scanning: pre-commit, CI, or both?
Both, and the two layers do different jobs that are often confused. A pre-commit hook prevents a mistake on a machine you do not control and can be skipped with a flag. A server-side check applies to every clone and leaves a record, but on both major forges it is bypassable by default and it only fires once the credential has left the developer's laptop. Neither is a remedy, because the moment a live credential reaches a shared system the only response that changes anything is rotation. Detection buys you the chance to rotate before someone else uses it.
Why is deleting the commit not a fix?
Because you cannot prove nobody read it. A pushed commit may have been fetched by every developer, cached by CI runners, mirrored to a backup, indexed by a forge search, or picked up by automated scrapers that watch public repositories for exactly this. Rewriting history removes the credential from the branch and changes none of that. The credential is compromised from the moment it is pushed, and the only action that restores the invariant is issuing a new one and invalidating the old.
This reorders the whole response. Rotate first, then clean the history, then work out how it got there. Teams that reverse this sequence spend an afternoon on a careful history rewrite while a valid key sits in a scraper's queue. The clean-up matters for hygiene and for anyone cloning later, but it is not the control.
It also changes what you want from a scanner. Speed of detection is worth more than completeness of history, because value decays in minutes. A tool that tells you within thirty seconds that a live cloud key was pushed is worth more than one that produces a beautifully complete report of every credential ever committed, though you eventually want both.
What does each detection layer actually catch?
They differ on three axes: whether they can be bypassed, whether they see history, and whether they can tell a live credential from a dead one. Choosing a layer means choosing which of those properties you need.
| Layer | Catches | Bypassable | Main cost |
|---|---|---|---|
| Pre-commit hook | The mistake before it exists, in the staged diff | Yes, with a skip flag or an unconfigured clone | Developer setup and a few seconds per commit |
| Server-side push protection at the forge | The credential as it arrives, before it lands on the branch | Yes by default on both major forges, until delegated bypass or an equivalent approval gate is configured | Rejected pushes and occasional false positives mid-flow |
| CI scan of the diff | What the pre-commit hook missed or skipped | No | Detection happens after the push, so rotation is already needed |
| Full history scan, scheduled | Everything committed before you had any of this | No | A large one-off backlog and noisy old test fixtures |
| Image and artefact layer scan | Credentials baked into a build, including deleted files in earlier layers | No | Often skipped entirely, and a common real source of leaks |
| Provider-side validity checking | Whether the found string is a working credential right now | No | Requires calling the provider, which needs care and permission |
How do the detectors work, and where do they fail?
Two mechanisms, with different failure modes. Pattern matching recognises credentials with a known shape, such as provider key prefixes, and is precise: few false positives, and it finds nothing it has no rule for. Entropy analysis flags high-randomness strings regardless of format, catches the credentials nobody wrote a rule for, and produces false positives on hashes, test fixtures, minified assets, base64 images and lockfile integrity digests.
Most teams want pattern rules blocking and entropy rules reporting, at least initially. That combination gives you a gate that almost never fires wrongly, plus a queue that occasionally finds something interesting. Inverting it, with entropy rules blocking, is the fastest way to have your secrets gate disabled by popular demand.
The gap both mechanisms share is credentials that look like ordinary strings: a database password that is a dictionary word, an internal service token with no distinctive prefix, a private key stored as a single line inside a configuration value. If your organisation issues credentials with no recognisable format, no scanner will reliably find them, and the answer is to change how credentials are issued rather than to buy a better detector.
What do you do about the years of history you already have?
Scan it once, then decide by validity rather than by volume. A full history scan of a mature repository commonly returns hundreds of hits, most of which are expired credentials, test fixtures and example values. Trying to triage that list by reading it is a week of work with almost no security outcome.
Instead, sort by whether the credential still works. Anything that can be checked against its provider gets checked, and the ones that validate become an incident queue with a rotation task each. Anything that cannot be checked gets rotated if it belongs to a system that still exists, and ignored with a note if it points at something decommissioned years ago.
Do not start the programme with a history rewrite across every repository. It breaks every open branch and local clone, consumes the goodwill the ongoing controls need, and reduces no risk once the credentials are rotated. Rewrite history where a still-valid, high-value credential was exposed, and record the decision for the rest.
Why is the real fix removing long-lived credentials?
Because a credential that does not exist cannot be committed. The bulk of what secrets scanners find is static, long-lived keys handed to developers and pipelines because that was the only way to authenticate. Replacing them with short-lived, workload-bound credentials removes both the incentive to paste one into a configuration file and the value of any that get exposed.
The concrete version of this in a pipeline is federated identity: the CI system presents a signed token asserting which repository, branch or workflow is running, and the cloud provider exchanges it for temporary credentials. No key is stored in the CI system, nothing needs rotating on a schedule, and a leaked log line is worth nothing an hour later. Setting up the trust relationship correctly matters, particularly the conditions on which repository and reference may assume which role, because a permissive condition turns your pipeline identity into a shared account.
Inside applications, the equivalent is fetching credentials at runtime from a secret manager with a short lease rather than injecting them as long-lived environment variables at build time. This is a larger change than adding a scanner and it is the one that ends the category, which is why it belongs in the roadmap rather than the backlog.
What should you set up this week?
Enable server-side detection on every repository, with pattern-based rules blocking and entropy-based rules reporting. That is usually a day of configuration and it applies to every clone and every developer, which makes it the one worth having first if you only get one.
Close the bypass in the same sitting, because push protection is bypassable by default. On GitHub, anyone with write access can push the blocked secret anyway by selecting a bypass reason such as a false positive or a promise to fix it later, and restricting that requires turning on delegated bypass, after which organisation owners, security managers and anyone on the bypass list can still bypass without asking. On GitLab, secret push protection is skipped by a skip directive in the commit message or by the secret_push_protection.skip_all push option. Configure the approval gate your forge offers and alert on every bypass, otherwise the layer you are relying on is advisory.
Then write the rotation runbook before you need it. For each of your top five credential types, record who can rotate it, how long rotation takes, what breaks during it and how you confirm the old value is dead. A detection at four in the afternoon is only useful if somebody knows how to invalidate the thing in the next ten minutes, and that knowledge is rarely written down until the second incident.
Last, scan one built container image rather than a repository. Credentials baked into image layers, including files deleted in a later layer, are a common source of real exposure and are missed entirely by teams who only scan source. If you find something there, you have also learned that your build process handles secrets in a way that needs changing.
Common questions
- Should secrets scanning run pre-commit or in CI?
- Both, because they solve different problems. A pre-commit hook can prevent the commit existing at all, but it runs on a machine you do not control and can be skipped, so it is a convenience for the author rather than assurance. A server-side or CI check applies to every clone, but by the time it fires the credential has already left the laptop and must be rotated. Forge push protection is also bypassable by default: on GitHub a developer with write access can push the blocked secret by selecting a bypass reason, and on GitLab a skip directive in the commit message or the secret_push_protection.skip_all push option does the same, so turn on delegated bypass or your forge's approval gate if you want it to hold. If you can only have one, take the server-side check, with that gate configured.
- Is removing a secret from git history enough?
- No. Once a commit is pushed, the credential may have been fetched by other developers, cached by CI runners, mirrored to backups or collected by automated scrapers that watch public repositories for exactly this. Rewriting history changes none of that, and you cannot prove nobody read it. Treat the credential as compromised, rotate it first, then clean the history for hygiene, then investigate how it got there in the first place.
- Why do secrets scanners produce so many false positives?
- Because entropy-based detection flags any high-randomness string, which catches credentials nobody wrote a rule for and also hashes, test fixtures, minified assets, embedded images and lockfile integrity digests. Pattern-based detection recognising known key formats is far more precise but finds nothing it lacks a rule for. Run pattern rules as a blocking gate and entropy rules as a reporting queue, not the other way round.
- How should you handle secrets found in old git history?
- Sort by whether the credential still works rather than by volume. Validate what can be validated against its provider and treat every live hit as an incident with a rotation task. Rotate anything unverifiable that belongs to a system still in use, and record a decision for the rest. Avoid a mass history rewrite across every repository: it breaks open branches and local clones and reduces no risk once the credentials are rotated.
- What stops secrets being committed in the first place?
- Not having long-lived credentials to commit. In pipelines that means federated identity, where the CI system presents a signed token asserting which repository and workflow is running and the cloud provider exchanges it for temporary credentials, so no static key is stored anywhere. In applications it means fetching short-lived credentials from a secret manager at runtime instead of injecting durable values at build time.
- Do container images need secrets scanning too?
- Yes, and skipping it is common. Build processes copy configuration files, private keys and cloud credentials into images, and a file deleted in a later layer is still present in the earlier one. Scanning a built image is a different check from scanning the repository, it finds a class of exposure source scanning cannot see, and a hit tells you your build handles secrets in a way that needs redesigning.