Home/Security/Emergency Recovery Guide: Accidentally Pushed .env or API Keys to Git
Securitygitgit-filter-repo

Emergency Recovery Guide: Accidentally Pushed .env or API Keys to Git

Accidentally pushed .env files or API keys to git? This guide walks through fully purging them from history with git filter-repo and BFG, immediately rotating exposed keys, and locking down prevention so it does not happen again.

Emergency Recovery Guide: Accidentally Pushed .env or API Keys to Git
Contents
  1. Trap 1 — `git rm --cached` plus a new commit will never actually remove it
  2. Completely purge history: git filter-repo vs BFG Repo-Cleaner
  3. The most important step — exposed keys are not "deleted", they are "revoked"
  4. Wrap-up — a 3-piece prevention kit
  5. FAQ

Emergency Recovery Guide: Accidentally Pushed .env or API Keys to Git

Just ran git push and realized your .env or AWS_SECRET_ACCESS_KEY went up with it? Your heart just dropped. What you need right now is not panic — it is first aid you can follow in order. Stick to the sequence below.

Do this first — priority order

  1. (First) Immediately revoke/rotate the exposed keys and passwords — deletion comes after
  2. Completely remove secrets from git history, then force push
  3. Notify collaborators + set up prevention

The order might feel backwards. Key invalidation comes before rewriting history. Here is why.

Trap 1 — git rm --cached plus a new commit will never actually remove it

This is the most common mistake.

Bash
git rm --cached .env
git commit -m "remove .env"
git push

That removes .env from the current file tree. But git permanently stores every change as commit objects. The secret is still frozen in earlier commits. Check for yourself.

Bash
# Past commits still print the full .env contents
git log -p -- .env

# Or check out that commit and the file comes back
git checkout <commit-with-secret> -- .env
cat .env   # the password is sitting there intact

The "deletion commit" actually becomes a billboard saying a secret used to live here — which makes things worse. Bottom line: you do not need a new commit, you need to rewrite history itself.

Completely purge history: git filter-repo vs BFG Repo-Cleaner

Both tools strip files/strings out of history entirely. Before you start, take a backup clone (git clone --mirror).

Bash
# Install
pip install git-filter-repo      # or brew install git-filter-repo

# Remove a specific file from all of history
git filter-repo --path .env --invert-paths

# Keep the file, mask only the values
# patterns.txt example: AKIA[0-9A-Z]{16}==>***REMOVED***
git filter-repo --replace-text patterns.txt

Method B — BFG Repo-Cleaner (large repos, simpler)

Bash
# After downloading
java -jar bfg.jar --delete-files .env
# Or replace specific strings (list tokens to remove in secrets.txt)
java -jar bfg.jar --replace-text secrets.txt

# BFG requires you to run post-processing yourself
git reflog expire --expire=now --all
git gc --prune=now --aggressive

filter-repo auto-cleans .git/refs/original, but it is still a good idea to run reflog and gc with either tool.

Itemgit filter-repoBFG Repo-Cleaner
SpeedFastVery fast (large repos)
FlexibilityVery high (paths, regex, etc.)Mostly simple files/strings
DependenciesPythonJava (JVM)
Best forWhen you need fine-grained controlQuickly cleaning huge repositories

force push

Bash
git push --force-with-lease --all
git push --force-with-lease --tags

Use --force-with-lease instead of plain --force. It prevents accidentally overwriting work someone else pushed in the meantime.

The most important step — exposed keys are not "deleted", they are "revoked"

Core principle: assume a bot scanned it the moment you pushed. Public repos get crawled by automated scanners within seconds. Private does not mean safe either. No matter how clean you make history, a key someone may already have copied is worthless unless you invalidate it.

Where to revoke, by service:

  • AWS: IAM → Access keys → Deactivate then Delete the key, issue a new one
  • OpenAI: Platform → API keys → Revoke, then reissue
  • Stripe: Dashboard → Developers → API keys → Roll/Reveal
  • DB passwords: Change immediately and sync app config / secret manager
  • GitHub PAT/OAuth: Settings → Developer settings → delete the token

GitHub Secret Scanning detects some partner tokens (e.g. GitHub Token, AWS, Stripe), notifies the issuer, and sometimes auto-revokes the token. Do not ignore those alerts. Since 2024, Push Protection is on by default for public repos and can block the push itself.

A note from the field: Early in my career I figured a private repo was fine and delayed rotating keys. A token that lingered on a forked internal mirror blew up a month later. "It's private, I'll get to it" is the most expensive assumption you can make.

Impact on collaborators — deleting is not the end

After you rewrite history, other teammates still have the old history (secrets included) locally. Also:

  • Open PRs / cached diffs: GitHub PR pages can cache old commit diffs → close and recreate the PR if needed
  • forks: old objects still live in forked repos

Send the team something like this:

"We rewrote history. Throw away your existing clone and clone fresh. Do not rebase/merge from your local copy. Related keys have already been revoked and reissued."

Wrap-up — a 3-piece prevention kit

Set these up today so you never go through this twice.

Bash
# 1. Add to .gitignore
echo ".env" >> .gitignore
echo "*.pem" >> .gitignore

# 2. Install and register git-secrets
brew install git-secrets
git secrets --install
git secrets --register-aws
git secrets --add 'AKIA[0-9A-Z]{16}'

# 3. Block at push time with a pre-commit hook
pip install pre-commit detect-secrets

Example .pre-commit-config.yaml:

YAML
repos:
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.5.0
    hooks:
      - id: detect-secrets

Add gitleaks or trufflehog in CI for another filter before merge. These days AI coding assistants often auto-fill example keys that then get committed as-is — another reason to keep automated detection on.

Apply-today checklist

  • Rotate/revoke every exposed key
  • Change DB and service passwords
  • Clean history with filter-repo/BFG, then --force-with-lease push
  • Tell the team to re-clone; check forks and PRs
  • Set up .gitignore + git-secrets + pre-commit

FAQ

Q. It's a private repo — do I still have to revoke the keys? A. Yes. Collaborators, forks, CI logs, caches — lots of leak paths, and permissions can change anytime. "I deleted it so we're safe" is a dangerous assumption. Always rotate.

Q. git filter-repo or BFG? A. Need fine-grained path/regex control? filter-repo. Need to quickly clean a multi-GB repo? BFG is easier. After either, finish with reflog expire + gc --prune=now.

Q. I force-pushed but teammates still have the old secrets on their machines. Now what? A. History rewrite only changes the remote. Tell them to delete the old clone and clone again. More importantly: if you already revoked the keys, leftover copies cannot actually do damage.

✦ ✦ ✦
Sources and verification

Sources and checks vary by article. Only articles with a recorded check date, scope and sources below were checked, and only within that scope. For other articles, verify against the current docs for your version before applying anything, and email us about errors. Editorial policy · email us

This article has no recorded human check (date and scope) yet.

Published ·
Topic-related official docs (auto-linked · not per-claim sources)Git documentation ↗OWASP documentation ↗HashiCorp Vault documentation ↗

Comments

Be the first to comment.