Deleting a sensitive file from a GitHub repo and committing again does not remove it from history. Git stores every commit permanently, so anyone can retrieve old files from the commit log. BFG Repo-Cleaner rewrites git history to strip files from every commit. The process involves creating a mirror clone, running BFG to remove target files, then force-pushing the clean version. After cleaning, rotate any exposed credentials and have collaborators re-clone.
If you’ve ever pushed a file to a public GitHub repo and later realized it contained something private, you already know the sinking feeling. An API key, a config file with credentials, a spreadsheet that should have stayed local.
The instinct is to delete the file and commit again. That doesn’t work. GitHub keeps your entire history, and anyone who knows where to look can still see the file in an older commit. This tutorial walks through how to actually remove it, from the current version and from every commit that ever contained it.
Why deleting the file isn’t enough
For a plain-language overview of how commits and branches work, start with Git for Non-Developers.
Git is a version control system. Every commit you’ve ever made is stored in the repository’s history, and that history is public if your repo is public. When you delete a file in a new commit, you’re adding a record that says “this file is gone now.” But all the previous commits that contained the file are still there, intact, and readable by anyone.
So if you pushed config.json in commit 3 and deleted it in commit 47, anyone can still run git checkout <commit-3-hash> -- config.json and get the file back. Or they can just browse the commit history on GitHub and read the contents directly.
To actually remove sensitive data, you need to rewrite the history: go back through every commit, strip the file out, and replace the old commits with new ones that never contained it.
What BFG Repo-Cleaner is
BFG Repo-Cleaner is a tool built specifically for this job. It’s faster and simpler than git filter-branch, which is the older built-in option for rewriting history. BFG’s whole purpose is removing unwanted data from git history, whether that’s big files, passwords, or specific files you want gone.
You give it a pattern or a list of files to remove, it rewrites every commit in your history to exclude them, and you’re left with a clean version of the repo that can be force pushed back to GitHub.
What a mirror clone is
A mirror clone is a complete copy of a git repository, including all branches, all tags, and all the internal git objects. You use a mirror clone when you need to operate on the full history of a repo, not just the files in your working directory.
The command is git clone --mirror <repo-url>. This gives you a .git-style directory (no working files) that BFG can operate on directly.
What “force push” means and why it’s needed here
A normal git push adds your new commits on top of whatever is already on the remote. Git refuses to push if your local history has diverged from the remote, because you’d be overwriting commits the remote has that you don’t.
A force push (git push --force) says: replace whatever is on the remote with exactly what I have locally. It overwrites history. In normal development, this is dangerous because you can destroy other people’s work. You never force push to a shared branch without coordination.
But here, force pushing is the point. You’ve rewritten history to remove sensitive data, and you need GitHub to accept the rewritten version. The old history is what you’re trying to get rid of. Force push is the only way to do that.
The full process
Here’s the sequence Claude Code runs through to clean a repo:
1. Create an allowlist-style .gitignore
An allowlist .gitignore flips the normal pattern. Instead of listing things to ignore, you start by ignoring everything (*) and then explicitly un-ignore the files you want to keep. This is safer than trying to enumerate every sensitive file type. If a file isn’t on the allowlist, it won’t be tracked.
# Ignore everything by default
*
# Un-ignore specific files you actually need
!deploy.sh
!Makefile
!src/
!src/**
2. Remove files from current tracking
git rm --cached <file> removes a file from git’s index (what git is tracking) without deleting it from your hard drive. This stops git from committing it going forward.
3. Install BFG Repo-Cleaner
BFG requires Java. On a Mac with Homebrew: brew install bfg. Or you can download the jar directly from the BFG site.
4. Clone a mirror of the repo
git clone --mirror https://github.com/yourname/yourrepo.git
This gives you yourrepo.git, a bare clone with the full history.
5. Run BFG to strip the files from history
bfg --delete-files secrets.json yourrepo.git
You can pass a filename, a glob pattern, or a text file listing multiple files to remove. BFG rewrites every commit that contained those files. The current commit is protected by default since BFG assumes you’ve already cleaned it.
6. Clean up and force push
cd yourrepo.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force
The reflog expire and gc commands clean up the leftover git objects from the old history so they don’t linger. Then the force push sends the clean version to GitHub.
After force pushing, GitHub takes a few minutes to update cached views of old commits.
7. Audit which repos are public
While you’re doing this, it’s worth checking how many of your repos are public when they don’t need to be. Claude Code listed all public repos using the GitHub CLI and flagged ones that looked like personal experiments, side projects, or anything with no real reason to be visible.
gh repo list --visibility public --limit 100
The answer in this case was 28 repos public. Most of them didn’t need to be. Switching them to private takes one command each:
gh repo edit yourname/reponame --visibility private
What Claude Code actually does with this prompt
When given the prompt “I need to clean up the public GitHub repo, found some private data that shouldn’t be there, remove it and make sure it’s gone from the git history too,” Claude Code:
- Inspects the repo and identifies which files shouldn’t be tracked
- Creates or updates
.gitignorewith an allowlist pattern - Removes those files from git’s index
- Installs BFG if it’s not already installed
- Runs the full mirror clone, BFG scrub, and force push sequence
- Lists all public repos and offers to make private any that don’t need to be public
The whole process takes a few minutes. The alternative is doing it manually, which involves reading the BFG docs, understanding git internals, and running the right sequence of commands without skipping the cleanup steps.
A few things to do after
Once the force push is done:
- Rotate any credentials that were in the exposed files. Even if the exposure window was short, assume the keys are compromised.
- Check if the repo is indexed anywhere else, like npm, package registries, or internal mirrors.
- If other people have cloned the repo, they’ll need to re-clone it. Their local copies still contain the old history, and if they push from them, the sensitive files come back.
Further reading
- BFG Repo-Cleaner for the official docs, including how to remove passwords and large files
- GitHub docs: Removing sensitive data from a repository for GitHub’s official guidance, including notes on cached views and forks
- git-filter-repo if you want the more powerful (and more complex) alternative to BFG
- GitHub docs: .gitignore for how
.gitignoreworks and the allowlist pattern - GitHub CLI for managing repos, visibility, and settings from the terminal
Common Questions
How do I remove a file from git history completely?
Use BFG Repo-Cleaner. Clone a mirror of the repo with git clone --mirror, run bfg --delete-files <filename> repo.git, then clean up with git reflog expire and git gc, and force push. This rewrites every commit to exclude the file.
Why doesn’t deleting a file in git remove it from history?
Git stores every commit as a permanent snapshot. Deleting a file creates a new commit saying the file is gone, but all prior commits that contained it remain intact. Anyone can check out an older commit and retrieve the file.
What is BFG Repo-Cleaner?
BFG is an open-source tool for removing unwanted data from git history. It is faster and simpler than git filter-branch. It rewrites commits to exclude specified files, passwords, or large binaries while protecting the latest commit by default.
How do I check which of my GitHub repos are public?
Run gh repo list --visibility public --limit 100 using the GitHub CLI. To make a repo private: gh repo edit yourname/reponame --visibility private. Review public repos periodically to ensure nothing is exposed unintentionally.
A note from Alex: hi i’m alex - i run code for creatives. i’m a writer so i feel that it is important to say - i had claude write this piece based on my ideas and ramblings, voice notes, and teachings. the concepts were mine but the words themselves aren’t. i want to say that because its important for me to distinguish, as a writer, what is written ‘by me’ and what’s not. maybe that idea will seem insane and antiquated in a year, i’m not sure, but for now it helps me feel okay about putting stuff out there like this that a) i know is helpful and b) is not MY voice but exists within the umbrella of my business and work. If you have any thoughts or musings on this, i’d genuinely love to hear them - its an open question, all of this stuff, and my guess is as good as yours.