Deleting an API key in a new commit leaves the earlier commits still holding it, and 3 of the 21 third-party apps I audited in June and July 2026 had a real secret permanently in git history. Secret scanning searches a repository and its full history for keys, tokens and passwords; the job ends when each live find is rotated and proven dead.
What secret scanning is, and what a secret scanner looks for
Secret scanning is an automated search of a repository and its full history for credentials: API keys, tokens, passwords and private keys. A secret scanner combines up to three methods: known provider patterns, entropy checks for random strings, and live verification that asks the provider whether the key still works.
Secrets scanning and secret detection are the same job under other names. Those 21 apps from the opening line are the ones I audited in June and July 2026: 11 public vibe-coded apps audited across all 12 pillars, plus a held-out set of 10 other third-party apps audited blind. They are a selected set of audited apps, not a random sample and not a leak rate for AI-built apps in general.
Keeping keys out of the repository in the first place is the wider job of secrets management; this page is about finding the ones that got in anyway, and closing each one.
A secret in code is any value that grants access to something: an API key, a database URL with the password inside it, a webhook signing secret, a private key, an OAuth client secret, a session secret. The three detection methods catch different ones.
| Detection method | What it catches | What it misses (my reading) |
|---|---|---|
| Provider patterns: one rule per known key format (GitHub’s partner and provider patterns, a Gitleaks rule) | Keys with a recognizable shape; GitHub’s supported list includes Stripe, Anthropic and OpenAI API keys | A plain password or a homemade token with no fixed format |
| Entropy: a score for how random a string looks (a Gitleaks rule can set a minimum Shannon entropy; detect-secrets has an entropy detector) | Random-looking strings with no known prefix | Short, low-randomness secrets; it also flags hashes and IDs that are not secrets |
| Live verification: the scanner asks the provider whether the key works (TruffleHog’s “verified” result, GitHub’s validity checks) | Which hits are still live today | Key types it has no check for; a failed check does not prove a key is dead |
Where the scan runs decides what it can see. GitHub’s secret scanning documentation says it covers “your entire Git history on all branches of your repository,” plus issues, pull requests, Discussions, wikis and secret gists. A scanner on your laptop covers whatever clone you give it, so a shallow or single-branch clone hides commits from it.
Some places no repository scan reaches. To find secrets there, my list is short: the hosting dashboard’s environment settings and build logs, team chat, screenshots pasted into tickets, and the old copy of the project on a former contractor’s laptop.
Credential scanning versus a credentialed scan: two different jobs
Credential scanning means looking for credentials in code, the same job as secrets scanning. A credentialed scan is a different thing: a vulnerability scan that logs in to the target with an account, so it can do what a local user of that account can do.
A credential scanner, then, is a secret scanner under another name, and everything else on this page applies to it. Credentialed vulnerability scanning belongs to network and server security. Tenable, which makes the Nessus vulnerability scanner, puts it this way in its documentation: “Credentialed scans can perform any operation that a local user can perform,” and the depth “depends on the privileges granted to the user account.”
The difference between a credentialed and a non-credentialed scan is the login. A non-credentialed scan, also written uncredentialed, runs with no account, and Tenable says configuring credentials lets its scanner “perform a wider variety of checks that result in more accurate scan results.” For a web app the nearest equivalent is authenticated scanning, which vibe coding security scanners compared covers.
What goes wrong without it
The three secrets behind the opening count, from the 21 third-party apps I audited, were a webhook signing secret, a live AI-provider key, and a Stripe test key with its webhook secret. Across the same 21 third-party apps, 6 shipped a real secret, while Secrets and Credentials was still the highest-scoring pillar on average at 84 out of 100: most vibe-coded apps do keep keys in env vars. I picked those apps to audit, so read the counts as what that group showed, not as a rate for every AI-built app.
My reading of how this happens: a secret committed to git history can date from one early commit, made before the project had a .env file. In one app I audited, an early version wrote the live AI-provider key into its own config file; the project later moved to a .env file and deleted the config, but a full-history scan still recovered the key from past commits, as told in why moving off Lovable Cloud forces a security recheck. The report’s fix was to rotate the key; deleting the file is not a fix.
| What leaked | Where it sat | What a stranger with a clone can do with it (my reading) |
|---|---|---|
| A webhook signing secret | An earlier commit in git history | Sign fake webhook calls that the app accepts as genuine |
| A live AI-provider key | An early config file, still in past commits | Run model requests billed to the owner’s account |
| A Stripe test-mode API key, with the webhook secret beside it | An earlier commit in git history | Read and create test-mode data, and sign fake test webhooks |
An AI API key on GitHub gets outside help only in a public repository. GitHub’s partner program runs by default on public repositories and sends a matching key straight to its provider; GitHub’s supported secret patterns list Anthropic, OpenAI and Stripe API keys as partner patterns. For a Claude API key found in a public GitHub repository, Anthropic’s API key guidance says GitHub notifies Anthropic and “Anthropic automatically deactivates the exposed API key.” Other partners get the same report, and GitHub’s remediation guide says the provider “may immediately revoke the secret.” In a private repository that default does not run, so the key can keep working until someone rotates it.
If what you committed was a whole .env file, the rotate-first order and the rollout are covered in if you committed .env to GitHub. Keys that reach the browser by design are a different problem, covered in API keys exposed on the frontend.
How to do it: the history, GitHub’s scanner, the tools and the clean-up
Scan repo for leaked credentials: the first full-history run
Scanning a repo for leaked credentials takes 6 steps: clone with full history, scan the history and every branch, save the dated report, classify each hit, check build and CI logs, and decide whether each finding is live, dead or false.
To scan a Git repository for secrets properly, the scanner has to see every commit on every branch. A normal clone creates remote-tracking branches for each branch; a --depth clone truncates the history and implies a single branch.
- 01 Clone the repository with a plain git clone, never with --depth
- 02 Run one scanner over every commit on every branch, not only over the current files
- 03 Save the report as a file with the date in its name, and keep the command that produced it
- 04 For each hit, record the secret type, the file, the first commit and whether the key still works
- 05 Check what git never held: the hosting dashboard build logs, the CI logs and the issue tracker
- 06 Decide each hit: real and live, real and already dead, or a false positive
With Gitleaks, the first two steps and the report look like this:
git clone https://github.com/<owner>/<repo>.git && cd <repo>
gitleaks git --log-opts="--all" --report-format json --report-path "../gitleaks-$(date +%F).json" .
Gitleaks reads history through git log -p, and --all makes that log take every ref, remote branches included. Each finding in the report carries its rule, file, line, commit and a fingerprint, and also the secret itself, which is why the command writes the report outside the repository. It exits with code 1 on “leaks or error encountered,” so the same command can fail a CI job.
The names git scanner and GitHub scanner also get used for malware and code-quality scanners; for keys, the tool you want is a secret scanner.
The GitHub secret scanner and push protection
GitHub secret scanning runs free on public repositories and needs GitHub Secret Protection on organization-owned private ones, and so does push protection. Push protection is the part I turn on first: it blocks pushes that contain secrets before they reach the repository, so it stops the next leak instead of reporting it.
On GitHub, secret scanning does two jobs: it raises alerts on the repository, and in public repositories it reports partner-pattern keys straight to the provider. Public repositories get it free. Organization-owned private and internal repositories need GitHub Secret Protection enabled on GitHub Team or GitHub Enterprise Cloud, and a private repository under a personal account gets it only on GitHub Enterprise Cloud with Enterprise Managed Users or on GitHub Enterprise Server with Secret Protection. GitHub secret scanning pricing is per active committer; on 2026-09-28, GitHub’s security plans and pricing page listed Secret Protection at $19 USD per active committer per month.
GitHub’s push protection comes in two forms. The one for users is on by default and stops you from pushing secrets to public repositories. The one for a repository “Requires GitHub Secret Protection to be enabled,” is off until an admin enables it, and by default lets anyone with write access bypass a block by giving a reason. GitHub’s availability table shows secret scanning and push protection on a public repository without Secret Protection, and neither on a private one without it.
My working rule is to turn on push protection before anything else, ahead of the scan itself. Turning it on for a repository takes four clicks: Settings, then Advanced Security, then Enable beside Secret Protection if it is not on yet, then Enable beside Push protection. On a public repository that costs nothing. On a private one it comes only with Secret Protection, and in GitHub’s words, “You must be on a GitHub Team or GitHub Enterprise plan in order to purchase GitHub Code Security or GitHub Secret Protection.” Without it, a pre-commit hook does the same job on each laptop: git-secrets installs one, Gitleaks ships one, and GitHub’s own guide suggests asking each collaborator to set up the hook you choose.
Secret Protection also adds custom patterns, your own regular expressions for keys no provider pattern covers, and validity checks, which help you prioritize by “verifying whether a detected secret is still active.” The GitHub side stops at GitHub: a copy of the code on another host, a branch nobody pushed, or a clone on a laptop is outside its view.
Which secret scanner to run, and what each one misses
Gitleaks and TruffleHog are the two open-source scanners I would start with. Gitleaks matches rules across git history and suits CI. TruffleHog can also check whether a found key is still live. git-secrets is built to block new commits, and detect-secrets alerts only on secrets missing from its baseline.
| Scanner | Maintainer | License | Scans history | Verifies keys live | Built for |
|---|---|---|---|---|---|
| Gitleaks | The Gitleaks project; its README says it is feature complete, with security patches only | MIT | Yes: gitleaks git reads git log -p (additions only) | Not stated in Gitleaks’s docs | Rule scans of git repos, directories, files and stdin; a pre-commit hook and a GitHub Action |
| TruffleHog | Truffle Security | AGPL-3.0 (open-source edition) | Yes, and also GitHub, GitLab, Docker, S3, filesystems and more | Yes: “verified” means confirmed valid and active by API testing | Finding and classifying credentials, over 800 secret types, across git and other sources |
| git-secrets | AWS Labs | Apache-2.0 | Yes: git secrets --scan-history reads all revisions | Not stated in git-secrets’s docs | Git hooks that reject commits matching prohibited patterns; AWS patterns via --register-aws |
| detect-secrets | Yelp | Apache-2.0 | No: built to avoid “digging through all git history” | Partly: secrets found by its regex-based rules “can optionally be verified” by a network call; --no-verify turns it off, and --only-verified flags only secrets that can be verified | A committed baseline, so only secrets missing from it alert; audit to label baseline entries |
| GitGuardian | GitGuardian, a commercial service | Commercial; a free Starter plan for up to 25 developers | On the free plan, up to 500 historical scan detections | On paid plans only: its pricing table lists validity and presence checks, but not on the free Starter plan | Hosted internal secrets monitoring, with the ggshield CLI |
Gitleaks is a rule scanner: each rule is a regular expression, optionally with a minimum entropy, and each finding comes back with a fingerprint you can use later to ignore it. Its README on GitHub now warns that Gitleaks is “feature complete” and that “Future releases will be security patches only,” so do not expect new features from it.
TruffleHog, from Truffle Security, is a secret scanner built around verification: for every secret it can classify, “it can also log in to confirm if that secret is live or not.” A TruffleHog scan of a local clone runs from the folder above it: trufflehog git file://your-repo. Its --results flag “Defaults to verified,unverified,unknown”; the README’s own local-repo example narrows it to verified,unknown, which hides the unverified hits a history audit still has to classify. Its “unverified” label means “detected but not confirmed valid (may be invalid, expired, or verification disabled),” which is why a failed check proves nothing on its own. The TruffleHog tool on GitHub is the AGPL-3.0 edition; TruffleHog Enterprise is the commercial product, pitched for “continuously monitoring Git, Jira, Slack, Confluence, Microsoft Teams, Sharepoint (and more) for credentials.”
git-secrets, from AWS Labs, installs three hooks (pre-commit, commit-msg and prepare-commit-msg) and rejects a commit that matches a prohibited pattern. A git-secrets scan with git secrets --scan and no file names covers the files git ls-files returns; git secrets --scan-history is the one that reads every revision.
detect-secrets, from Yelp, works the other way round: you commit a .secrets.baseline of what is already there, detect-secrets-hook alerts only on secrets missing from it, and detect-secrets audit lets you label each baseline entry. That makes it good at stopping new leaks and the wrong tool for the history question this page starts with.
Whether GitGuardian is legit comes down to what you can check: it is a commercial service, its pricing page lists a $0 Starter plan with “Unlimited real-time scanning” for teams of up to 25 developers, and its Business and Enterprise plans carry no listed price.
Gitleaks vs TruffleHog, as I read them: Gitleaks for a fast rule scan in CI, TruffleHog when you need to know which hits are still live. One scanner over the full history beats three secret detection tools run over the working tree. Four of the five secret scanning tools in the table are open source; GitGuardian is the hosted, commercial one. The wider scanner classes, from static analysis to dependency checks, are sorted in what SAST, DAST, SCA and secret scanners leave unresolved.
gitleaksignore and TruffleHog ignore: silencing a false positive safely
A .gitleaksignore file lists the fingerprints of reviewed findings, so a false positive stops alerting while the rule stays on. Ignore by fingerprint or by line, never by rule or folder, and record the reason.
Besides config allowlists and a baseline report, Gitleaks offers two ways to ignore a single finding: the .gitleaksignore file at the repository root, one fingerprint per finding, and an inline gitleaks:allow comment on a single line of code. The README calls the ignore file “experimental and is subject to change in the future,” so check it still works after an upgrade.
TruffleHog’s equivalent is a trufflehog:ignore comment on the line that holds the value. Running with --no-ignore-tag reports those lines again, which is how you review past decisions.
My rule for both tools: ignore one finding at a time, and write the reason in the commit message that adds the ignore. A test fixture that looks like a key should hold an obviously fake value instead, so nothing needs ignoring. A rotated key left in history is ignored the same way, with its rotation date as the reason.
How to remove a key from git history, and why rotation comes first
Removing a key from git history comes second. The first step is rotating it, because a rewrite alone does not reach existing clones, forks or GitHub’s cached views. After the old key is revoked and proven dead, git filter-repo can rewrite history, and every collaborator re-clones or rebases.
GitHub’s guide to removing sensitive data is plain about the order: for a secret, “as a first step you need to revoke and/or rotate that secret,” and after that a rewrite “may not be warranted.” The same guide lists where old commits stay reachable after a rewrite and force-push: clones and forks, their hashes in GitHub’s cached views, and pull requests that reference them.
The rotation itself is its own job. Rotating without taking the app down is covered in how to rotate API keys safely, and the .env rollout in the environment variables article linked above. The five steps below start once the old key is replaced.
- 01 Confirm the old key is revoked and proven dead, with the dead-key test in the next section
- 02 Decide whether a rewrite is worth it: a private repository with a dead key loses little by keeping its history
- 03 If the repository is or will be shared, rewrite it with git filter-repo using --sensitive-data-removal and --replace-text
- 04 Force-push, then have every collaborator re-clone, or rebase (never merge) branches made from the old history
- 05 Ask GitHub Support to remove cached views and pull request references only where rotation cannot remove the risk
Step 2 is a judgment call, and mine leans toward skipping the rewrite on a private repository: rewriting changes the hash of every commit from the leak onward, and GitHub recommends merging or closing open pull requests first. For step 3, the command in GitHub’s guide is git-filter-repo --sensitive-data-removal --replace-text ../passwords.txt, where the file lists the text to replace, and it needs git filter-repo “at least version 2.47.” For step 5, GitHub Support helps only “in cases where we determine that the risk can’t be mitigated by rotating affected credentials,” so for a key already rotated and proven dead, my reading is that the step usually does not apply.
A rewrite never pulls a key back from a clone someone already made. If the key is still live and you are in the first hour, start with what to do when an API key leaked.
How to verify it
A secret scan is verified by 5 records: the dated full-history report, a row per real finding, the provider’s refusal of the old key, a working call with the new key, and a clean re-scan with reasons for anything ignored.
- 01 The dated scan report covers full history and all branches; evidence: the report file and the command that made it
- 02 Each real finding has a row with secret type, first commit, rotated on and old key tested on; evidence: that table
- 03 The provider refuses the old key after its expiry; evidence: the status code, the response body and the date
- 04 The replacement works in production; evidence: one real call that succeeds, with its response and time
- 05 A re-scan shows only ignored fingerprints with written reasons, and push protection or a pre-commit hook is on; evidence: the second report and the setting
The dead-key test is one call with the old key. Stripe makes a clean worked example: its keys guide says a request with an expired key gets an authentication error, and Stripe’s API error reference lists 401 as “Unauthorized” with “No valid API key provided.”
curl -s -o dead-key-response.json -w "%{http_code}\n" \
-H "Authorization: Bearer <old key>" https://api.stripe.com/v1/balance
The -o flag saves the response body as your evidence, and -w prints the status code; a 200 means the old key still works. Timing matters: when you rotate a key in the Stripe Dashboard, “both the old and new keys work for up to 7 days” unless you pick Now as the expiration, so a test inside that window sees the old key succeed. A verifying scanner marking the old key unverified is a second signal, never the only one, for the reason given in the scanner section.
Check 5 closes the loop. When you skipped the rewrite, the rotated key’s fingerprint is one of the ignored entries, and its reason is the rotation date plus the dead-key test.
In the Production Hardening Sprint, deliverable 2.2 is verified this way: we retain scan results and confirm exposed credentials have been invalidated and replacements work.
Where the sprint does this
We scan Git history for committed secrets and rotate every exposed credential found; that is deliverable 2.2. The result goes into the production readiness report, deliverable 13.1, which accounts for all 123 IDs, keeps failures visible until resolved and explains genuine non-applicable items. Hosting, paid tools and API usage remain in your accounts. Both deliverables are listed in the published scope.
Common questions about scanning a repo for secrets
How much does GitHub secret scanning cost?
Nothing on a public repository. On an organization-owned private or internal repository it needs GitHub Secret Protection on GitHub Team or GitHub Enterprise Cloud, which GitHub’s security plans page listed at $19 USD per active committer per month on 2026-09-28. That page carries the current figure.
Is TruffleHog free?
The open-source edition is: TruffleHog’s GitHub repository publishes it under the AGPL-3.0 license. TruffleHog Enterprise, the continuous-monitoring product, is Truffle Security’s commercial offering, and the README sends you to Truffle Security’s site for it.
Is gitleaks free?
Yes. Gitleaks is open source under the MIT license and runs on your machine, as a pre-commit hook or in CI through its GitHub Action.
Is GitHub private actually private?
For access, yes: GitHub says private repositories “are only accessible to you, people you explicitly share access with, and, for organization repositories, certain organization members.” In my reading that still includes every collaborator, every app you granted access and every clone already made, so a key in a private repository is still a key to rotate. Making a public repository private does not help either: its current forks stay public.
Can you remove something from git history?
Yes, with a history rewrite such as git filter-repo followed by a force-push. GitHub’s guide warns that a rewrite does not reach existing clones or forks, and that cached views need GitHub Support; for a leaked key, rotation is what matters, because a rotated key “can no longer be used for access.”
The checks in this guide show you where the app is open. The sprint below closes those gaps, tests the result and writes the evidence down.
Built it with AI. Now it has to hold up for real customers.
The Production Hardening Sprint takes the app you already have and builds the production foundation underneath it. Authentication and access rules, payments that stay consistent, error handling, monitoring, backups, automated tests and a documented handover. Our engineers work inside your existing codebase for ten working days. All 123 deliverables are included, and you get the evidence for each one.
See the Production Hardening Sprint →
$2,500 fixed price · 10 working days · One codebase