Keys and incidents
How to rotate a leaked API key and clean your git history
If an API key has been committed to a repo, rotate it at the provider first: create a new key, switch your app to it, and revoke the old one. Deleting the key from your code doesn’t help on its own, because every earlier commit still contains it; rewriting git history is optional clean-up that only makes sense after the key is dead.
TL;DR
- Rotation is the fix. Any key that was ever in a public repo must be treated as copied.
- Order: revoke or roll the key → put the new one in a server-side secret → remove it from code → optionally rewrite history.
- Find every key, including deleted ones, with
git log -pover all branches. git filter-reporemoves files or strings from history; everyone must re-clone afterwards, and forks keep the old commits.
Why deleting the key isn’t enough
Git keeps every version of every file. If .env was committed last month and deleted last week, git show <old commit>:.env still prints it, for anyone who can read the repo. For a public repo that means anyone, and automated scanners watch public pushes for key patterns, so assume a pushed key is found quickly. Forks, clones and caches keep the old commits even if you rewrite yours.
So the only thing that makes a leaked key harmless is making it stop working.
Step 0: find every key, including deleted ones
# Key-shaped strings ever added or removed, on every branch
git log --all -p -G 'sk-proj-|sk-ant-|sk_live_|rk_live_|service_role|sb_secret_|AIza' \
--format='%h %ad %s' --date=short \
| grep -E '^[0-9a-f]{7,} |^[+-].*(sk-|sk_live_|rk_live_|service_role|sb_secret_|AIza)' | head -80
# Was an env file ever committed?
git log --all --format='%h %ad %s' --date=short -- '.env' '*.env' '.env.*'Make a list: which key, which provider, which commit. Ignore publishable keys meant for browsers (Supabase anon or publishable, Stripe pk_…, a Firebase web config) and test keys (sk_test_…), but do rotate anything secret even if it was “only there for a day”.
Step 1: rotate at the provider
| Key | Where to rotate it |
|---|---|
| Supabase service role / secret key | Project settings, API keys. With the newer secret keys (sb_secret_…) you can create a new one and delete the old. Legacy JWT keys are rotated by rotating the project’s JWT secret, which also invalidates the anon key and signs users out: plan for that. |
| OpenAI, Anthropic and other AI keys | The provider’s API keys page: create a new key, update your app, delete the old key. Check usage for spend you don’t recognise. |
| Stripe secret or restricted key | Developers, API keys: roll the key. Stripe lets you choose when the old one expires, so you can deploy the new one first. |
| Database connection string | Change the database password (for Supabase, in the database settings), then update every server that uses it. |
| GitHub tokens, cloud keys | Revoke the token or key in the provider’s settings and issue a new one with the narrowest scope that works. |
Do this even before you’re sure the key was used. Then look at the provider’s logs or usage for the period the key was exposed, and note what you find; you may need it (see the first hour after a leak).
Step 2: put the new key where it belongs
Secret keys belong on a server, never in frontend code or in a VITE_ or NEXT_PUBLIC_ variable (the build copies those into the browser bundle). In a Supabase app, that usually means an Edge Function:
supabase secrets set OPENAI_API_KEY=sk-proj-new...import OpenAI from 'npm:openai@4';
const openai = new OpenAI({ apiKey: Deno.env.get('OPENAI_API_KEY')! });
// ...verify the caller first, then call the API on their behalfThe frontend then calls supabase.functions.invoke('chat', …) instead of the AI API. Securing Edge Functions covers checking the caller.
Step 3: remove it from the code and stop it happening again
git rm --cached .env # stop tracking it, keep your local copy
printf '.env\n.env.*\n!.env.example\n' >> .gitignore
git add .gitignore && git commit -m "Stop tracking env files"Keep a .env.example with placeholder values so others know which variables exist. GitHub’s push protection and secret scanning, where available on your plan, will block or flag many key formats on future pushes.
Step 4 (optional): rewrite history
Once the key is revoked, rewriting history is tidiness, not security. Do it if you want the old commits gone from your repo, for instance before making a private repo public. It rewrites every later commit id, so coordinate with anyone else who works on the repo.
git filter-repo is the tool the Git project recommends over the older filter-branch. Install it (brew install git-filter-repo or pip install git-filter-repo), then work on a fresh clone:
git clone --mirror git@github.com:you/your-app.git
cd your-app.git
# Remove a file from every commit
git filter-repo --invert-paths --path .env
# Or replace a key string wherever it appears
printf 'sk-proj-OLDKEY==>REMOVED\n' > ../replacements.txt
git filter-repo --replace-text ../replacements.txt
# filter-repo removes the origin remote as a safety step; add it back to push
git remote add origin git@github.com:you/your-app.git
git push --force --mirror originAfterwards:
- Everyone with a clone must re-clone (or carefully rebase onto the new history). An old clone pushed back will reintroduce the commits.
- Forks and existing clones elsewhere still have the old commits; you can’t rewrite those. This is why Step 1 comes first.
- GitHub may keep old commits reachable through cached views or pull request refs for a while. GitHub’s guide on removing sensitive data explains how to ask support to purge them.
Checklist
- Every secret key ever committed is listed.
- Each one is rotated and the old one revoked.
- Provider usage and logs checked for the exposure window.
- New keys live in server-side secrets only.
- Env files untracked and ignored;
.env.examplecommitted. - History rewritten, if you chose to, with collaborators re-cloned.
Sources
- GitHub: removing sensitive data from a repository
- git-filter-repo
- Supabase: API keys
- Stripe: API keys
Related findings
The checks behind this guide, each with example code, a self-check and the fix.
AI, payment or database key in the code
PL-010Service role key in client code
PL-011Key still in git history
Free check
Check your app for this, free.
Paste your public repo. PatchLabs rebuilds your database from its migrations, tests it as a visitor, a stranger and the owner, reads the code for keys and risky patterns, and hands you the fix.
Written by Manraj Singh Dhillon. Last checked 10 Nov 2026. Code samples are synthetic and run against your own project; nothing here names a real app. Spotted a mistake? Tell us.