How to remove an API key from git history (accidentally committed .env file)
You accidentally committed a .env file, or pasted an API key into a config file, and now you need to remove the API key from git history. The order matters, so it comes before any command.
1. Rotate the key first
If the commit was pushed, treat the key as compromised and revoke it at the provider now. Rewriting history does not un-leak it. The old commit can still exist in forks, in every clone a collaborator or CI runner already pulled, in GitHub's cached views of the commit, and with the secret-scanning partners GitHub reports to. A rotated key makes all of those copies worthless. The provider-by-provider steps are in what to do when you leak an API key.
Cleaning the history is still worth doing afterwards, so the repository stops tripping scanners and nobody copies a dead key back into use.
2. If it is only your last local commit and you have not pushed
Untrack the file, ignore it, and fold both changes into the commit you already made:
git rm --cached .env
echo ".env" >> .gitignore
git add .gitignore
git commit --amend --no-edit
git rm --cached removes the file from the index and leaves it on disk. Check with git show --stat HEAD that .env is no longer in the commit. Nothing left your machine, so the key is safe unless it leaked some other way.
3. If it was pushed, or it is further back in history
Use git filter-repo. The Git project's own git filter-branch manual page warns against filter-branch and points to filter-repo instead. Install it with Homebrew:
brew install git-filter-repo
filter-repo refuses to run unless the repository looks like a fresh clone (you can override that with --force, but a fresh clone is the safer habit). Push any work you want to keep, then:
git clone [email protected]:you/your-repo.git repo-clean
cd repo-clean
# Drop a file from every commit in history:
git filter-repo --path .env --invert-paths
--path .env selects the file and --invert-paths flips the selection, so everything except that file is kept. The path is exact and renames are not followed, so repeat --path for each location, for example --path .env --path config/.env.production.
If the secret is a string inside a file you want to keep, replace the text instead. Create replacements.txt with one line per secret:
sk_live_THE_SECRET_VALUE==>REMOVED
git filter-repo --replace-text replacements.txt
Each line is literal text by default (regex: and glob: prefixes are supported). Leave off ==>REMOVED and the replacement is ***REMOVED***. Keep replacements.txt outside the repository and delete it when you are done.
4. Push the rewritten history
filter-repo removes the origin remote on purpose, so you cannot push rewritten history somewhere by accident. Add it back and force-push every branch and tag:
git remote add origin [email protected]:you/your-repo.git
git push --force --all
git push --force --tags
Every commit after the rewritten one now has a new hash. Every collaborator must delete their old clone and clone again. If anyone pushes from an old clone, the secret comes straight back. Branch protection rules that block force pushes need to be lifted for the push and restored afterwards.
5. The alternative: BFG Repo-Cleaner
BFG does the same two jobs with a shorter syntax. It needs Java and works on a bare mirror clone:
brew install bfg
git clone --mirror [email protected]:you/your-repo.git
bfg --delete-files .env your-repo.git
bfg --replace-text passwords.txt your-repo.git
cd your-repo.git
git reflog expire --expire=now --all && git gc --prune=now --aggressive
git push
--delete-files matches on file name, not path, so it removes every file called .env anywhere in the tree. By default BFG leaves the contents of your latest commit alone, so remove the file from HEAD with a normal commit first.
6. Why the commit can still be visible on GitHub
After a force push, GitHub can still serve the old commit by its full hash from cached views, and pull requests that contained it keep their own read-only references. GitHub's doc "Removing sensitive data from a repository" covers this: contact GitHub Support, give them the repository and the affected commits or pull requests, and ask them to purge the cached views and dangling references. Forks are separate repositories, and you cannot rewrite them. That is the reason step 1 is step 1.