It happens to everyone. You commit a file you shouldn't have — maybe a .env with API keys, a huge binary, or just something private — and then you push it.
The bad news: deleting the file and committing again doesn't actually remove it from git history. Anyone can still go back and see it.
The good news: you can rewrite history to remove it completely. Here's how.
Why deleting the file isn't enough
Git keeps a full snapshot of every commit. Even after you delete a file, it still lives in older commits. Someone can always run git log -- filename or check out an old commit to get it back.
To truly remove it, you need to rewrite every commit that ever touched that file.
Step 1 — Make sure the file is deleted locally
If the file still exists on disk, delete it first. Then stage the deletion:
git rm the-file.txt
git commit -m "Remove the-file.txt"
Or if you already deleted it manually:
git add -A
git commit -m "Remove the-file.txt"
Step 2 — Rewrite git history
This is the actual scrubbing step. Run this command (replace the-file.txt with your file path):
FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch --force \
--index-filter "git rm --cached --ignore-unmatch the-file.txt" \
--prune-empty \
--tag-name-filter cat \
-- --all
What each flag does:
-
--index-filter— runs a command on every commit's index (faster than checking out files) -
git rm --cached --ignore-unmatch— removes the file from the index, silently skips commits that don't have it -
--prune-empty— removes any commits that become empty after the file is gone -
--tag-name-filter cat— rewrites tags to point to the new commits -
-- --all— applies to all branches and refs
Note: Git will warn you about
filter-branchbeing error-prone and recommendgit-filter-repoinstead. If you can install it (pip install git-filter-repo), it's faster and safer. The equivalent command is just:git filter-repo --path the-file.txt --invert-paths
Step 3 — Clean up local dangling objects
After rewriting, old commits still hang around in your local reflog. Clean them up:
git reflog expire --expire=now --all
git gc --prune=now --aggressive
Step 4 — Force push
This is the step that overwrites the remote history:
git push origin main --force
Cautions — read before you do this
This is a destructive, irreversible operation. Once you force push rewritten history, the old commits are gone from the remote. Here's what that means in practice:
If others have cloned the repo
Anyone who cloned or pulled before your rewrite will have the old history. They'll need to re-clone or do a hard reset:
git fetch origin
git reset --hard origin/main
Their local branches with the old history will conflict with the new remote. Coordinate with your team before doing this.
The secret may already be compromised
If you pushed API keys or credentials, assume they are already leaked. Bots scan GitHub in real time. Rewriting history is cleanup — but your first step should be revoking and rotating the credentials immediately, before anything else.
It doesn't work on forks
If someone forked your repo before the rewrite, they still have the old history. You can't rewrite their copy.
GitHub/GitLab may cache the old commits
GitHub keeps cached views of old commits for a short time even after a force push. You can contact GitHub support to purge the cache, but for leaked secrets, rotate first — don't wait.
Quick recap
| Step | Command |
|---|---|
| Delete the file | git rm the-file.txt |
| Rewrite history |
git filter-branch ... or git filter-repo
|
| Clean up local objects |
git reflog expire + git gc
|
| Force push | git push origin main --force |
Best practices to avoid this in the first place
- Add sensitive files to
.gitignorebefore you create them - Use a
.env.examplefile with dummy values instead of committing real secrets - Tools like git-secrets or gitleaks can scan commits before they're pushed and block accidental leaks
Rewriting history is a useful escape hatch — but the best commit to regret is the one you never made.
Top comments (0)