TL;DR
- Deleting a secret file from the current branch does not remove it from old commits, forks, CI caches, or every clone that already exists.
- git rm, .gitignore, and revert commits help with the present state, not the historical object database.
- You need a history scan to know what leaked, a credential rotation to reduce risk, and a rewrite tool such as BFG to remove the old blobs.
- After cleanup, force-push coordination, collaborator re-clones, and new prevention controls matter as much as the rewrite itself.
Why git keeps secrets long after you delete the file
Git is built to preserve history. That is why it is excellent for collaboration and debugging, and also why it is dangerous for secrets. Once a value is committed, it is stored in the repository object database and can remain reachable through old commits, tags, branches, pull requests, forks, and local clones.
Developers often think in terms of the current branch view: "I deleted the file, so it is gone." Git does not think that way. It preserves snapshots over time. The old snapshot still exists unless you explicitly rewrite history.
If the secret made it into a commit, your problem is no longer "a bad file in the repo." Your problem is "a bad object in history."
Why the common fixes do not work
The most common remediations are useful, but they solve the wrong stage of the problem when history is already contaminated.
git rm secret.envremoves the file from the latest tree, not from historical commits..gitignoreprevents future commits, but it cannot rewrite what is already stored.git revertcreates another commit; it does not erase the original content.- Deleting a file in the GitHub UI only changes the newest snapshot visible in the browser.
These steps are still worth doing, but only as part of a broader incident response sequence that includes rotation and history cleanup.
How to scan git history properly
Before you rewrite anything, you need a clear inventory. Which secret types leaked? How old are they? Did they appear on only one branch or across multiple branches and tags? A proper git scan answers those questions so your remediation is based on facts, not panic.
$ slickenv git scan
→ Scanning 2,341 commits across all branches...
✗ AWS_SECRET_ACCESS_KEY found in 2 commits (oldest: 14 months ago)
✗ DATABASE_URL found in 1 commit (postgres with password)
✗ GITHUB_TOKEN found in 4 commits (oldest: 2 years ago)
3 secret types exposed in git historyThis is where teams discover the uncomfortable part: the leak they remember is often not the only one. Old branches, hotfixes, and abandoned experiments are where historical secrets love to hide.
Scan before you clean because cleanup is disruptive
Rewriting history affects every collaborator and every integration that depends on commit hashes. That is why you want to do it once, with a complete list of what must be removed. If you miss a second secret and have to rewrite again next week, your team will hate the process and trust it less.
How to clean git history safely
The safe order is straightforward even if the operation feels scary:
- Rotate active credentials first so the exposed values stop being useful.
- Create a backup of the repository before rewriting anything.
- Run a purpose-built cleanup tool such as BFG Repo-Cleaner against the offending blobs or patterns.
- Garbage-collect the repository and force-push the rewritten branches and tags.
- Notify every collaborator to re-clone or hard refresh their local history according to your team process.
$ slickenv git clean
Step 1/4: Creating backup... ✓
Step 2/4: Running BFG... ✓ 8 blobs cleaned from history
Step 3/4: Running git gc... ✓
Step 4/4: Ready to force-push
Run next:
git push --force --all
git push --force --tagsHistory rewrite is a coordination event, not just a local command. Plan the communication before you run the force-push.
What to do after cleanup
Cleanup is not finished when BFG finishes. The value has already had exposure, so you need follow-up controls to keep the same failure mode from recurring.
- install a pre-commit hook so new secrets are blocked before they enter history
- run CI secret scans on every push and deployment
- replace raw env values with
slickenv://references where possible - audit old tags, release branches, mirrors, and backups that may still carry the bad history
- document the incident internally so future cleanup starts with rotation first
If you want a concrete incident story rather than the abstract mechanics, read We Found 3 Production Stripe Keys in Our Git History. It covers the human side of the same problem.
For the underlying concepts, GitHub's docs on removing sensitive data from a repository are also worth reviewing before you run a live cleanup.