How to delete commit history in Git, all of it or just one commit
Contents
Preface
Sometimes you accidentally commit something that shouldn’t be there, like an API key, a password or a whole .env file.
Deleting the file in a new commit doesn’t help, it’s still in the history for anyone who clones the repo.
There are three ways to get rid of it, depending on how much history you want to keep:
| Method | What’s left afterwards | When to use it |
|---|---|---|
| Orphan branch | one single commit | you don’t care about the history at all |
| Drop one commit | everything except that commit | the secret came in with one commit and no later commit touched that file |
| git filter-repo | everything, minus the secret | the secret is in a file you still need, or in several commits |
Rewriting the history hides it from the branch, but copies can already exist elsewhere. More on that at the end.
Wipe the whole history (orphan branch)
The simplest way: recreate the branch as an orphan branch, which removes all previous commits while keeping the current files.
This works for any branch (main, bleeding-edge, legacy, etc.).
main. Replace it with your branch name if needed.
First, check out the branch you want to clean:
git checkout main
Create an orphan branch (no history, working tree unchanged):
git checkout --orphan main-clean
Commit the current state as the new starting point:
git add .
git commit -m "cleanup"
Replace the old branch and force-push it:
git branch -D main
git branch -m main-clean main
git push origin main --force
After this, the branch contains only one commit, and the previous history is no longer referenced by the branch.
Drop a single commit
If one commit brought the secret in and nothing after it touched that file again, you don’t have to throw everything away.
Order, messages and content of all other commits stay. Only the hashes of the commits after the dropped one change.
Find the commit first:
git log --oneline
In my test repo that looks like this, 0db33f9 is the one that added the .env:
6c13408 add readme
ef463f6 update app
0db33f9 add config
a4ca0b7 add app
Start an interactive rebase from the commit right before it:
git rebase -i 0db33f9~1
Your editor opens with a list of commits.
Change pick to drop in front of the one you want gone, then save and close:
drop 0db33f9 add config
pick ef463f6 update app
pick 6c13408 add readme
git rebase --onto 0db33f9~1 0db33f9
Check that it’s gone. -S searches every commit on the branch for a string:
git log --oneline -S "supersecret123"
No output means no commit on the branch contains it anymore.
Then push the rewritten branch:
git push --force-with-lease origin main
--force-with-lease only overwrites the remote if nobody else pushed in the meantime.A plain
--force would silently throw their work away.
If a later commit changed the same file, the rebase stops with a conflict.
That usually means the secret is in those later commits too, and the next method is the better tool.
Remove a file or a password from every commit
git filter-repo rewrites the whole history in one go, and it’s what GitHub itself recommends for this.
You’ll also find BFG Repo-Cleaner in older guides, it does something similar, but I only tested filter-repo.
Install it:
pipx install git-filter-repo
Most distributions also package it under the same name.
filter-repo wants a fresh clone, so it can’t touch anything you haven’t pushed yet:
git clone https://github.com/<user>/<repo>.git
cd <repo>
Option 1: remove a file from all commits
git filter-repo --sensitive-data-removal --invert-paths --path .env
The file is gone from every commit.
A commit that only added that file ends up empty, and filter-repo removes it entirely.
Option 2: keep the file, but replace the secret everywhere
Create a replacements.txt next to (not inside) the repo, one line per secret:
hunter2secret==>REMOVED
Then run:
git filter-repo --sensitive-data-removal --replace-text ../replacements.txt
In my test, config.yml stayed in all three commits, just with password: REMOVED instead of the real value.
--sensitive-data-removal needs git-filter-repo 2.47 or newer.It also fetches every branch and tag from the remote first, so nothing with the secret in it gets left behind.
Check that it’s gone, this time across all branches:
git log --all --oneline -S "hunter2secret"
Then push everything back:
git push --force --mirror origin
--mirror makes the remote look exactly like your local clone, including deleting remote branches that don’t exist locally.That’s why this should only be run from the fresh clone that filter-repo just rewrote.
What rewriting can’t fix
After the force-push, the branch is clean. But in my test, the old commit was still on the server, it just wasn’t part of any branch anymore:
git cat-file -t 0db33f9
git show 0db33f9:.env
commit
API_KEY=supersecret123
Git only deletes commits that nothing points to during garbage collection, and on GitHub you don’t control when that happens.
On top of that:
- Everyone who cloned the repo before still has the old history.
- Forks keep their own copy, and you can’t clean those.
- Pull requests on GitHub keep pointing at the old commits. According to GitHub’s docs, only GitHub Support can clean those up, and only if changing the secret isn’t enough.
So once a secret was pushed, CHANGE IT!
Cheers.