BFG Repo-Cleaner Guide: Remove Sensitive Data and Shrink Your Git Repository at Lightning Speed

Git tutorial - IT technology blog
Git tutorial - IT technology blog

The Problem: When Your Git Repository Bloats and Holds Things It Shouldn’t

Many developers have run into this: you’re reviewing an old repo one day and discover a .env file with a real API key sitting right there in the git history from six months ago. Or maybe the repo is ballooning at 200MB even though the actual code is only a few dozen MB — because someone once accidentally committed a video file, a dataset, or a SQL backup.

Deleting the file from the working tree and committing again doesn’t solve the problem. Git stores the entire history — the file still lives in the object database and can still be checked out using the old commit hash. Anyone who knows to use git log --all and git checkout can still retrieve that content without any trouble.

Why git filter-branch Isn’t a Good Option

The classic solution is git filter-branch — Git’s built-in command for rewriting history. The problem is it’s extremely slow. I once ran git filter-branch on a 500MB repo with around 3,000 commits and it took nearly 2 hours. For repositories with tens of thousands of commits, you’re looking at days.

BFG Repo-Cleaner was built to solve exactly this pain point. According to the author’s benchmarks, BFG is 10 to 720 times faster than git filter-branch depending on the case. Written in Scala, it runs multithreaded and processes the object graph in parallel — that’s why the speed difference is so dramatic.

Installing BFG Repo-Cleaner

Prerequisite: Java Runtime

BFG runs on the JVM and requires Java 8 or later:

java -version
# java version "17.0.8" or similar is fine

If you don’t have Java installed:

# Ubuntu/Debian
sudo apt install default-jre

# macOS (Homebrew)
brew install openjdk

# Windows (Chocolatey)
choco install openjdk

Download the BFG JAR

BFG is a single JAR file — no complex installation needed:

# Download version 1.14.0 (latest stable)
wget https://repo1.maven.org/maven2/com/madgag/bfg/1.14.0/bfg-1.14.0.jar -O bfg.jar

# Verify
java -jar bfg.jar --version

For everyday convenience, add an alias to your shell config:

# Add to ~/.bashrc or ~/.zshrc
alias bfg='java -jar /path/to/bfg.jar'

source ~/.bashrc

Step-by-Step: Real-World Use Cases

Preparation: Clone the Repository as a Mirror

BFG works directly on a bare repository. Before doing anything, clone it as a mirror to your local machine:

# Clone as bare (mirror) — do NOT use a regular clone
git clone --mirror [email protected]:username/my-repo.git my-repo.git

# Inspect the bare directory
ls my-repo.git/

Working on a bare clone is safer — the original remote repo is unaffected until you deliberately force push. This is an important safety net.

Case 1: Remove a Sensitive File That Was Accidentally Committed

The most common use case. For example, accidentally committing a .env file, secrets.json, or a private key:

cd my-repo.git

# Delete a specific file from the entire history
java -jar bfg.jar --delete-files .env

# Delete multiple files (glob pattern)
java -jar bfg.jar --delete-files '*.pem'
java -jar bfg.jar --delete-files '{.env,.env.local,.env.production}'

# After BFG finishes, you must run these steps
git reflog expire --expire=now --all
git gc --prune=now --aggressive

The git gc command is what actually frees up disk space — BFG only marks objects as orphaned; it’s gc that removes them from disk.

Case 2: Remove Large Files to Shrink Repo Size

I once encountered a machine learning project repo where someone accidentally committed a 1.5GB dataset folder, ballooning the repo from 50MB to 1.6GB. BFG handles this cleanly:

# Remove all blobs larger than 50MB from history
java -jar bfg.jar --strip-blobs-bigger-than 50M

# Or use a stricter limit
java -jar bfg.jar --strip-blobs-bigger-than 10M

Important note: BFG automatically protects the latest commit (HEAD). If the large file still exists in HEAD, you need to remove it manually first:

# Remove the file from HEAD before running BFG
git rm --cached dataset.zip
git commit -m "chore: remove accidentally committed large file"

# Then run BFG
java -jar bfg.jar --strip-blobs-bigger-than 50M

Case 3: Replace Hardcoded Passwords or Tokens in Code

Sometimes you don’t need to delete an entire file — you just need to replace a sensitive string, like a hardcoded API key in a config file:

# Create a file containing the strings to replace (one per line)
cat > passwords.txt << 'EOF'
sk-ant-api03-xxx-yyy-zzz
ghp_xxxxxxxxxxxxxxxxxxxx
AKIAIOSFODNN7EXAMPLE
EOF

# BFG replaces all occurrences of those strings with ***REMOVED***
java -jar bfg.jar --replace-text passwords.txt

BFG scans the entire history and precisely replaces those strings. The file structure and all other content remain intact.

Case 4: Remove Entire Directories from History

# Remove the logs/ and node_modules/ directories from the entire history
java -jar bfg.jar --delete-folders logs
java -jar bfg.jar --delete-folders node_modules

# Remove multiple directories at once
java -jar bfg.jar --delete-folders '{logs,tmp,cache,__pycache__}'

Verify and Push to Remote

Verify Results Before Pushing

BFG prints a report after each run — read it carefully to see how many commits were rewritten and which files were processed. You should also verify manually:

# Check if the sensitive file has been removed from history
git log --all --full-history -- .env
# No output = successfully removed

# Compare size before and after
git count-objects -vH

# Search for sensitive strings across the entire history
git log -p --all | grep -n "sk-ant-api03"
# No results = clean

Force Push to Remote

Once you’ve verified everything, push to remote. This step rewrites history on the remote — make sure to notify your entire team beforehand, as everyone’s local repos will be out of sync:

cd my-repo.git

# Push all branches and tags
git push --force

Every team member will need to resync:

# Safest option: re-clone
git clone [email protected]:username/my-repo.git

# Or if you have local changes you want to keep
git fetch --all
git reset --hard origin/main

Rotate Credentials Immediately

This is something many people overlook: even after removing credentials from git history, you must still revoke and regenerate all exposed credentials. GitHub, GitLab, and most platforms have automated crawlers that scan public repos — there’s no way to know whether those credentials were already harvested between when they went public and when you removed them. Revoke first, clean second — in that order:

  • GitHub Token: Settings → Developer settings → Personal access tokens → Delete
  • AWS Key: IAM → Access keys → Deactivate → Delete
  • Anthropic API Key: console.anthropic.com → API Keys → Revoke

Prevent Recurrence with .gitignore

Add these to your .gitignore right away so no one accidentally commits them again:

# .gitignore
.env
.env.local
.env.*.local
*.pem
*.key
*.p12
secrets/
config/database.yml

In the 8-person team I manage, after the accidental .env commit incident, I added a git hook to block commits matching sensitive patterns — combined with a pre-commit check. It hasn’t happened since. Automated prevention at the point of commit is far more effective than catching it in review after the fact.

BFG or git filter-repo?

Since Git 2.24+, there’s also git filter-repo — a Python tool officially recommended by Git as a replacement for git filter-branch, faster than filter-branch and more flexible than BFG. But BFG still holds an advantage in many situations:

  • BFG: Simpler syntax for common use cases (deleting files, replacing text, stripping large blobs). No Python dependency. A single JAR — ready to run immediately.
  • git filter-repo: Better suited for complex operations like restructuring a repo, splitting a subdirectory into its own repo, or filtering by author.

If your goal is simply to remove sensitive files or shrink a repo’s size, BFG remains the fastest and lowest-risk option — installed in 2 minutes, running in seconds.

Share: