You committed an API key by accident, noticed a day later, deleted it, and pushed a clean commit. The file looks safe now, so it feels handled. It is not, because secrets in git history live in every past version of the file, and git keeps all of them forever.
Why secrets in git history survive a delete
Git is not a folder that holds the current state of your code. It is a chain of snapshots. Every commit records the full content of the files as they were at that moment, and it points back to the commit before it. When you delete a key and commit again, you add a new snapshot on top. The old snapshot still sits in the chain, key and all.
Think of it like a filing cabinet where you never throw anything out. You can slide a new page over an old one, but the old page is still in the drawer. Anyone who opens the drawer to an earlier date reads the original.
Here is the shape of the problem. Say you commit a config file with a live key in it:
git add config.py
git commit -m "add payment client"
The file at that point contains:
STRIPE_KEY = "sk_live_9fK2mQ7bTz4pR8xLwV0nHc…"
A day later you catch it, move the key to an environment variable, and commit the fix:
git rm --cached config.py
git commit -m "remove hardcoded key, load from env"
Your working copy is clean. But the key is one command away for anyone with the repo:
git log -p -- config.py
git show HEAD~1:config.py
That git show prints the old file exactly as it was, with sk_live_9fK2mQ7bTz4pR8xLwV0nHc… right there. The lesson is simple. A new commit hides a secret from the present, not from the past.
Copies you do not control
Even if you could scrub your own copy, you rarely have the only copy. A secret pushed to a shared repo spreads faster than most people expect.
- Forks and clones. Every developer who cloned the repo has the full history on their laptop. Every fork on the host has it too. Your delete does not touch any of those.
- CI caches and build logs. Pipelines check out the repo and often cache it. The key can also land in a build log if a script prints the environment. These logs sit around long after the commit is gone.
- Mirrors and backups. Backup jobs, read only mirrors, and archive services keep their own snapshots on their own schedule.
Here is a real sequence. A developer pushes a key at 09:00. A teammate pulls at 09:15 and now has it on disk. The nightly backup runs at 02:00 and stores it. You delete the key at noon the next day. Three copies already exist that your delete never reaches. That is why the fix has to start somewhere other than the repo.
Public repos get read by machines, fast
If the repo is public even for a short window, assume the secret is already collected. People run bots that watch the public commit feed and pull down new commits within seconds. They scan each one for things that look like keys and save the hits. This is not a rare, targeted attack. It is constant background traffic against every public repo.
Picture a repo that goes public for ten minutes during a migration. In that window a scraper clones it, extracts a live cloud access key from a commit made months earlier, and starts using it to spin up servers on your bill. The repo owner never sees a warning. The first sign is the invoice. Speed is the point here: for a public leak, the clock started the moment the commit was reachable, not the moment a human noticed.
Rewriting history does not un leak a secret. The moment it was reachable, treat it as burned and rotate it.
The correct response, in order
The order matters more than the tools. Do these steps in this sequence.
1. Rotate the secret first
Go to the service that issued the key and revoke it, then create a new one. This is the only step that actually stops the leak, because it makes the exposed value useless. Everything after this is cleanup. If you purge history but skip rotation, the old copies on laptops and in caches still hold a working key.
Concretely, if sk_live_9fK2mQ7bTz4pR8xLwV0nHc… leaked, you log into the payment dashboard, roll the key, and update your secret manager with the new one. The instant the old key is revoked, every scraped copy of it turns into dead text.
2. Purge it from history
Now remove the value from the repo history so you are not shipping a revoked but embarrassing key forever. Tools like git filter-repo rewrite every commit that touched the file:
git filter-repo --path config.py --invert-paths
This changes commit hashes, so everyone has to reclone, and you have to force push. Do it after rotation, never instead of it. Rewriting history is housekeeping, not incident response.
3. Prevent the next one
Stop the leak from happening again with two habits.
- Scan before the commit lands. A pre commit hook that checks staged changes for key shaped strings blocks the secret before it ever enters history. That is far cheaper than cleaning up after.
- Keep secrets out of the repo entirely. Use a secret manager or environment variables and load them at run time. A key that is never in a file is a key that can never be committed. This is the same discipline that stops an exposed env file and stops hardcoded API keys in frontend code from shipping to users.
Why a tool finds these better than you do
Reading every version of every file by eye does not scale. But this is a mechanical search, and machines are good at it. A key has structure. Some keys carry a fixed prefix like sk_live_ or AKIA for a cloud access key. Others are just long strings with no repeating pattern, which measures as high entropy, the statistical signature of something random like a token rather than English prose.
So a scanner walks every commit, applies a set of known key formats, and flags any string whose entropy is high enough to look generated. For example, a scan across a year of commits can surface a single AKIA string that was added, then deleted three commits later, in a file no one has opened since. A human skimming the current tree would never see it. The tool sees it because it reads the whole drawer, not just the top page.
This same thinking runs through CI/CD pipeline security, where a leaked secret in a build step can hand an attacker your whole deploy. For a deeper walk through how these leaks work and chain together, our deep dives cover the mechanics.
The one line to remember
A secret committed once is a secret that leaked, full stop. Deleting the file removes it from your view and from nobody else’s. So rotate the key first, purge history second, and put scanning and a secret manager in place so there is no third time. Finding an exposed value across an entire history is exactly the kind of methodical, evidence first work an autonomous security researcher like UnboundCompute is built to do, reading how a system really behaves rather than trusting that a clean working tree means a clean past. Read how we think about it on our about page.
Frequently asked questions
If I delete a key and commit again, is it gone from git?
No. Git stores every commit as a full snapshot, so the version of the file that held the key still sits in the history. Anyone with the repo can read it with a command like git show HEAD~1:config.py. A new commit hides the secret from the current tree, not from the past.
Do I still need to rotate the key if I rewrite the history?
Yes, and you should rotate first. Rewriting history does not un leak a secret that other people already cloned, cached in CI, or backed up. Revoking the exposed key and issuing a new one is the only step that actually makes the leaked value useless. Purging history comes after that.
How fast do public repos get scraped for secrets?
Within seconds. Bots watch the public commit feed, pull down new commits, and scan them for key shaped strings automatically. This is constant background traffic, not a targeted attack, so a repo that is public even for a few minutes should be treated as already read.
How does a scanner find a secret hidden in old commits?
It is a mechanical search. A scanner walks every commit and flags strings that match known key formats, such as an AKIA cloud key prefix, or that measure as high entropy, the statistical signature of a random token. It reads the whole history, so it catches a key that was added and later deleted in a file no one has opened since.
Put an autonomous researcher on your own systems
UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.
