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:

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:

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

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.

Scan your repo 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.

Scan your repo free →