Secret Scanning: Stop Secrets From Leaking Into Git

Secret scanning is the automated detection of credentials, API keys, tokens, and certificates in source code before or after they reach a git repository. It runs at three points: on your machine before a commit, in the CI pipeline before a merge, and continuously against the hosted repository. GitHub's own secret scanning program checks pushed content against token formats from dozens of major providers by default, no configuration required.

Why do secrets end up in git anyway #

Every engineer knows not to commit credentials. They end up in git constantly. The pattern is almost always the same: a developer hardcodes an API key while debugging a third-party integration, gets the thing working, and moves on to the next bug without stripping the key back out before the commit. A .env file gets created for local testing and the .gitignore update happens after the first commit, not before. Someone pastes a database connection string into a commit message to document what they were testing against. A config file gets checked in "temporarily" to unblock a deploy, with a mental note to remove it that never gets acted on.

None of this is negligence in the dramatic sense. It's the natural result of optimizing for speed under deadline pressure, in a workflow where nothing stops you from committing a secret. Git was not built with credential hygiene in mind, and by default it will happily version anything you hand it.

Why is a leaked secret in git history so hard to undo? #

Because deleting the file in a later commit doesn't delete the secret. Git tracks history as a chain of snapshots, and the old snapshot containing the secret is still reachable through the commit graph, git log, and git blame even after the file is gone from the working tree. Anyone who cloned the repo before the fix has a full copy of every commit, including the one with the exposed key, sitting on their disk whether they ever check it out or not.

If the repository is public, the exposure window is measured in minutes, not days. Bots scrape public GitHub pushes continuously looking for patterns that match cloud provider keys, database URLs, and webhook tokens. A key pushed to a public repo at 2 a.m. can be scanned, extracted, and tested for validity before you've had coffee. Private repos aren't immune either: they get cloned to laptops, forked into personal accounts, mirrored to CI runners, and archived in backups, all of which extend the blast radius past whatever access control the platform enforces on the original repo.

The three layers of defense #

No single scanning point catches everything, which is why this works best as three checks stacked in sequence rather than one you pick.

Layer 1: pre-commit, before the secret leaves your machine #

A git hook that scans staged changes before git commit completes is the cheapest place to catch a secret, because at that point it hasn't gone anywhere yet. Tools like gitleaks and detect-secrets run entirely locally, comparing staged diffs against regex and entropy-based patterns for API keys, private keys, and tokens. If a match is found, the commit is blocked and nothing leaves your laptop.

A minimal .pre-commit-config.yaml entry using gitleaks looks like this:

yaml.yaml

repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
hooks:
- id: gitleaks

Or with detect-secrets, which also supports a baseline file to track accepted false positives:

yaml.yaml

repos:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: ["--baseline", ".secrets.baseline"]

Layer 2: pre-push and CI, for what got past the hook #

Pre-commit hooks are opt-in on every developer's machine, and git commit --no-verify skips them entirely, sometimes deliberately, sometimes out of habit when a hook is slow or misbehaving. That's why a scan step belongs in the CI pipeline too: it runs against every pushed branch regardless of local configuration and fails the build if a secret pattern is detected, catching what pre-commit missed, skipped, or bypassed. This is also the natural place to run the same gitleaks or detect-secrets tool against the full diff of a pull request rather than just staged files. For the rest of what belongs in that pipeline, see how to secure your CI/CD pipeline.

Layer 3: continuous and platform scanning, the safety net #

Even with both earlier layers in place, something eventually slips through: a fork with different hook settings, a direct push from an admin account, a secret format the regex list doesn't cover yet. GitHub's secret scanning and push protection features flag known token formats in real time and can block a push outright before it completes. GitGuardian runs a similar continuous scan across repositories and history, and for some supported providers can trigger automatic revocation of a leaked token the moment it's detected, cutting the exposure window from hours to seconds.

What do you do when a secret leaks? #

Rotate it immediately. The value is compromised the moment it's pushed, even to a private repository, even if you catch it and delete the file within minutes. Treat "was it accessed" as unknowable and act as if it was.

Deleting the file in a new commit is not a fix; the secret is still in history as described above. Rewriting history with git filter-repo or BFG Repo-Cleaner can strip the secret out of every commit and force-push a clean history, and that's worth doing to reduce future exposure and keep the repo tidy. But it only helps going forward. It does nothing to undo exposure that already happened, and it doesn't reach anyone who already cloned the repo with the old commits intact. Rotation is the step that actually neutralizes the leak; history rewriting is cleanup on top of it, not a substitute for it.

After rotating, check the provider's access logs for the credential if they're available, and confirm the new value is deployed everywhere the old one was before you consider the incident closed. This whole workflow sits inside the broader practice covered in application security best practices.

The call we'd make #

Put a pre-commit hook on every repo with gitleaks or detect-secrets and treat a CI scan step as non-negotiable, not optional. Turn on GitHub push protection or a GitGuardian integration as the backstop, not the primary defense. And write down a rotation runbook now, before you need it, so the response to a leaked secret is a five-minute checklist instead of a scramble.