Here’s a practical, safe way to do it with git filter-repo (the modern, fast, supported tool). The idea is: keep only the paths that exist in HEAD today, so every file that’s gone will disappear from all history. (You’ll keep history for files that still exist; you may lose pre-rename history for files that were renamed—see “Renames” notes.)
- This rewrites history. Everyone must re-clone or hard-reset afterwards.
- Make a backup before you start.
- If you’re scrubbing secrets that ever lived in deleted files, rotate those credentials anyway.
- Ensure you have
git-filter-repo installed (pip install git-filter-repo) and do this in a fresh mirror clone.
# fresh bare mirror (best for history surgery)
git clone --mirror <YOUR_REMOTE_URL> repo-clean.git
# optional: duplicate the mirror as a safety backup
cp -r ../repo-clean.git ../repo-clean-backup.git
git remote -v # sanity check
# Choose the branch whose current tree defines “what exists” (e.g., main)
# List every tracked file path in HEAD of that branch
git ls-tree -r --name-only "$BRANCH" > keep-paths.txt
# (Optional) inspect/trim this list if needed
This file is the allowlist. Anything not listed is purged from the repo’s entire history.
# filter-repo works in-place on the mirror
git filter-repo --refs "$BRANCH" --paths-from-file keep-paths.txt --force
What this does:
- Drops every file that doesn’t exist in
HEAD of main.
- Commits that only touched deleted files vanish.
- Commits that touched both kept and deleted files are rewritten to only include the kept paths (and may coalesce if they end up empty).
Want to apply to all branches/tags? Omit --refs "$BRANCH" to rewrite everything:
git filter-repo --paths-from-file keep-paths.txt --force
# See top-level history size and a few commits
git log --oneline --graph --decorate --all | head -n 50
# Spot check: files that are gone should be gone across history
git log --all -- '**/a-deleted-file.ext' # should return nothing
If something looks wrong, throw away this mirror and retry from the backup you made in step 1.
⚠️ Coordinate with collaborators first. Freeze merges/PRs until done.
# Overwrite the remote (mirror push updates all refs)
git push --force --prune --tags origin 'refs/heads/*'
After this:
- Teammates must re-clone (recommended) or hard-reset:
git reset --hard origin/main
The allowlist above uses current paths. If a file was renamed in the past, you’ll keep its history only from the point of its current name. If you need to preserve history across renames for surviving files, you have two options:
-
Good-enough, manual add
For any file that was renamed, add its former path(s) to keep-paths.txt. You can discover former names with:
# from a non-bare clone (easier to read):
git log --follow --name-status -- <current/path.ext> | grep -E '^R[0-9]+'
Extract the old path on each Rxxx line and append to keep-paths.txt, then rerun step 3.
-
Advanced (scripted)
Write a small script to walk rename history for each current file and collect prior names only for that file (not other files touched in the same commit), then feed the union into keep-paths.txt. (Happy to draft that if you want the automated route.)
If your repository uses a different default branch (e.g., main, master, trunk), set BRANCH accordingly. If you want the current tree from a different branch to define “what exists,” use that branch in step 2.
After the rewrite, all open PRs will diverge. Close and reopen against the new history, or have contributors rebase on top of the new base.
- Consider temporarily disabling branch protection on the default branch to allow the force-push, then re-enable it.
- Update any cached artifacts that may contain old history (e.g., Actions caches).
If the goal includes scrubbing secrets that were only in deleted files, this process removes them from Git history. Still rotate credentials; copies may exist elsewhere (clones, CI logs, etc.).
git clone --mirror <URL> repo-clean.git
git ls-tree -r --name-only "$BRANCH" > keep-paths.txt
# keep only current files across all refs:
git filter-repo --paths-from-file keep-paths.txt --force
git push --force --prune --tags origin 'refs/heads/*'
If you want, I can also give you the rename-aware script that auto-discovers prior names for current files and builds keep-paths.txt accordingly.