← All articles

Git History Never Forgets: How to Find and Purge Secrets Committed in the Past

June 15, 2026

The Delete Commit That Doesn't Actually Delete Anything

A developer accidentally commits a .env file containing a live database password. They catch it twenty minutes later, delete the file, and push a follow-up commit. Crisis averted — right?

Wrong. Git is an append-only ledger. Every previous commit is still fully intact and readable. Anyone with a clone of the repository — or access to a platform like GitHub — can check out that earlier commit and read the secret in plain text. The "deletion" commit only removes the file from the current working tree; it has no effect on the repository's history.

This is one of the most common and underestimated secret-exposure patterns in software teams. The credential is invisible in day-to-day development, so it gets forgotten — while quietly waiting for anyone who knows where to look.

Why Git History Is So Valuable to Attackers

When an attacker gains read access to a repository — through a stolen token, a misconfigured public repo, or a phishing link — the first thing many automated tools do is walk the entire commit history looking for high-entropy strings and known credential patterns. The current HEAD is almost never where the gold is; the gold is in the history.

Several things make historical secrets especially dangerous:

  • Long shelf life. A secret committed two years ago and "deleted" a week later has been sitting in history for nearly two years. If it was never rotated, it may still be valid.
  • Forgotten context. Developers who left the team often never rotated credentials they committed. Nobody remembers those secrets exist.
  • Forks and mirrors. If anyone forked or mirrored the repo before you scrubbed it, your secret now lives in a copy you don't control.
  • Archive downloads. GitHub and GitLab let users download ZIP archives of any branch or tag. If the tag predates the deletion commit, the ZIP contains the secret.

How to Audit Your Git History for Exposed Secrets

Before you can remediate, you need to know what's there. Here are practical approaches at different levels of effort.

Quick manual spot-check

For a targeted search across all commits, not just the current checkout:

# Search every version of every file ever committed
git log --all --full-history -- "**/.env" 

# Show the actual content of a specific past commit
git show <commit-sha>:path/to/file

# Search commit diffs for a known string or pattern
git log -p --all -S "AKIA" | head -200

The -S flag (the "pickaxe") finds commits where a given string was added or removed — useful for known prefixes like AWS access key IDs (AKIA) or Stripe keys (sk_live_).

Automated secret scanning across history

Manual grep doesn't scale. Several open-source tools scan Git history with pattern libraries:

  • Gitleaks — scans local repositories against a large ruleset; can be run in CI or ad hoc.
  • TruffleHog — supports entropy analysis in addition to regex patterns; has a Git history mode.
  • detect-secrets — Yelp's tool; useful for baseline-tracking in ongoing development.

A typical Gitleaks invocation against a full history looks like:

gitleaks detect --source . --log-opts="--all" --report-format json --report-path leaks.json

Review the output file carefully — there will be false positives (test fixtures, example keys), but any live-looking credential deserves immediate rotation regardless.

For repositories hosted on GitHub or GitLab, run a free GhostCred scan to get a prioritized, SOC 2 / HIPAA-mapped view of exposed secrets across your repos and .env files in around 60 seconds — without needing to configure a local tool chain first.

Rotating First, Purging Second

The instinct is to rewrite history immediately. Resist it. Rewriting history is irreversible and disruptive; rotating the secret is fast and safe. Do them in this order:

  1. Revoke or rotate the exposed credential right now. Go to the service's dashboard (AWS IAM, Stripe, Twilio, GitHub, etc.) and either revoke the key entirely or generate a new one and deprecate the old one. Don't wait until after the rewrite.
  2. Check access logs. Most cloud providers (AWS CloudTrail, GCP Audit Logs, Stripe's event log) let you verify whether the exposed key was ever used in unexpected ways. Even a few minutes of investigation here can tell you whether you have an active incident versus a theoretical exposure.
  3. Rewrite history to remove the secret from the record. This prevents future clones from ever seeing the credential — important even after rotation, because the old key pattern reveals which services you use, which vaults you should have, and which teams might have similar hygiene problems.

Rewriting Git History: Your Two Main Options

Option 1: git filter-repo (recommended)

git filter-repo is the modern, officially-recommended replacement for git filter-branch. Install it via pip install git-filter-repo, then:

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

# Replace a specific string everywhere in history
git filter-repo --replace-text <(echo "sk_live_actualkey==>REDACTED")

After rewriting, you'll need to force-push all affected branches and tags, and every collaborator must re-clone or hard-reset. Coordinate this — a force-push without warning will confuse anyone mid-PR.

Option 2: BFG Repo-Cleaner

BFG is simpler for the specific use case of removing files or replacing text. It's faster on large repositories but slightly less flexible. If all you need is to nuke a file or swap out a credential string, BFG is a reasonable choice:

bfg --delete-files .env
bfg --replace-text passwords.txt
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force --all

After the Rewrite: What Teams Often Miss

  • Force-push protection rules. Many repos block force pushes on main. You'll need to temporarily disable branch protection, force-push, then re-enable it. Don't leave protection off.
  • Cached versions on the hosting platform. GitHub caches commit data. After a force-push, open a support ticket to request a cache flush if the old commit SHAs remain accessible via the web UI.
  • Forks. You cannot rewrite forks you don't own. If the repo was ever public or the fork is external, treat the secret as permanently compromised and rely entirely on rotation — not on history rewriting — as your actual security control.
  • IDE and local clones. Developer machines that cloned before the rewrite still have the old objects in .git/objects. Ask every contributor to delete and re-clone rather than pull.

Prevention: Stop Secrets from Reaching History in the First Place

Remediation is painful. Prevention is a config file.

  • Pre-commit hooks. Tools like pre-commit with the detect-secrets hook or gitleaks protect reject commits containing high-entropy strings before they ever reach the remote.
  • A comprehensive .gitignore. Every project should ignore .env, *.pem, *_rsa, credentials.json, and similar files by default — not just the project-specific ones you remember today.
  • Secret manager references, not values. Applications should load secrets from AWS Secrets Manager, HashiCorp Vault, or equivalent at runtime. The codebase should only ever contain references (ARNs, path names) — never the values.
  • Periodic history scans in CI. Run a secret scanner against the full history on a schedule (weekly is reasonable for active repos), not just on new commits. This catches secrets that slipped through before you had the pre-commit hook in place.

Key Takeaways

  • Deleting a file in Git does not remove it from history — the secret remains readable in every prior commit.
  • Always rotate a compromised credential before rewriting history; rotation is your real security control.
  • Use git filter-repo or BFG Repo-Cleaner to purge sensitive content, then force-push and coordinate re-clones.
  • If the repo was ever public or forked externally, treat rotation as your only reliable defense.
  • Prevent recurrence with pre-commit hooks, a solid .gitignore, and periodic automated history scans.

See what's exposed in your own code.

Run a free scan