How to Remove API Keys
From Git History
Deleting the file isn't enough — the key still lives in every previous commit. Here's the complete playbook.
Do this first — before reading anything else
Revoke or rotate the exposed key immediately from your provider's dashboard. Assume it has already been scraped — automated bots scan GitHub commits in real time. History cleanup comes second.
How to remove API keys from Git history is something most developers only search after the panic has already set in. You committed a .env file, pushed to GitHub, and then realised what you'd done. The instinct is to delete the file and commit the deletion. That doesn't work — the key still exists in every previous commit in your repository's history.
Why Deleting the File Isn't Enough
Git is designed to never lose data. When you commit a deletion, Git records that the file is gone in the new commit — but all previous commits still contain it. Anyone can run git log, find the commit where the file was added, check it out, and read your secret.
If your repository is public, it's worse. GitHub caches repository content for performance. Even after you delete the file, cached views of the commit may remain accessible. And within seconds of a push, automated scanners are crawling public commits looking for AWS keys, Stripe secrets, OpenAI tokens and hundreds of other patterns.
Step 1 — Revoke the Key Immediately
Before touching Git, go to your provider's dashboard and revoke or rotate the exposed credential. This is the only action that actually stops the damage. History cleanup stops future exposure — it doesn't undo what's already happened.
Step 2 — Choose Your Tool
There are two main options. git-filter-repo is the modern standard — recommended by GitHub, faster than the old git filter-branch, and requires only Python. BFG Repo-Cleaner is simpler for basic cases but requires Java.
| Tool | Requires | Speed | Best For |
|---|---|---|---|
| git-filter-repo | Python 3 | Fastest | Recommended for all cases |
| BFG Repo-Cleaner | Java 11+ | Very fast | Simple file deletion or string replacement |
| git filter-branch | Nothing extra | Very slow | Avoid — deprecated and error-prone |
Method A — git-filter-repo (Recommended)
Install
pip install git-filter-repo
Option 1: Replace the secret string everywhere in history
Create a text file listing what to replace:
# secrets.txt — one replacement per line
sk_live_abc123xyz==>REDACTED
AKIA1234567890ABCDEF==>REDACTED
# Clone a fresh mirror first
git clone --mirror https://github.com/you/repo.git repo-mirror.git
cd repo-mirror.git
# Run the replacement
git filter-repo --replace-text ../secrets.txt
# Push cleaned history
git push --force --all
git push --force --tags
Option 2: Remove an entire file from history
git clone --mirror https://github.com/you/repo.git repo-mirror.git
cd repo-mirror.git
# Remove .env from all commits
git filter-repo --path .env --invert-paths
git push --force --all
git push --force --tags
Method B — BFG Repo-Cleaner
Download the JAR from rtyley.github.io/bfg-repo-cleaner. Requires Java 11+.
# Replace secrets listed in a file
java -jar bfg.jar --replace-text secrets.txt repo-mirror.git
# Or delete a specific file from all history
java -jar bfg.jar --delete-files .env repo-mirror.git
# Clean up and push
cd repo-mirror.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force --all
BFG doesn't clean the HEAD commit by default
BFG protects your latest commit. Make sure your current HEAD doesn't contain the secret before running BFG — remove it manually first, commit the clean version, then run BFG on the history.
Step 3 — Tell Your Team
Force-pushing rewrites history. Every team member's local clone now has diverged history. They must delete their local copy and re-clone:
# Each team member needs to run this
git fetch --all
git reset --hard origin/main
# Or just: delete local clone and git clone again
GitHub caches old commits
Even after force-pushing, GitHub may serve cached views of old commits. Contact GitHub Support and request they flush cached views of your repository. This is especially important for public repos where the key may have been indexed by search engines.
Step 4 — Stop It Happening Again
.env, *.pem, config/secrets.* to your .gitignore immediately. Add a .env.example with placeholder values for documentation.detect-secrets or gitleaks as a pre-commit hook. It scans staged files for API key patterns before the commit is made..env locally (gitignored), and your hosting platform's env config in production (Vercel, Netlify, Railway, Heroku all support this).Before You Share the Code — Sanitize First
If you need to share code, logs or stack traces for debugging — with a colleague, on StackOverflow, or with an AI assistant — run it through a sanitizer first to catch any remaining secrets.
Sanitize Code Before Sharing
Strip API keys, emails and credentials from logs and stack traces. 100% client-side — nothing leaves your browser.
FAQ
How do I remove an API key from Git history? +
Revoke the key first, then use git filter-repo --replace-text secrets.txt or BFG Repo-Cleaner to rewrite all commits. Force-push the cleaned history. Tell all collaborators to re-clone.
Is deleting the file enough to remove a secret from Git? +
No. Deleting a file only removes it from the latest commit. Every previous commit still contains the secret. You must rewrite history with git-filter-repo or BFG to remove it from all commits.
What should I do first if I committed an API key? +
Revoke and rotate the key immediately — assume it is already compromised. Bots scan public GitHub commits in real time. History cleanup is important but secondary to revoking the key.
What is the difference between git-filter-repo and BFG? +
git-filter-repo is the modern recommended tool — Python-based, no Java required, and GitHub's official recommendation. BFG is simpler to use but requires Java. Avoid the old git filter-branch — it is slow and deprecated.
How do I stop API keys being committed to Git in future? +
Add .env to .gitignore, use a pre-commit hook with detect-secrets or gitleaks, and enable GitHub Secret Scanning with push protection in your repo settings.