You leaked an API key. Here's the damage control playbook
Bots find it in minutes, and deleting the commit doesn't save you. What to revoke first, in what order, and how to make sure it never happens again.
If you pushed an API key to GitHub, the only action that actually neutralises it is revoking it in the provider's console. Deleting the commit, force-pushing, or flipping the repo to private does nothing — the key is out, and from that moment it is burned. Go rotate it now, then come back for the rest of the order of operations. When you do, sweep the rest of the project with Docuboxer's local secret scanner, which matches credential patterns without the text ever leaving your browser.
Why deleting the commit does not save you
This is the part most people learn the hard way. Git does not store "files with history" — it stores immutable objects addressed by hash. When you rewrite history with git rebase, git filter-repo or BFG, you change the references. The object containing your key still exists on the remote until garbage collection runs, and on a hosted remote that is not on your schedule.
Three more holes stack on top of that:
- GitHub's cache. A commit stays reachable by its SHA through the web interface and the API even after no branch points at it. That is documented behaviour, not a bug — GitHub's own guidance is to treat the secret as compromised and rotate it.
- Forks and clones. Anyone who forked or cloned the repository holds a complete copy of the original history. Your force push never reaches them.
- Mirrors and archives. Code search indexes, CI artefacts, analysis services and bots that archive the public events firehose all had the opportunity to copy the commit before you touched it.
And the clock started at the first second. Public repositories surface in GitHub's public event stream almost in real time, and automated scrapers exist for exactly this: read the stream, pull out anything shaped like a credential, test it against the matching API. So the correct response is not "delete it fast before anyone sees" — it is "assume it has already been seen".
The right order: four steps, in this sequence
1. Revoke or rotate the credential. Right now.
Open the provider console — AWS IAM, GitHub, Stripe, OpenAI, SendGrid, whichever — and delete or disable that specific key. If the service supports overlapping rotation, use it: create the new key, deploy it, then kill the old one. If it does not, revoke first and fix the broken deployment afterwards. A few minutes of downtime costs far less than someone else's compute bill on your account.
One special case: cloud service credentials often carry broad permissions. If the leaked pair could touch IAM, revoking it is not enough — check that no extra user, role or access key was created while it was exposed, because that is how a short leak turns into persistent access.
2. Read the usage logs before you call it closed
Revocation stops the future; it tells you nothing about the past. Pull up the credential's usage history and look for activity you cannot account for: request spikes, IPs or regions outside your operation, resources created, emails sent, unexpected charges. On AI and email providers the classic abuse is a burst of consumption within hours; on cloud accounts it is instances spun up for mining.
If you find third-party usage, this is no longer a scare — it is an incident. Write down the exposure window (commit timestamp to revocation timestamp), tell whoever owns security at your company, and check whether the key could reach personal data, because that may trigger breach notification duties.
3. Put the replacement where it belongs
The new key does not go back into the code. It goes into an environment variable read at runtime, and in production into your platform's secret manager — Vercel, GitHub Actions secrets, AWS Secrets Manager, Doppler. Locally, a .env file that was added to .gitignore before you created it.
Two honest caveats about environment variables. They are not encryption: anyone with access to the process or the host can read them. And they leak into logs with remarkable ease, usually when someone prints the whole config object while debugging. Also watch your framework's client-side prefix — anything named NEXT_PUBLIC_ or equivalent is public by design and ships inside the bundle the browser downloads.
4. Find the others
A leaked key rarely travels alone. Sweep the whole repository, not just the file from the incident: source, config files, notebooks, test fixtures, deploy scripts, screenshots in the README, logs pasted into issues. Drop the suspicious content — or your entire .env — into the secret scanner and work through the findings: it recognises known provider patterns as well as generic high-entropy strings, and masks part of each value in the results.
For history, a quick manual pass is git log -p | grep -iE "api[_-]?key|secret|token"; for anything serious, use a dedicated history scanner. And if you are going to clean the history anyway, do it after revoking, never instead of it.
Making sure it does not happen again
- A pre-commit hook that scans. This is the only barrier that acts before the secret exists in history at all. Tools like gitleaks or git-secrets hook the commit and block it.
.gitignorefrom day one. Add.env,.env.local,*.pemand your stack's credential files when you create the project, not after the commit has landed.- A
.env.examplewith empty values. Document which variables are required without carrying a single real one. - Platform secret scanning. GitHub detects partner-provider credential patterns in public repositories and alerts; several providers auto-revoke on notification. A genuinely useful safety net — but it fires after the push, so it does not replace the local hook.
- Least-privilege, short-lived keys. A key scoped to one bucket that expires in 90 days turns a leak into a footnote instead of an outage.
What a secret scanner cannot tell you
Worth being blunt about the limits, because false confidence is expensive here. Pattern-based scanning finds things that look like known credentials, which means false positives (a random string in a test fixture flagged as a possible secret) and false negatives (an internal token with a custom format, or a key split across lines, sailing straight through). A clean result means "no known patterns matched", not "this file is safe".
It also cannot tell you whether the key was ever used — only the provider's logs can. And it inspects the text you give it, not your git history or your remote. Think of it as a fast, private magnifying glass for step 4, not as a security audit.
The first-hour summary
Revoke the credential at the provider. Read the usage logs for signs of abuse. Issue a replacement and store it in environment variables or a secret manager. Scan the rest of the repository for anything else that got committed. Install a pre-commit hook so it cannot repeat. And any time you find yourself weighing "delete the commit" against "rotate the key", the answer is rotate the key — the commit is one copy among many, but the credential is the one thing you can genuinely switch off.
Frequently asked questions
Does deleting the commit remove the leaked key?
No. Rewriting history removes the reference, not the data. The object still exists on the remote until garbage collection runs, it stays reachable by its SHA through GitHub's web UI and API, and it remains intact in every fork, clone and open pull request. History rewriting is cleanup you do after revoking, never instead of revoking.
How quickly do bots find secrets pushed to GitHub?
Assume minutes, not days. Public repositories show up in GitHub's public events stream almost immediately, and automated scrapers read that stream, extract credential-shaped strings and test them against the matching API. The realistic planning assumption is that a public key was seen and tried before you noticed the mistake.
What if the key was only in a private repository?
Lower urgency, not zero risk. A secret in a private repo is exposed to everyone with repo access, to CI tokens that clone it, to build logs, and to every laptop holding a clone. If you cannot rotate it today, at minimum move it out of the code and schedule the rotation.
Do I need to rewrite git history at all?
It is good hygiene, not a fix. Clean the history with git filter-repo or BFG so the secret does not keep resurfacing in code search and future clones, but do it only after the credential is dead. Rewriting also forces everyone else to re-clone, so coordinate it with your team.
Will GitHub push protection stop this from happening again?
It catches a lot, but only patterns from partner providers that it recognises, and it acts at push time. Internal or custom-format credentials slip through. A local pre-commit scanner plus a correct .gitignore is the layer that stops the secret from entering history in the first place.
Is it safe to paste a .env file into an online scanner?
With most of them, no — you would be repeating the exact mistake you are fixing, by sending your secrets to someone else's server. Docuboxer's secret scanner analyses the text entirely in your browser, uploads nothing, and partially masks any values it finds so you can share a screenshot without leaking them again.
Scan your code for exposed secrets
Paste a .env, a log or a config file. Everything is analysed in your browser — nothing is uploaded.
Related tools
- Secret scanner — Finds API keys, tokens and private keys in text, locally.
- Password checker — Real strength plus breach exposure, without the password leaving your browser.
- Hash generator — SHA-256 and friends, for comparing values without exposing them.
- JWT decoder — See what a token actually carries before assuming it is opaque.
You might also like: the privacy risks of pasting JSON into an online formatter and 13 privacy-first developer tools.