How to Remove Secrets from Git History

Share
How-To Guide

How to Remove Secrets from Git History

Cleaning up after accidental commits

TL;DR

TL;DR: First, rotate the compromised credential. Then use BFG Repo Cleaner to remove the secret from all commits. Force push the cleaned history. Alert team members to re-clone. Remember: if the secret was ever pushed to a public repo, consider it compromised regardless of cleanup.

Critical First Step: Rotate the Credential

Always rotate (replace) the exposed secret BEFORE cleaning git history. Once a secret is pushed, especially to a public repo, assume it's been scraped. Cleaning history prevents future exposure but doesn't undo potential current compromise.

BFG is faster and simpler than git filter-branch. It's designed specifically for removing unwanted data.

1

Install BFG

# macOS
brew install bfg

# Or download the JAR file
# https://rtyley.github.io/bfg-repo-cleaner/
2

Clone a fresh copy

# Clone with --mirror for full history
git clone --mirror git@github.com:you/your-repo.git
3

Create a file with secrets to remove

# secrets.txt - one secret per line
sk_live_abc123xxxxx
AKIA1234567890xxxxx
ghp_xxxxxxxxxxxx
4

Run BFG to remove secrets

# Remove specific strings from all history
bfg --replace-text secrets.txt your-repo.git

# Or remove entire files
bfg --delete-files .env your-repo.git
bfg --delete-files "*.pem" your-repo.git
5

Clean up and push

cd your-repo.git

# Clean up the repo
git reflog expire --expire=now --all
git gc --prune=now --aggressive

# Force push
git push --force

Method 2: git filter-repo

A newer, Python-based alternative to filter-branch:

# Install
pip install git-filter-repo

# Remove a file from all history
git filter-repo --invert-paths --path .env

# Replace text in all files
git filter-repo --replace-text expressions.txt

Create expressions.txt:

# Format: literal:old==>new or regex:pattern==>replacement
literal:sk_live_abc123==>***REMOVED***
regex:sk_live_[a-zA-Z0-9]+==>[REDACTED]

Method 3: git filter-branch (Legacy)

The traditional method, slower but works without additional tools:

# Remove a specific file from all commits
git filter-branch --force --index-filter \
  "git rm --cached --ignore-unmatch .env" \
  --prune-empty --tag-name-filter cat -- --all

# Clean up
git reflog expire --expire=now --all
git gc --prune=now --aggressive

# Force push all branches
git push origin --force --all
git push origin --force --tags

Warning: Force Push Required

Rewriting history requires force pushing. This will cause issues for anyone who has cloned the repo. All team members will need to re-clone or carefully rebase their local copies.

After Cleaning: Team Coordination

  1. Notify your team that history was rewritten
  2. Ask everyone to delete and re-clone the repository
  3. Delete any forks that might still contain the secret
  4. Check CI/CD caches for cached copies
  5. Request GitHub to clear caches if it was a public repo

For GitHub: Contact Support

If secrets were in a public repo, contact GitHub Support to clear their caches:

  1. Go to GitHub Support
  2. Select "Sensitive data removal"
  3. Provide the repository and commit details

Verify Secrets Are Removed

# Search all history for the secret
git log -p --all -S "sk_live_abc123" --source

# If no results, the secret is removed from all commits

# Also check for the file
git log --all --full-history -- .env

Do I really need to clean history if the repo is private?

It's still recommended. Private repos can become public, team members change, and the secret sits in history indefinitely. However, rotating the credential is the priority. History cleaning can be secondary for private repos.

Will this affect open pull requests?

Yes, open PRs will have conflicts after history rewrite. They'll need to be rebased or closed and reopened against the new history.

Can someone still see the secret from before I cleaned it?

If anyone cloned the repo before cleanup, they have the secret in their local history. GitHub and other hosts may also have cached copies. This is why rotating the credential is essential.

Related guides:How to Rotate API Keys · How to Gitignore Secrets · How to Enable Secret Scanning

How-To Guides

How to Remove Secrets from Git History