DEV Community

Karam Khoury
Karam Khoury

Posted on

Oops, I Committed a Secret: A Calm Guide to Scrubbing Git History

You just finished a great feature. You're in the flow.

git add .
git commit -m "Fixed the bug!"
git push
Enter fullscreen mode Exit fullscreen mode

Then you see it, staring back from the GitHub UI: your live API key. ๐Ÿ’€

We've all been there. Here's the calm, correct order of operations โ€” from "quick save" for a mistake you caught instantly, to the deep clean for a secret that's been buried in history for months.

Step 0: Rotate the secret first (yes, before anything else)

This is the step people skip, and it's the only one that's non-negotiable. The moment a secret hits a remote, assume it's compromised. Bots scrape public GitHub for credentials within seconds.

So before you touch history:

  1. Revoke / rotate the leaked key, token, or password at its source (AWS, Stripe, your DB, wherever).
  2. Then clean the repo.

Cleaning history without rotating is like changing the locks after posting the old key online. Rotate first, always.

Case 1: You caught it before pushing (the quick save)

If the secret is only in your last, un-pushed commit, this is trivial:

# Stop tracking the file and ignore it going forward
git rm --cached .env
echo ".env" >> .gitignore

# Fold the fix into the last commit
git commit --amend --no-edit
Enter fullscreen mode Exit fullscreen mode

Done. Nothing left the building. (Still rotate the key if it was ever real.)

Case 2: It's deep in history, or already pushed (the deep clean)

Once the secret is several commits back โ€” or on the remote โ€” you need to rewrite history. The modern, recommended tool is git filter-repo (Git's own docs now steer you here instead of the old filter-branch).

# Install (Python)
pip install git-filter-repo

# Option A: remove a whole file from ALL history
git filter-repo --path config/secrets.yml --invert-paths

# Option B: redact a specific string everywhere it appears
echo 'AKIAIOSFODNN7EXAMPLE==>REDACTED' > replacements.txt
git filter-repo --replace-text replacements.txt
Enter fullscreen mode Exit fullscreen mode

Prefer BFG Repo-Cleaner if you like a faster, simpler tool for the common cases:

bfg --delete-files id_rsa
bfg --replace-text passwords.txt

# Expire the old refs and garbage-collect
git reflog expire --expire=now --all && git gc --prune=now --aggressive
Enter fullscreen mode Exit fullscreen mode

Then: force-push and warn your team

History rewriting changes commit hashes, so you must overwrite the remote:

git push --force
Enter fullscreen mode Exit fullscreen mode

โš ๏ธ This rewrites shared history. Tell every collaborator to re-clone or hard-reset โ€” if someone merges an old local branch, the secret comes right back. And remember: on GitHub, the old commit can still be reachable via its hash / cached views for a while, which is exactly why Step 0 (rotation) is what actually protects you.

Prevention: never live on the edge again

  • .gitignore first. Add .env, *.pem, secrets.* before the first commit.
  • Environment variables / a secret manager (Azure Key Vault, AWS Secrets Manager, Doppler) โ€” secrets never touch the repo.
  • A pre-commit scanner so a secret is blocked before it's committed:
# gitleaks catches secrets in staged changes
brew install gitleaks
gitleaks protect --staged
Enter fullscreen mode Exit fullscreen mode

Wire that into a pre-commit hook and the "cold sweat" moment mostly disappears.

TL;DR

  1. Rotate the secret (non-negotiable, do it first).
  2. Un-pushed? git commit --amend.
  3. In history? git filter-repo or BFG, then git push --force.
  4. Tell your team to re-clone.
  5. Add a pre-commit scanner so it never happens again.

How do you handle secrets in your workflow โ€” pre-commit hooks, or living life on the edge? ๐Ÿ‘‡


Written by Karam Khoury โ€” Lead Software Engineer (.NET & Azure) with 14+ years building secure, scalable fintech systems. More at karamkhoury.me.

Top comments (0)