Safeguard
AppSec

How to Run a Secret Scan Across Your Codebase

A secret scan finds hardcoded credentials, API keys, and tokens in your code and history before an attacker does. Here is how to scan, what to catch, and how to respond.

Aisha Rahman
Security Analyst
7 min read

A secret scan is an automated search through your source code, configuration, and git history for credentials that should never have been committed: API keys, database passwords, private keys, and access tokens. The premise is uncomfortable but true: developers hardcode secrets, and those secrets end up in version control, where they persist in history long after someone deletes the offending line. A secret scan finds them so you can rotate and remove them before an attacker with read access to your repository does the same search first.

The stakes are high because a leaked credential is often a direct path in, no exploit chain required. A committed AWS access key can be used to spin up compute or read a storage bucket. A leaked database password gives whatever the account can reach. Automated bots continuously crawl public repositories and paste sites for exactly these strings, and the window between a public commit and an attempted use of the credential is frequently measured in minutes.

Why deleting the line is not enough

The single most important thing to understand about secrets in git is that removing a secret in a new commit does not remove it. Git keeps history. Anyone who clones the repository, or who reads it on the hosting platform, can check out the old commit and read the secret as it was. This is why a secret scan must cover the full commit history, not just the current working tree, and why the correct response to a leak is always rotation of the credential rather than a cleanup commit. You can rewrite history to purge the blob, and you sometimes should, but rewriting history does not un-leak a secret that was already public. Rotation is what actually closes the exposure.

How secret scanning works

Detectors use two broad techniques. The first is pattern matching with regular expressions tuned to the shape of known credential types. AWS access key IDs start with a fixed prefix and have a known length; GitHub tokens, Stripe keys, and Slack tokens each have recognizable formats. A good detector ships hundreds of these signatures. The second technique is entropy analysis: a long random-looking string with high Shannon entropy is probably a secret even if it matches no known format, because human-written text has low entropy and generated keys have high entropy.

Pattern matching gives precision, it knows an AWS key when it sees one, and entropy gives recall, it catches the custom token you invented that no signature knows about. The tradeoff is that entropy detection generates false positives on things like base64-encoded test fixtures, hashes, and minified JavaScript, so the better tools add validation. The most useful feature in a modern scanner is live verification: when it finds something that looks like an AWS key, it makes a harmless read-only API call to check whether the key is currently active. A verified-active finding is an emergency; an inactive or malformed match can wait.

Where to run the scan

Run secret scanning at three points, because each catches a different failure.

The strongest control is a pre-commit hook on the developer's machine. It scans the staged diff before the commit is created, so the secret never enters history at all. This is the cheapest possible fix, no rotation, no history rewrite, because the secret was stopped at the door. The catch is that hooks are local and can be bypassed, so they are a first line, not the only line.

The second point is the CI pipeline or a server-side check on push. This catches anything that slipped past the local hook and gives you an authoritative, unbypassable gate. Configure it to scan the incremental diff on pull requests for speed, and to fail the build on a verified finding.

The third is a scheduled full-history scan of every repository. This is your baseline and your safety net. The first time you run it against a mature codebase, expect findings, because secrets have been accumulating for years. Work through them, rotate what is live, and then rely on the incremental gates to keep the tree clean going forward.

Reducing false positives

The failure mode of secret scanning is alert fatigue. A scanner that cries wolf on every base64 string in a test fixture trains the team to click "ignore," and then a real leak gets ignored too. Fight this on three fronts. Use verification so live secrets are visibly distinct from inert matches. Maintain an allowlist for known-safe patterns, example keys in documentation, dummy values in tests, but keep the allowlist narrow and reviewed, because an over-broad ignore rule is how a real secret hides. And tune the entropy threshold to your codebase; a repo full of generated fixtures needs a higher bar than a repo of hand-written config.

Responding to a real leak

When a scan finds a live secret, the order of operations matters. Rotate first: generate a new credential and revoke the old one, so the leaked value stops working. This is the step that actually closes the exposure, and it comes before any cleanup. Then remove the secret from the code and replace it with a reference to a secrets manager or an environment variable injected at deploy time. Then, if the repository is public or widely cloned, consider whether to rewrite history to purge the blob, weighing the disruption of a force-push against the marginal benefit given the secret is already rotated. Finally, check access logs for the exposed credential to see whether it was used before you caught it. A leaked key that was never used is a near miss; one that shows unfamiliar API activity is an incident.

Preventing the leak in the first place

Detection is a backstop for a prevention failure. The durable fix is to make it hard to hardcode a secret at all. Inject secrets at runtime from a dedicated secrets manager rather than storing them in files. Keep a well-maintained .gitignore that excludes .env files and local config. Give developers a documented, easy way to get the credentials they need for local development so they are not tempted to paste a production key into a config file. A secret scan tells you where prevention broke; the goal is to make it break less often.

FAQ

What is the difference between a secret scan and a normal vulnerability scan?

A vulnerability scan looks for known flaws in code or dependencies. A secret scan looks specifically for exposed credentials, keys, and tokens that were committed by mistake. They solve different problems and are usually separate tools.

Do I need to scan git history or just the current code?

Both. History is essential, because a secret committed months ago is still readable in old commits even if the current code no longer contains it. A current-tree-only scan will miss the majority of real exposures.

Should I rewrite git history after finding a secret?

Rotate the credential first; that is what closes the exposure. History rewriting removes the blob but does not un-leak a value that was already public, so treat it as optional cleanup rather than the fix.

How do I stop developers from committing secrets?

Combine a pre-commit hook that blocks staged secrets locally with a server-side CI gate that cannot be bypassed, and give developers an easy path to real credentials through a secrets manager so they are not tempted to hardcode them.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.