Skip to content

Bits of .NET

Daily micro-tips for C#, SQL, performance, and scalable backend engineering.

  • Asp.Net Core
  • C#
  • SQL
  • JavaScript
  • CSS
  • About
  • ErcanOPAK.com
  • No Access
  • Privacy Policy
Git

Git: Find and Remove Sensitive Data from Entire Repository History

- 03.02.26 - ErcanOPAK

Accidentally committed API keys or passwords? Deleting the file in a new commit doesn’t remove it from history. Here’s how to purge it completely.

⚠️ The Problem:

# You committed config.json with API key
git add config.json
git commit -m "Add config"
git push

# Then realized mistake and deleted it
git rm config.json
git commit -m "Remove sensitive data"
git push

# ❌ API key still in history!
# Anyone can:
git checkout abc1234~1  # Go to commit before deletion
cat config.json         # See your API key!

The Nuclear Option – BFG Repo-Cleaner:

# Install BFG
# Mac: brew install bfg
# Windows: Download from https://rtyley.github.io/bfg-repo-cleaner/

# Clone fresh copy
git clone --mirror https://github.com/yourname/yourrepo.git

# Remove file from entire history
bfg --delete-files config.json yourrepo.git

# Or remove text pattern (API keys, passwords)
bfg --replace-text passwords.txt yourrepo.git

# passwords.txt contains:
# API_KEY=abc123  ==> API_KEY=***REMOVED***
# PASSWORD=secret ==> PASSWORD=***REMOVED***

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

# Force push (rewrites history!)
git push --force

# ⚠️ Everyone must re-clone repository after this

Manual Method – Git Filter-Branch:

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

# Explanation:
# --index-filter: Run on staging area (faster than --tree-filter)
# git rm --cached: Remove from index
# --ignore-unmatch: Don't fail if file doesn't exist in that commit
# --prune-empty: Remove commits that become empty
# --all: Apply to all branches

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

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

Remove Specific Text Pattern:

# Replace API key in all files throughout history
git filter-branch --tree-filter \
  "find . -type f -exec sed -i 's/sk-abc123xyz456/***REDACTED***/g' {} +" \
  HEAD

# This finds ALL occurrences of API key and replaces with ***REDACTED***

Modern Alternative – Git Filter-Repo:

# Install
pip install git-filter-repo

# Remove file
git filter-repo --path config.json --invert-paths

# Remove text pattern
git filter-repo --replace-text <(echo 'API_KEY=abc123==>API_KEY=***REMOVED***')

# Much faster than filter-branch!

Check What Will Be Removed:

# See all commits that touched the file
git log --all --full-history -- config.json

# See file size throughout history
git rev-list --objects --all | grep config.json

# List largest files in history
git rev-list --objects --all | \
  git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | \
  sed -n 's/^blob //p' | \
  sort -k2 -n -r | \
  head -20

# This shows what's eating repository space

After Purging – Team Communication:

# Email team:
"Repository history has been rewritten to remove sensitive data.

Everyone must:
1. Commit and push any local changes
2. Delete local repository
3. Clone fresh copy:
   git clone https://github.com/yourname/yourrepo.git

Do NOT merge or pull - you'll bring back the removed data!"

# Team members run:
cd ~/projects
rm -rf yourrepo
git clone https://github.com/yourname/yourrepo.git

Verify Removal:

# Search entire history for sensitive string
git log --all --source -S 'API_KEY=abc123'

# If returns nothing = successfully removed

# Or check specific file doesn't exist anywhere
git log --all --full-history -- config.json

# Should return nothing if file removed

Prevention – .gitignore:

# Add to .gitignore BEFORE committing
echo "config.json" >> .gitignore
echo ".env" >> .gitignore
echo "*.key" >> .gitignore
echo "secrets/" >> .gitignore

git add .gitignore
git commit -m "Add .gitignore"

# If you already staged sensitive file:
git rm --cached config.json  # Remove from index, keep file locally

Template .gitignore for Secrets:

# Secrets
.env
.env.local
*.key
*.pem
config.json
secrets.yml
credentials.txt

# Cloud provider
.aws/credentials
.gcloud/key.json
azure.json

# IDEs
.idea/
.vscode/settings.json

# OS
.DS_Store
Thumbs.db

Emergency: Invalidate Leaked Secrets Immediately:

If API key was exposed:
1. Rotate/delete API key FIRST (before cleaning history)
2. Then clean Git history
3. Generate new API key
4. Update production secrets

Even after removing from Git, assume key is compromised!
Attackers may have already cloned repository.

Related posts:

Undo Local Changes Safely

Git: Use git blame -L to See Who Changed Specific Lines

The Hidden Cost of Large Commits (It’s Not Code Review)

Post Views: 3

Post navigation

Git: Undo Last Commit Without Losing Changes (3 Different Scenarios)
.NET Core: Use IMemoryCache for Lightning-Fast Data Access Without Redis

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

March 2026
M T W T F S S
 1
2345678
9101112131415
16171819202122
23242526272829
3031  
« Feb    

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (938)
  • How to add default value for Entity Framework migrations for DateTime and Bool (836)
  • Get the First and Last Word from a String or Sentence in SQL (828)
  • How to select distinct rows in a datatable in C# (801)
  • How to make theater mode the default for Youtube (736)
  • Add Constraint to SQL Table to ensure email contains @ (575)
  • How to enable, disable and check if Service Broker is enabled on a database in SQL Server (554)
  • Average of all values in a column that are not zero in SQL (523)
  • How to use Map Mode for Vertical Scroll Mode in Visual Studio (477)
  • Find numbers with more than two decimal places in SQL (441)

Recent Posts

  • C#: Saving Memory with yield return (Lazy Streams)
  • C#: Why Records are Better Than Classes for Data DTOs
  • C#: Creating Strings Without Memory Pressure with String.Create
  • SQL: Protecting Sensitive Data with Dynamic Data Masking
  • SQL: Writing Readable Queries with Common Table Expressions (CTE)
  • .NET Core: Handling Errors Gracefully with Middleware
  • .NET Core: Mastering Service Lifetimes (A Visual Guide)
  • Git: Surgical Stashing – Don’t Save Everything!
  • Git: Writing Commits That Your Future Self Won’t Hate
  • Ajax: Improving Perceived Speed with Skeleton Screens

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (938)
  • How to add default value for Entity Framework migrations for DateTime and Bool (836)
  • Get the First and Last Word from a String or Sentence in SQL (828)
  • How to select distinct rows in a datatable in C# (801)
  • How to make theater mode the default for Youtube (736)

Recent Posts

  • C#: Saving Memory with yield return (Lazy Streams)
  • C#: Why Records are Better Than Classes for Data DTOs
  • C#: Creating Strings Without Memory Pressure with String.Create
  • SQL: Protecting Sensitive Data with Dynamic Data Masking
  • SQL: Writing Readable Queries with Common Table Expressions (CTE)

Social

  • ErcanOPAK.com
  • GoodReads
  • LetterBoxD
  • Linkedin
  • The Blog
  • Twitter
© 2026 Bits of .NET | Built with Xblog Plus free WordPress theme by wpthemespace.com