First, the mindset that makes the rest correct: the key is already burned. Not “might be.” The second a secret lands in a public repo, automated scrapers have it, usually within a minute. That single fact reorders everything you are about to do. Your job is not to hide the key. It is too late to hide it. Your job is to make the leaked copy worthless, then clean up, then make sure it does not happen again.
1. Rotate the key. Right now, before anything else.
Go to whatever service issued the key (your database, Stripe, OpenAI, a cloud provider) and revoke or regenerate it. This is the only step that actually protects you, and it is the one people skip because their instinct is to delete the commit first. Deleting the commit feels like fixing it and does almost nothing. Rotating invalidates the leaked copy so that whoever already grabbed it is holding a dead string.
Do this before you touch git. A perfectly scrubbed history around a key that is still live is a false sense of safety. A rotated key in a messy history is genuinely safe.
Want to see exactly what Prbl flags? Watch it scan a demo app, no repo or account needed.
See a live scan →2. Assume it was used, and check.
Once the key is rotated, look at what the old one did while it was exposed. Open the provider's usage, logs, or billing dashboard and scan for activity you do not recognize: requests from unfamiliar regions, a spike in usage, new resources you did not create, charges you cannot explain. For a database key, check for reads or writes you cannot account for. If you find something, that is now an incident to investigate, not just a cleanup, and you treat the data as potentially accessed.
3. Purge it from git history (cleanup, not the fix)
Now remove the value from your history so the next person to read the repo does not find it. Be clear-eyed that this is hygiene: the key is already rotated, so this step is about not leaving a live-looking secret in the record, and about the small set of secrets you cannot rotate. The standard tools are git filter-repo or the BFG Repo-Cleaner:
# remove a file that contained the secret, from all history git filter-repo --path config/keys.ts --invert-paths # then force-push the rewritten history git push origin --force --all
Understand what a force-push does not reach: existing clones, forks, pull-request diffs, and platform caches can retain the old commit. GitHub, for example, may keep a cached view of a force-removed commit for a while. This is exactly why step 1 comes first. History rewriting cleans the room; rotation is what changes the locks.
4. Get the secret out of source for good
Put the new key somewhere the repository cannot see it. Move it into an environment variable, confirm the .env file is listed in .gitignore before you save the new value, and read it at runtime:
// .env (this file must be gitignored) DATABASE_URL="postgres://..." // in code, read it at runtime instead of hardcoding const dbUrl = process.env.DATABASE_URL; // Node const key = import.meta.env.VITE_API_KEY; // Vite / frontend
Two traps to check. First, confirm .env was never already committed; if it was, it needs the same history purge as step 3. Second, remember that anything shipped to the browser is public no matter how you load it, so a frontend key is only safe if the service behind it enforces its own permissions (for a database, that means row level security configured on every table).
5. Put a guard in front of the next one
This will happen again, because the assistant that did it is still writing your code. The fix is not willpower, it is a gate that blocks a hardcoded secret before it can be committed. Install a pre-commit secret scanner so the commit itself fails when a key is present:
# gitleaks as a pre-commit hook blocks secrets before they land brew install gitleaks gitleaks protect --staged --verbose
Then turn on your host's server-side net as a backstop: GitHub offers secret scanning with push protection that rejects a push containing a recognized key, free on public repos. A local hook plus a server-side block means a leaked secret has to beat two gates instead of zero.
Why this keeps happening to AI-built code specifically
We have scanned nearly 2,000 AI-built apps, and a hardcoded secret was the number one high-severity finding in every single corpus, across every tool. The reason is simple and worth internalizing so you stop being surprised by it: a coding assistant optimizes for code that runs. Pasting the key inline makes it run right now. The model is not weighing “this will be in git history forever” against “this works in one line,” because that is a human judgment about consequences the model was never asked to make. So it will reach for the inline key every time the surrounding context makes it the shortest path, and the guardrail has to live in your workflow, not in your hope that it won't.
Frequently asked questions
I deleted the commit. Is the key still dangerous?
Yes. The moment a secret hits a public repository it should be treated as compromised, because automated bots scrape new commits within seconds and the value is copied before you can react. Deleting the commit removes it from the current tree, not from every clone, fork, cache, and bot that already read it. Rotation is the only step that actually revokes access.
The repo is private. Do I still need to rotate?
Almost always yes. A private repo still exposes the key to every collaborator, every CI log, every fork, and anyone who later gains read access or clones it. Private is a smaller blast radius, not a safe one. If the key protects anything that costs money or touches user data, rotate it.
How fast do bots actually find leaked keys?
Fast. Security researchers who have planted canary tokens in public commits routinely see the first unauthorized use within minutes, sometimes seconds. Assume any key pushed to a public repo has already been read by someone other than you.
How do I stop Cursor or Copilot from doing this again?
Keep secrets out of the model's reach in the first place: store them in a .env file that is gitignored, read them through process.env or import.meta.env, and install a pre-commit secret scanner so a hardcoded key is blocked before it can ever be committed. The AI will still occasionally paste one in; the guard is what stops it from shipping.
The 60-second version
- Rotate the key first. It is already compromised; a new key is the only real fix.
- Check the provider's logs for use you do not recognize.
- Purge it from git history with filter-repo or BFG, knowing forks and caches persist.
- Move it to an environment variable and confirm
.envis gitignored. - Install a pre-commit secret scanner so the next one cannot ship.
If you want to know whether there are other secrets already sitting in a repo you shipped, that is exactly what a scan is for. It reads the code the way the bots do and hands you the file and line, so you can rotate what you find before someone else does.