Committed Your .env File? How to Remove It From Git History and Rotate Your Keys
You open your GitHub repo and there it is: .env, with your Supabase keys, Stripe secret, OpenAI key and database URL in plain text. Maybe Cursor ran git add ., maybe your .gitignore was missing, maybe you exported from Lovable or Bolt and pushed without looking.
You're not alone. GitGuardian's State of Secrets Sprawl 2025 report found 23.8 million new secrets leaked in public GitHub commits in 2024, up 25% on the year before, and found that 70% of secrets leaked in 2022 were still valid when they checked. Leaks happen constantly. What hurts is leaving them live.
Here's the order that actually protects you.
The most important rule: rotate first, clean history second
Deleting .env in a new commit does nothing for security. The file is still in every earlier commit, and anyone can see it with git log -p. Even rewriting history isn't enough on its own: the repo may already have been cloned, forked or scraped by bots that watch public GitHub for keys.
GitHub's own documentation says this directly. Before rewriting history, revoke or rotate any exposed secret, because a rewrite doesn't make a leaked credential safe again.
So: assume every value in that file is compromised.
Step 1: Rotate every secret in the file
Go through the .env line by line. For each value, create a new one at the provider and kill the old one.
| Secret | Where to rotate |
|---|---|
| Supabase service_role / secret key | Supabase → Settings → API Keys: create a new secret key, switch to it, then delete the old sb_secret_ key or deactivate the legacy anon/service_role keys (docs) |
| Supabase database password / DATABASE_URL | Supabase → Database settings → reset password |
| Stripe secret key (sk_live_…) | Stripe Dashboard → API keys → ⋯ → Rotate key, expiration Now (docs) |
| Stripe webhook secret (whsec_…) | Stripe → Webhooks → your endpoint → roll secret |
| OpenAI / Anthropic keys | Provider dashboard → API keys → revoke, create new |
| GitHub tokens | GitHub → Settings → Developer settings → tokens → revoke |
| Auth secrets (NEXTAUTH_SECRET, your own JWT secrets) | Generate a new random value; existing sessions will be logged out |
| Supabase legacy JWT secret | Migrate to JWT signing keys, rotate, then revoke the legacy secret (docs) |
Then put the new values in your hosting provider's environment settings (Vercel, Netlify, Supabase function secrets, etc.) and redeploy.
Things that are public by design, like a Supabase anon/publishable key or a Stripe publishable key (pk_…), are lower priority. Still rotate them if it's easy, and make sure your Supabase Row Level Security is actually on.
Step 2: Check for misuse
While you're in each dashboard, look for:
- Usage or billing spikes (AI providers are a favourite target, because a stolen key turns straight into spend).
- New users, admins or API keys you didn't create.
- Unfamiliar charges, refunds or payouts in Stripe.
- Unusual database activity in Supabase logs.
Step 3: Stop tracking the file and fix .gitignore
# Stop tracking it, but keep your local copy
git rm --cached .env
# Make sure it never comes back
printf '\n.env\n.env.local\n.env.*.local\n.env.production\n' >> .gitignore
git add .gitignore
git commit -m "Stop tracking .env"
Add a .env.example with placeholder values so collaborators (and AI tools) know which variables exist without seeing real ones.
Step 4: Remove the file from git history
GitHub recommends git-filter-repo. You need version 2.47 or newer, which has the --sensitive-data-removal flag.
# Install (macOS shown; pip install git-filter-repo also works)
brew install git-filter-repo
# Work in a fresh clone
git clone https://github.com/YOU/YOUR-REPO.git cleanup && cd cleanup
# Remove .env from every commit on every branch and tag
git-filter-repo --sensitive-data-removal --invert-paths --path .env
Use the path as stored in the repo (for example apps/web/.env in a monorepo). Repeat --path for each file.
If secrets were pasted into other files (a hardcoded key in config.ts, say), list the exact strings in a text file outside the repo and replace them everywhere in history:
# ../secrets.txt contains one secret per line
git-filter-repo --sensitive-data-removal --replace-text ../secrets.txt
Check it worked:
git log --all --oneline -- .env # should print nothing
Then overwrite GitHub:
git push --force --mirror origin
Warning: this rewrites every commit hash after the change. Merge or close open pull requests first. Anyone else with a clone needs to re-clone or carefully rebase, or they could push the old history back.
Step 5: The bits git can't clean
Even after a force push, copies can survive on GitHub:
- Pull requests that referenced the old commits.
- Forks of your repo, which you don't control.
- Cached views of old commits.
GitHub's docs describe contacting GitHub Support with the "First Changed Commit(s)" that git-filter-repo prints, so they can dereference affected PRs and garbage-collect the data. This is exactly why Step 1 matters most. You can't guarantee every copy is gone, but you can make sure the leaked values no longer work.
Step 6: Prevent the next one
- Turn on GitHub secret scanning and push protection in the repo's Security settings. Push protection blocks pushes containing many known key formats. GitGuardian notes it misses "generic" secrets like passwords and connection strings, which made up 58% of leaks in their 2025 report, so don't rely on it alone.
- Tell your AI tool the rules. Add something like this to your Cursor rules or project instructions: "Never commit
.envfiles. Never hardcode secrets. Read secrets from environment variables, and only prefix a variable withVITE_orNEXT_PUBLIC_if it is safe to be public." - Look before you push. Run
git statusand read the file list beforegit add .. - Scan regularly, especially before you share the repo or launch.
Did you get it all?
The hard part isn't the .env you know about. It's the key that slipped into a commit three weeks ago, or the one Cursor hardcoded in a test file. Run a free ShipCheck scan on your GitHub repo (public, or private via Connect GitHub). It runs gitleaks across your current files and your last 100 commits, checks your .env and .gitignore setup, and flags vulnerable npm and Python packages, all summed up in a plain-English 0–100 score. Run it again after your cleanup to confirm the history is clean.
Built it with Lovable, Bolt, Cursor, Claude, Grok or ChatGPT? Is your repo safe to ship?
Paste a public GitHub repo, or connect GitHub for a private one, and get a risk score in under a minute. Free.
Built it with Lovable, Bolt, Cursor, Claude, Grok or ChatGPT? Is your repo safe to ship?
Paste a public GitHub repo, or connect GitHub for a private one, and get a risk score in under a minute. Free.