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 -p over all branches.
  • git filter-repo removes 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

find-keys.sh
# 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

KeyWhere to rotate it
Supabase service role / secret keyProject 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 keysThe 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 keyDevelopers, API keys: roll the key. Stripe lets you choose when the old one expires, so you can deploy the new one first.
Database connection stringChange the database password (for Supabase, in the database settings), then update every server that uses it.
GitHub tokens, cloud keysRevoke 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:

terminal
supabase secrets set OPENAI_API_KEY=sk-proj-new...
supabase/functions/chat/index.ts
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 behalf

The 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

terminal
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:

terminal
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 origin

Afterwards:

  • 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.example committed.
  • History rewritten, if you chose to, with collaborators re-cloned.

Sources

The checks behind this guide, each with example code, a self-check and the fix.

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.

Free for your first appNever touches your database

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.