← Back to blog

Your Git History Is Still Leaking That Deleted API Key

You committed an API key by accident. You noticed, deleted the line, and committed again. The current code is clean. The key is still there, and anyone who clones your repo can get it in one command.

This post covers why, what to do in the first ten minutes (rotate the key, before anything else), how to search your full history for leaks you have forgotten, and how to rewrite history with git filter-repo when that is worth doing. It also covers what a rewrite cannot reach.

Does deleting a secret remove it from git history?

No. Git records every commit as a permanent snapshot, so a later commit that deletes a key only adds a new snapshot without it. The commit that introduced the key is unchanged and still reachable. Anyone with a clone, or with read access to the remote, can print the old value with a single git log command.

Git stores the full history of every change. "Deleting" a secret in a later commit does not remove the earlier commit that added it. The value sits in the object database, and git log -p or a history-scanning tool will surface it instantly. Prove it with git's pickaxe option, which the git documentation describes as detecting changes in the number of occurrences of a block of text:

git log --all -p -S"PASTE_A_FRAGMENT_OF_THE_KEY"

Here --all walks every branch and tag, -p prints the diff, and -S limits the output to commits that added or removed that string. If the command prints anything, the secret is in your history, whatever the current files look like.

Why this matters more for small projects

Indie repos go public more casually, get shared as portfolio pieces, and rarely have anyone auditing history. A secret that lived in a commit for ten minutes two years ago is still a live credential today if it was never rotated.

GitGuardian's State of Secrets Sprawl 2026 report counted 28.65 million new hardcoded secrets added to public GitHub commits in 2025, a 34% increase on the year before. The more uncomfortable finding is what happens afterwards. When GitGuardian retested a set of credentials it had confirmed valid in 2022, more than 64% of them still worked in January 2026. Leaked keys stay live for years.

Flipping a private repository to public is the classic trigger. The working tree was cleaned up for the occasion. The history was not. For the other ways keys escape, including build output and client bundles, see how indie apps leak their API keys.

The only real fix for a committed secret is to rotate it. History surgery removes the evidence, not the risk.

What should you do first after committing a secret?

Rotate it. Create a replacement credential, deploy it, and revoke the old one before you touch git. GitHub's documentation says the same thing: "as a first step you need to revoke and/or rotate that secret." A dead key is worthless to anyone who copied it, and nothing you do to history achieves that.

Assume the key was compromised the moment it was pushed, because anyone who cloned the repository has it. In order:

  1. Generate a new key in the provider's dashboard.
  2. Update it everywhere the app reads it: your host's environment variables, CI secrets, your local .env file.
  3. Deploy and confirm the app works on the new key.
  4. Revoke or delete the old key. A key that is merely unused is still valid.
  5. Check the provider's usage logs and billing for activity you do not recognise during the exposure window.

Then decide whether to rewrite history at all. GitHub's guidance is that once the secret is revoked "that may be sufficient to solve your problem," and that the extra steps to rewrite history "may not be warranted." Rewriting earns its cost when the committed data cannot be rotated, such as personal data, a customer export, or internal details you would rather not publish.

One caveat: some keys are public by design. A Firebase web config or a Supabase anon key is meant to ship in client code, as covered in whether the Supabase anon key is safe to expose. A service role key, a payment secret key or a service account file is never in that category.

How do you find secrets in your git history?

Search every commit on every branch, not the current files. For a key you already know about, git log --all -p -S finds each commit that added or removed it. For keys you have forgotten, run a dedicated scanner such as gitleaks across the whole repository history, then treat every confirmed hit as a rotation job.

Scan your full git history, not just the working tree, since a key deleted in a later commit is still sitting in an earlier one. To hunt for a pattern instead of an exact string, -G takes a regular expression and matches added or removed lines:

git log --all -p -G"(SECRET|TOKEN|API_KEY)="

That finds what you think to look for. A scanner finds the rest. Gitleaks, an open source tool, describes itself as detecting "secrets like passwords, API keys, and tokens in git repos," and its git subcommand walks the commit history:

gitleaks git -v .

If the repository is public on GitHub, secret scanning already runs on it at no cost. GitHub's documentation says it scans "your entire Git history on all branches" for known credential types. Through its partner program GitHub also reports matches in public repositories to the issuing service, which may revoke the credential. Do not count on that. It covers known token formats, not the random string you chose as a webhook secret.

How do you remove a secret from git history?

Use git filter-repo on a fresh clone, after the secret has been rotated. Either delete the offending file from every commit or replace the secret string wherever it appears, then force-push every branch and tag. The git project recommends filter-repo over the older git filter-branch, and GitHub's own removal guide uses it.

The git documentation for filter-branch now opens with a warning that "its use is not recommended" and points to filter-repo instead. Filter-repo is a single Python script (its README lists git 2.36.0 and Python 3.6 as minimums), and it refuses to run outside a fresh clone unless you force it, so a mistake cannot destroy history that exists nowhere else.

# 1. Work in a fresh clone
git clone https://github.com/YOUR-USERNAME/YOUR-REPOSITORY
cd YOUR-REPOSITORY

# 2a. Remove a file from every commit on every branch
git filter-repo --sensitive-data-removal --invert-paths --path config/secrets.json

