How to Scrub Leaked Secrets from Git History: A Complete Guide
Accidentally committing a secret (API key, password, or token) is a rite of passage for developers. However, simply deleting the secret in a new commit isn't enough—it remains in your Git history forever.
To truly secure your repository, you must rewrite the timeline. This guide covers the three best ways to do it, depending on your needs.
Step 0: Revoke the Token (Non-Negotiable)
Before you start fixing Git, assume the secret is already compromised.
- Revoke/Delete the secret at the source (Bitbucket, AWS, Stripe, etc.).
- Generate a new one, but do not hardcode it. Keep it aside for your
.envfile.
Method 1: The "Professional" Choice (git-filter-repo)
This is the officially recommended tool for rewriting history. It is precise and removes the entire history of a specific file.
1. Installation
On modern Linux (Ubuntu 24.04+), avoid pip. Use apt:
sudo apt update && sudo apt install git-filter-repo
2. Run the Scrub
Go to your project root and run:
git filter-repo --path path/to/your/file.js --invert-paths
- Result: The file and its entire history are wiped from the repository.
3. Restore Cleanly
Because the file is now gone, recreate it using Environment Variables:
const TOKEN = env.BITBUCKET_TOKEN; // Safe
Then commit:
git add .
git commit -m "chore: restore file with safe env variables"
Method 2: The "Fast Text Replacer" (BFG Repo-Cleaner)
If you want to keep your file but simply want to replace every instance of the secret string with ***REMOVED*** across all commits, BFG is the fastest tool.
1. Setup
Download the bfg.jar and create a file named passwords.txt. Put your secret strings inside it (one per line).
2. Run the Replacer
java -jar bfg.jar --replace-text passwords.txt .
- Result: Your history remains intact, but your secrets are replaced by a placeholder string in every old commit.
Method 3: The "Total Reset" (Solo Developer Shortcut)
If you are the only developer and you don't care about keeping your commit history (messages/dates), this is the simplest way to "nuke" the history and start fresh.
1. Remove Git
rm -rf .git
2. Re-initialize
Make sure your files are clean (secrets moved to .env), then:
git init
git add .
git commit -m "Initial clean commit"
Step 2: Overwriting the Remote (The Force Push)
Regardless of the method used, your local history now contradicts the "dirty" history on GitHub or Bitbucket. You must force the remote to accept your clean version.
Note: If you used Method 1 or 3, you must re-add your remote first:
git remote add origin https://github.com/username/repo.git
Then Force Push:
git push origin --force --all
git push origin --force --tags
Step 3: Syncing Other Machines
If you have this repo on another machine, DO NOT PULL. If you pull, you will merge the dirty history back into your clean history.
On the other machine, run a Hard Reset:
# 1. Download the new clean history
git fetch origin
# 2. Force the local branch to match the clean remote
git reset --hard origin/main
# 3. Clean up the internal cache
git gc --prune=now --aggressive
Final Security Check & Prevention
1. Use a .gitignore
Never let your .env file reach Git. Create a .gitignore in your root:
# .gitignore
.env
node_modules/
dist/
2. Architecture: Passing env
Ensure your functions are designed to receive configuration from the environment, especially in serverless environments like Cloudflare Workers:
// bitbucket-poll.js
export async function pollBitbucket(env) {
const token = env.BITBUCKET_TOKEN;
// ...
}
3. Use Secret Scanning
Enable "Secret Scanning" in your GitHub/Bitbucket settings. These platforms will now block pushes if they detect a known API key format, saving you from having to rewrite history ever again.
Summary:
- Method 1 (
git-filter-repo) is best for deleting specific files. - Method 2 (BFG) is best for replacing specific strings.
- Method 3 (Total Reset) is best for starting fresh on solo projects.