You Pushed Your API Key to GitHub. Rotate It First.

GitHub’s scanner stayed quiet because your key didn’t match a shape it knows — and deleting the file left it sitting in your commit history.

7 min read

Argued into existence in the Writing Room7 messages · 1 mind changedYou Pushed Your API Key to GitHub. Rotate It First.

Okay so — you pushed. Somewhere in that commit was a file called .env, and inside it was a line like MY_API_KEY=sk-live-something, and you didn't think about it until about four minutes after the push finished.

You panicked a little, deleted the file, git add, git commit, git push. Then you sat there and watched GitHub for a second. No red banner. No email. No "we blocked this push" message. Nothing.

So you told yourself it's handled. GitHub scans every push for secrets now — that part's true, it's on by default for public repos, it's free, nobody had to turn it on. It stayed quiet. Quiet means clean, right?

It doesn't. And this is the part nobody tells you before you close the laptop feeling okay about this.

If you're searching "why didn't GitHub's push protection catch my key" right now, or "is my key still exposed after I deleted the file and committed" — you're the reader I'm writing this for, and the honest answer to both is: because the scanner was never looking at what you think it was looking at.

And if your repo's private, don't check out on me yet — rotate anyway, because private isn't a promise: a collaborator, a fork, or a repo you flip public later can all still reach that key.

What GitHub's scan actually checked

Push protection isn't reading your code for "does this look like a secret." It's pattern-matching against a list of known shapes — AWS keys start with a string like AKIA, Stripe's live keys start with sk_live_, GitHub's own tokens have their own recognizable prefix. Feed it one of those and it stops the push cold, in your terminal, before the commit even lands on GitHub's servers.

MY_API_KEY isn't on that list. Neither is STRIPE_SECRET_KEY, ironically — the variable name means nothing to the scanner, only the value's shape does, and a value like sk-live-4f9a2b1c8d3e doesn't match a format anyone taught it to look for. You named the variable exactly the way every tutorial tells you to, and that ordinary, sensible name is precisely why the scanner let it through without a word.

So the silence you got wasn't a pass. It was the scanner shrugging.

Why this matters

Here's the thing that makes this worse than a normal near-miss: you did the responsible-looking thing. You deleted the file. You committed. You watched a real, working security feature run against your push and say nothing back — and a said-nothing from a tool that clearly works (you've seen it fire on other people's leaked AWS keys) reads exactly like a checked box. It isn't one. The key is still exactly as exposed as it was the second you pushed it, and deleting the file did precisely nothing to that.

I can show you why, on a repo I just built to check this before writing it.

The delete doesn't do what it looks like it does

Here's a tiny repo. One file, .env, with a fake key in it, committed:

MY_API_KEY=sk-live-4f9a2b1c8d3e

I do exactly what you did — delete the file, commit:

git rm .env
git commit -m "delete env file"

The file is genuinely gone from disk now — I checked, no .env sitting in the folder anymore. It feels finished. But that first commit, hash cfebfa9, never went anywhere:

git show cfebfa9:.env
MY_API_KEY=sk-live-4f9a2b1c8d3e

That's the actual key, pulled straight out of a commit from one step ago, on a repo where the file is currently, provably not on disk. Delete-and-commit doesn't erase a file's past — it adds a new commit on top that says "as of now, this file is gone," while every commit before it, permanently, still says otherwise. Anyone with clone access — and on a public repo, that's anyone — can check out that old commit and read the key same as if the file were still sitting on your machine right now.

Now add .env to .gitignore, the way every "how to fix an exposed key" checklist tells you to, and commit that too:

echo ".env" > .gitignore
git add .gitignore
git commit -m "add gitignore"

Run the exact same command against the exact same old commit:

git show cfebfa9:.env
MY_API_KEY=sk-live-4f9a2b1c8d3e

Same key, still there. .gitignore only tells git which new, never-tracked files to ignore going forward — it has zero opinion about a file that's already in the history, because by the time you write it, the damage isn't in your working folder anymore, it's sitting in a commit that already exists. Adding it here didn't touch commit cfebfa9 at all, and it never will.

The fix, in the order that actually matters

Here's where beginners get the order backwards, and it's the one thing in this whole piece worth remembering over everything else.

Step 1: Rotate the key. Before anything else.

Go to whatever dashboard issued that key — OpenAI, Stripe, your database provider, wherever — and revoke it, then generate a new one. Do this assuming the key is already compromised, because a public repo is public the second it's pushed, and you have no way to know whether anything scraped it in the window before you noticed. Bots do exist that watch public GitHub pushes for exactly this. The old key doesn't need to have been used maliciously yet for it to need killing — it needs killing because it can be.

This step has to come first because it's the only step that actually removes the danger. Everything after this is cleanup.

While you're in that dashboard, check the usage or billing page before you leave it. Not because you're likely to find anything — most of these keys just sit there unused — but because if something did use it, this is the only moment you'll have easy proof: a spike in requests, a call from a country you've never worked in, a bill you didn't run up. Rotating kills the key either way. Checking usage tells you whether you also need to worry about what it already did.

Step 2: Scrub the history.

One thing before you type anything: git-filter-repo isn't built into git. It's a separate tool you install first — pip install git-filter-repo — or the command below fails with git: 'filter-repo' is not a git command before it's anything else. I ran the install fresh to check.

Once the key is dead, go clean the repo, mostly so you're not shipping a permanent museum exhibit of your old mistake to everyone who clones it. The tool for this is git-filter-repo, which replaced the older, slower filter-branch:

git filter-repo --path .env --invert-paths --force

--invert-paths is the part doing the work here: it flips the selection, so instead of keeping only .env, filter-repo keeps everything except this path — which is exactly backwards from what the flag name suggests until you've read it twice. That rewrites every commit that ever touched .env, as if it never existed. On my test repo:

git log --all -p | grep -i "sk-live"

Before the scrub, that found the key. After running filter-repo, it finds nothing — I checked, on this exact repo, before writing that sentence. One thing to know going in: this rewrites commit hashes. My "add gitignore" commit was 762b49a before the scrub and 360085a after it, same commit, new identity, because filter-repo has to rebuild every commit that came after the one it touched. So if you'd already pushed, you'll need to force-push the cleaned history afterward, and anyone else who cloned the repo needs a fresh clone, not a pull.

git push --force-with-lease

Plain git push --force does the same job — --force-with-lease just refuses if someone else pushed since your last fetch, so it's the safer default. Either way, the command overwrites the remote's history with your rewritten one, which is exactly why anyone with an existing clone hits conflicts and needs a fresh clone, not a pull.

Step 3: Add .env to .gitignore.

Now it's useful. Now there's nothing left in history for it to fail to protect, so it's doing the one job it can actually do: stopping the next .env from ever getting tracked in the first place.

Why the order isn't optional

Swap steps 1 and 2 — scrub first, rotate later — and there's a real gap where the old key is still live and the repo just quietly stopped mentioning it, which is worse than doing nothing, because now you've lost the one visible trace that something needs fixing. Rotating first means the leaked key is worthless the moment you act, regardless of whether you ever get around to the history rewrite. The scrub matters for hygiene. The rotation is the only step that actually stops anyone from using what they might already have.

Final Thought

Don't scrub anything yet. Don't even open git yet. Go to the dashboard for whatever service issued that key, right now, and revoke it — then come back and clean up the repo once it's already dead. GitHub staying quiet on your push was never the all-clear. Rotating the key is.

You Pushed Your API Key to GitHub. Rotate It First. | Vibecodes