Remove a password or API key from Git history
I committed a secret — how do I remove it from Git history?
Short answer
Rotate the secret first — that's the only step that genuinely protects you. Then purge it from history with git filter-repo (or the BFG Repo-Cleaner) and force-push. Deleting the file in a new commit does nothing, because the old commit still contains it and is still fetchable.
pip install git-filter-repo
git filter-repo --path .env --invert-paths
Strips a path out of every commit that ever contained it
Does this match your situation?
- You committed a .env, credentials file, private key or hard-coded API token.
- A secret scanner (GitHub, GitGuardian) flagged your repository.
- You deleted the file in a later commit but the secret is still visible in history.
Step-by-step fix
Rotate the secret before touching Git
Assume the credential is compromised the moment it lands on a remote. Bots scrape public repositories within seconds, and history rewrites cannot recall what was already cloned, cached or indexed. Revoking and reissuing the key is the only step that actually restores security — everything after this is cleanup.
step 11. Revoke the key in the provider's dashboard 2. Issue a replacement 3. Store it in an environment variable or secret manager 4. Only then continue with the history rewriteIf the repo is public, treat the secret as leaked no matter how quickly you rewrite history.
Install git-filter-repo
This is the tool the Git project itself recommends. The older git filter-branch is officially discouraged: it's dramatically slower and easy to get subtly wrong.
step 2pip install git-filter-repo # macOS: brew install git-filter-repo # Windows: pip install git-filter-repo (needs Python on PATH)Back up the repository, then purge the file
Rewriting history is not reversible in place, so clone a backup first. Then remove the path from every commit that ever contained it.
step 3git clone --mirror . ../repo-backup.git # safety copy git filter-repo --path .env --invert-paths # multiple paths at once: git filter-repo --path .env --path config/secrets.json --invert-pathsOr redact just the string, keeping the file
When the secret is one line inside a file you want to keep, replace the text everywhere instead of deleting the whole path.
step 4printf 'sk_live_abc123==>REDACTED\n' > ../replacements.txt git filter-repo --replace-text ../replacements.txtForce-push and get everyone onto the new history
Every commit hash after the rewrite point has changed, so the remote needs the new history and every clone is now invalid. Teammates should re-clone rather than pull, because merging old history back in would reintroduce the secret.
step 5git remote add origin <url> # filter-repo removes remotes by design git push --force --all git push --force --tagsOn GitHub, also delete stale forks and ask support to purge cached views of the old commits.
Why this works
Git history is an append-only chain of immutable snapshots. Deleting a file in a new commit adds a snapshot without it, but every earlier commit still contains the file verbatim and anyone can check it out. The only way to make the content actually disappear is to rebuild every affected commit without it, which produces entirely new hashes for that commit and all its descendants. Since the old objects may already exist on the server, in forks, in CI caches and in other people's clones, the rewrite is damage control — rotation is the fix.
If that didn’t work
- The BFG Repo-Cleaner is an easier alternative for the common cases: java -jar bfg.jar --replace-text passwords.txt.
- For a repository whose entire history is compromised, deleting and recreating it can be cleaner than rewriting.
- GitHub Support can purge cached commit views that remain reachable by direct URL after a force-push.
How to stop it happening again
- Add .env and credential paths to .gitignore before your first commit.
- Enable push protection / secret scanning on GitHub or GitLab.
- Install a pre-commit hook such as gitleaks or detect-secrets to block secrets locally.
- Keep configuration in environment variables and commit only a .env.example with placeholder values.
Commands used in this guide
git filter-repoRewrite an entire repository's history - the supported way to purge a leaked secret or a huge file.
git pushUpload your commits to the remote repository.
git logBrowse commit history with as much or as little detail as you want.
git rebase -iReorder, squash, split, reword or drop your recent commits.
Still stuck?
Search the full command reference and every other rescue guide — there are 56 of them, covering everything from detached HEAD to force-push disasters.
Browse all guides →Frequently asked questions
Is deleting the file in a new commit enough to remove a secret?
No. The earlier commits still contain the file and anyone can retrieve it with git show or by checking out that commit. You must rewrite every commit that contained the secret, and you must rotate the credential regardless.
Should I use git filter-repo or git filter-branch?
git filter-repo. The Git project explicitly discourages filter-branch — it is orders of magnitude slower on real repositories and has sharp edges that silently corrupt history. filter-repo is the officially recommended replacement.
Do I still need to rotate the key if I rewrote history immediately?
Yes. Automated scrapers monitor public repositories continuously and can grab a key within seconds of the push. The remote may also keep the old objects reachable by direct hash URL, and forks and clones are entirely outside your control.
What happens to my teammates after the rewrite?
Their clones now contain history that no longer exists upstream. They should re-clone. If they pull or merge instead, the old commits — and the secret — get pushed straight back into the repository.
Related rescue guides
I committed a large file and my push is rejected — how do I remove it?
If it's still the last commit, git reset --soft HEAD~1 and re-commit without it. If it's further back, purge it from history with git filter-repo --strip-blobs-bigger-than. Going forward, route large binaries through Git LFS.
Read the fix →Always fixableWhy is .gitignore not working for files that are already tracked?
.gitignore only applies to untracked files. Once a file has been committed, Git keeps tracking it no matter what the ignore rules say. Remove it from the index with git rm --cached <file> — that stops tracking while leaving the file on your disk.
Read the fix →Always fixableHow do I undo a commit that's already on the remote?
On a shared branch, use git revert <hash> — it adds a new commit that reverses the change, so nobody else has to do anything. Rewriting with reset plus a force-push is only appropriate on a branch you're certain nobody else has pulled.
Read the fix →Always fixableHow do I undo git add without losing my changes?
git restore --staged <file> removes a file from the staging area and leaves your edits completely untouched. The older equivalent is git reset HEAD <file>. Neither one touches the contents of your file, so this is a zero-risk operation.
Read the fix →