# 2b. Or replace a string wherever it appears
git filter-repo --sensitive-data-removal --replace-text ../replacements.txt

# 3. Confirm it is gone
git log --all -p -S"PASTE_A_FRAGMENT_OF_THE_KEY"

# 4. Overwrite the remote
git push --force --mirror origin

The replacements file holds one secret per line and lives outside the repository. Each match is replaced with ***REMOVED*** unless you specify something else. Filter-repo can remove the origin remote as a guard against accidental pushes. If the push says origin does not exist, add it back with git remote add origin and the clone URL. On GitHub the push will fail for refs under refs/pull/, which are read-only.

BFG Repo-Cleaner is the older alternative, and its --replace-text and --delete-files options do the same two jobs on a mirror clone. BFG does not modify your latest commit by default, so the current files have to be clean before you run it.

Rewriting is not free. GitHub's documentation lists the side effects: commit hashes change from the first rewritten commit onward, signatures on commits and tags are lost, diffs on closed pull requests break, and branch protections have to be lifted for the force push. Merge or close open pull requests first.

What rewriting history does not fix

A successful force push cleans one copy. GitHub's documentation is direct about the others: the commits "may still be accessible" in any clones or forks of your repository, directly "via their SHA-1 hashes in cached views on GitHub," and through any pull requests that reference them.

  • Clones. GitHub states plainly that you cannot remove sensitive data from other people's clones. A collaborator who pulls into an old clone and pushes can put the secret straight back, so GitHub tells collaborators to rebase, not merge.
  • Forks. If the commit exists in a fork, it stays there until the fork's owner removes it or deletes the fork.
  • Cached views and pull request refs. These need GitHub Support, which can dereference affected pull requests, run garbage collection on the server and remove cached views.

That last route comes with a condition. GitHub says Support "will only assist in the removal of sensitive data in cases where we determine that the risk can't be mitigated by rotating affected credentials." For a leaked API key, in other words, GitHub's answer is the same as this article's: rotate it.

Independent research backs this up. In July 2024, Joe Leon at Truffle Security showed that commits from deleted forks, and from deleted repositories that had been forked, stay retrievable through GitHub's fork network by anyone with the commit hash. His conclusion: "The only way to securely remediate a leaked key on a public GitHub repository is through key rotation."

So if you must purge history, use a tool built for it, then force-push and rotate anyway.

Catch it before it ships

The cheapest fix is not committing the secret in the first place. Four habits cover most of it, and each one appears in GitHub's own advice on avoiding accidental commits.

  • Ignore the file before it exists. Add .env to .gitignore in your first commit. If the file is already tracked, git rm --cached .env removes it from the index and leaves it on disk.
  • Stage deliberately. Avoid git add . and git commit -a. Add files by name and read git diff --cached before you commit.
  • Scan on commit. Gitleaks ships a pre-commit hook that fails the commit when it detects a hardcoded secret.
  • Leave push protection on. GitHub's push protection for users is enabled by default and stops you from "pushing secrets to public repositories on GitHub." It recognises supported secret types and it can be bypassed, so treat it as a backstop.

This matters more if a coding assistant writes your commits. The same GitGuardian report found Claude Code-assisted commits leaked secrets at a 3.2% rate, against a 1.5% baseline across all public GitHub commits. The matching habits are in the pre-launch security checklist.

The whole response, in order:

  1. Rotate the secret immediately. Assume it was compromised the moment it was pushed.
  2. Revoke the old key and check the provider's logs for use you do not recognise.
  3. Scan your full git history, not just the working tree, for keys and high-entropy strings.
  4. Rotate everything else the scan turns up.
  5. Rewrite history with git filter-repo only if the data cannot be rotated or you need it gone.
  6. If you rewrite: force-push all refs, contact GitHub Support about cached views, and have every collaborator re-clone or rebase.
  7. Add the ignore rule, the pre-commit hook and push protection so it does not happen twice.

Scanning for this before you ship

Scanning both your current source and your git history before a release turns "we found it two years later in a breach report" into "we caught it on Tuesday." For a quick first read of your source, the free IOnclad browser scanner runs a 167-rule subset entirely in your browser, with no signup. Your code stays in the tab.

The IOnclad desktop app runs 24 scanners and 500+ checks across secrets and git history, dependencies, the OWASP web categories, session and auth, an API surface map, and a denial-of-wallet check. It returns one Ship-It verdict with the file, line and a fix for each finding. It does not promise your app is secure, and it cannot tell you whether a key in an old commit has been rotated. Only the provider's dashboard knows that.

Secrets are one line of a longer pre-launch list. The rest is in the fast pre-launch security audit and the app security guides. The first step never changes. Rotate the key, then worry about the history.

Try it free
IOnclad

The pre-launch security scanner that answers "Am I safe to ship?" in under a minute. Runs 100% offline, free to start.

→
Keep reading
IOnclad Hardcoded Secrets: How Indie Apps Leak Their API Keys Read → IOnclad Is the Supabase Anon Key Safe to Expose in Your App? Read → IOnclad Am I Safe to Ship? A 60-Second Pre-Launch Security Audit Read →
Reading us on Google?
Add The IOn Project as a preferred source

One click on Google’s preferences page, and our articles show up more often in your Top Stories, AI Overviews, and AI Mode.

→