Every developer has done it at least once: committed an API key, a database password, or a `.env` file, then deleted it in the next commit and assumed the problem was solved. It wasn't. Git doesn't forget. Deleting a file in a new commit just hides the secret from the current view — it's still sitting in your repository's history, waiting for anyone with clone access to find it.
If your repo has ever been public, forked, mirrored, or shared with a contractor who left the company, you need to assume someone already has that history. Here's how to actually go find what's leaking before they do.
Why Deleted Commits Don't Delete Secrets
Git is designed around immutability. Every commit references a snapshot of your entire tree, and those snapshots are chained together by hash. When you delete a line or a file and commit the change, Git creates a new snapshot — but the old one still exists in the `.git` object database unless you explicitly rewrite history and garbage-collect.
This means:
- `git log -p` can reveal content from any point in the repo's life, even if it's gone from the current branch.
- Forks and clones retain full history, including commits you think you've "removed."
- Secrets in squashed or rebased branches can still linger in reflogs or orphaned commits until a real `gc --prune` runs.
- CI/CD caches and backup mirrors often hold older `.git` folders than your live repo.
Step 1: Search Your History Manually (Quick and Dirty)
Before reaching for tooling, a few native Git commands can turn up low-hanging fruit fast.
Search commit content for common secret patterns
git log -p | grep -i -E "api[_-]?key|secret|password|token|aws_access_key_id"
This pipes the full patch history through grep, looking for common naming conventions. It's noisy, but it's a fast first pass on smaller repos.
Search for specific filenames that should never have been committed
git log --all --full-history -- "*.env" "*.pem" "*id_rsa*" "*.p12" "config/secrets.yml"
This finds every commit that ever touched a file matching these patterns — even if the file no longer exists in HEAD.
Check the reflog and dangling commits
git reflog show --all
git fsck --unreachable --no-reflogs
Dangling commits from abandoned branches or interactive rebases can still contain unpruned secrets that `git log` on your current branch won't show you.
Step 2: Use Purpose-Built Secret Scanners
Manual grepping misses a lot — rotated key formats, base64-encoded tokens, multiline PEM blocks. For real coverage you want a tool with built-in detection rules.
TruffleHog
TruffleHog scans full git history (not just the current branch) and uses both regex and entropy analysis to catch high-randomness strings that look like keys even without a recognizable prefix.
trufflehog git file://./your-repo --since-commit HEAD~500 --only-verified
The `--only-verified` flag is worth using first — it actively checks found credentials against the relevant provider APIs (AWS, GitHub, Slack, etc.) to confirm they're still live, cutting down false positives dramatically.
Gitleaks
Gitleaks is fast, has a solid default ruleset covering 100+ secret types, and plugs easily into CI.
gitleaks detect --source . --report-path gitleaks-report.json
Run it against the full history, not just a diff:
gitleaks detect --source . --log-opts="--all"
GitHub's native secret scanning
If your repo lives on GitHub (public or with Advanced Security enabled on private repos), GitHub automatically scans for known secret patterns from 100+ partners and will alert you — and sometimes automatically revoke the credential — when a match is found. It's not a substitute for scanning history yourself, since it mostly watches new pushes, but it's a useful second layer.
Step 3: Know What You're Actually Looking For
A checklist of what typically shows up in leaked git history:
- Cloud provider keys — AWS access key/secret pairs, GCP service account JSON, Azure connection strings
- Database connection URIs with embedded credentials (`postgres://user:pass@host`)
- Third-party API keys — Stripe, SendGrid, Twilio, OpenAI
- Private SSH and TLS keys (`id_rsa`, `.pem`, `.key` files)
- Hardcoded JWT signing secrets
- OAuth client secrets
- Internal webhook URLs with tokens embedded in the query string
- `.env` or `.env.local` files committed during early setup and never removed
Step 4: Once You Find a Secret — Rotate First, Clean History Second
This order matters more than people think. A common mistake is spending hours rewriting git history before rotating the actual credential. If the secret was ever pushed to a remote, assume it's compromised the moment you find it.
- Revoke and rotate the credential immediately at the source (AWS IAM console, Stripe dashboard, database user, etc.)
- Update deployed services with the new credential via your secrets manager or environment config
- Then remove the secret from history using `git filter-repo` (the modern, faster replacement for `filter-branch`) or BFG Repo-Cleaner
- Force-push the cleaned history and have all collaborators re-clone rather than pull
- Invalidate any cached forks or mirrors where possible — contact GitHub support if a public fork retains the leaked data
Example with BFG:
bfg --delete-files id_rsa
bfg --replace-text passwords.txt
git reflog expire --expire=now --all && git gc --prune=now --aggressive
Step 5: Prevent the Next Leak
- Add a `.gitignore` entry for `.env`, `*.pem`, `*.key` before the first commit, not after
- Install `gitleaks` or `git-secrets` as a pre-commit hook so leaks are caught before they're pushed
- Use a secrets manager (Vault, AWS Secrets Manager, Doppler) instead of config files entirely
- Run gitleaks or trufflehog as a required check in CI on every pull request
- Rotate credentials on a schedule regardless of whether a leak is suspected
Secrets in Git Are Only Half the Picture
Finding and rotating leaked credentials closes one door, but attackers rarely rely on a single weakness. A repo audit is worth pairing with a check of how your live site is actually configured — missing security headers, a misconfigured CSP, weak cookie flags, or an expired SSL certificate can expose just as much risk as a stray API key, and they're often overlooked because they live outside the codebase entirely.
This is where a tool like WebSentry is useful as a complementary check: it scans your live site across SSL, HTTP headers, CSP, cookies, DNS, and CORS configuration, and gives you an A–F grade so you can see at a glance where the gaps are. Agencies managing multiple client sites often run WebSentry alongside a git history audit — one covers what's in your codebase, the other covers what's exposed on the wire.
If you've just finished rotating a leaked key, it's also worth checking whether that key or its associated service is referenced anywhere in your site's public headers or client-side JS — WebSentry's CORS and header checks can flag configurations that inadvertently expose internal endpoints tied to the credentials you just rotated.
Run a free scan at websentry.dev and get your site's security grade in under a minute — it's a fast way to confirm that fixing your git history hasn't left other doors open.
Check your own site
Run a free security scan and see if your site has the issues covered in this article. Results in under 30 seconds.
