The Real Problem#
I recently realized I had pushed a .env file with API credentials to a private repository. Even though it was private, that’s no excuse. A compromised access, a repository that becomes public, or simply a security audit would have exposed my tokens. I learned that I can’t rely on deleting files in subsequent commits—Git keeps all the history.
Why git-filter-repo#
Years ago I would have used git filter-branch, but it’s slow and error-prone. git-filter-repo is the modern tool recommended by Git maintainers. It’s fast, precise, and has better options for this job.
Installation#
On my Debian server:
apt-get install git-filter-repoOr with pip:
pip3 install git-filter-repoStep 1: Identify the Damage#
First, I need to know which commits contain credentials. I search for suspicious patterns:
git log --all --oneline | head -20
git log -p --all | grep -i "token\|password\|api_key" | head -10I also review what sensitive files are in the history:
git log --all --full-history -- ".env"
git log --all --full-history -- "config.yml"In my case, I found that .env had been committed 3 times and a credentials.json file in 2.
Step 2: Make a Backup#
I never do this without a backup:
cd /path/to/my/repo
git clone --mirror . backup-mirror.gitIf something goes wrong, I have a complete copy of the repository with all its history.
Step 3: Clean Specific Files#
I run git-filter-repo to remove the sensitive files from the entire history:
git-filter-repo --invert-paths --path .env --path credentials.jsonThe --invert-paths parameter means it keeps everything EXCEPT what I specify. This is the opposite of what it seems, but it works perfectly.
The process takes a few seconds and rewrites all the history. At the end, I see:
Processed 47 commits
New history has 47 commitsStep 4: Force Push (Carefully)#
Since I’ve rewritten the history, I need to do a force push. On a home server where I’m the only dev, it’s safe:
git push origin --force --all
git push origin --force --tagsIf it’s a shared repository, I coordinate with the team so everyone does git reset --hard origin/main afterward.
Step 5: Rotate Compromised Credentials#
The credentials that were in Git are no longer there, but I must assume they were compromised. I rotate all tokens:
- API Keys: I revoke the old ones in the API panel and generate new ones
- Passwords: I change the password for any service that used those credentials
- Database Tokens: I regenerate database credentials
- SSH Keys: If they were exposed, I generate new pairs
I document these changes in a private file (not in Git):
2026-05-19 - Rotación de credenciales post-exposición
- API key antigua: revocada, nueva generada
- Token DB: regenerado
- Contraseña servicio X: cambiadaStep 6: Future Prevention#
I add rules to .gitignore (now it’s clean):
.env
.env.local
credentials.json
secrets/I also configure a pre-commit hook to detect dangerous patterns:
git config core.hooksPath .githooksAnd I create .githooks/pre-commit:
#!/bin/bash
if git diff --cached | grep -iE "(password|token|api_key|secret)" && \
! git diff --cached | grep ".gitignore"; then
echo "⚠️ Posible credencial detectada. Abortando."
exit 1
fiConclusion#
The cleanup takes 10 minutes. Credential rotation, another while. But it’s time well spent. On a home server, I have no excuse for being negligent with secrets. Now I use environment variables and local files that never go into Git.
Lesson learned: Secrets never go in version control, not even “private”. Period.
Recommended Equipment#
- YubiKey 5 NFC — Physical security key for SSH and GitLab — eliminates the risk of stolen tokens
- 2TB External Hard Drive — Backup repositories before destructive operations like git-filter-repo
Affiliate links. No extra cost to you.