- Security
- Git
- Secrets
Leaked Secrets in Git History: Find, Rotate, Prevent
Deleting a committed API key does not remove it from git history. How to find leaked secrets, rotate them first, rewrite history safely and stop the next leak.
· 7 min read · Lina Source LLC
Someone commits a .env file, or pastes a live API key into a config module to test something quickly. A reviewer notices, the key is deleted in the next commit, and everyone moves on. The key is still there. Git stores every version of every file, and anyone who can clone the repository can read the commit that added it.
Hardcoded credentials are tracked as CWE-798. This guide covers what to do when one has already landed in history: find every secret, rotate them before anything else, decide whether rewriting history is worth it, and set up the guardrails that stop the next one.
Why deleting the line is not enough
A commit that removes a secret adds a new snapshot without it. The previous snapshot, with the secret, is still referenced by the history of the branch. git log -p shows it, git checkout of the old commit restores it, and every existing clone and fork already has a copy. Squashing the branch before merge does not help either if the original commits were ever pushed: the remote may still hold them, and anyone who fetched them has them locally.
On a public repository, assume the secret was seen. Automated scrapers watch public pushes for credential patterns, and the window between a push and the first abuse can be very short. On a private repository the exposure is smaller but not zero: every employee, contractor, CI system and integration with read access has it.
Step 1: rotate first
Rotation is the only step that actually removes the risk. Rewriting history hides the secret from future clones; it does nothing about copies that already exist. So revoke and replace the credential before you touch the repository.
Plan the rotation so it does not cause an outage. Many providers let two keys be valid at once: create the new key, deploy it everywhere the old one was used, confirm traffic has moved, then revoke the old one. If the provider allows only one key, accept a short interruption; a few minutes of downtime is a better trade than leaving a known-leaked credential active while you coordinate a perfect cutover.
- Generate a new key in the provider's console and deploy it through your normal configuration path.
- Revoke the old key. Do not just stop using it; an unused valid key is still a valid key.
- Check the provider's audit logs for activity with the old key since the commit date: API calls, new users, changed permissions, unexpected billing.
- If the secret was a database password or signing key, consider what it could have unlocked. A leaked JWT signing secret means any token could have been forged, so invalidate existing sessions.
- Write down what was exposed, for how long and what you changed. You will want it if a customer or auditor asks.
Step 2: find everything that leaked
Where there is one committed secret there are often others. Search the full history, not just the current tree, and include every branch and tag.
# Commits that added or removed a string anywhere in history
git log -p -S "sk_live_" --all
# Regex search across the diffs of all commits
git log -p -G "AKIA[0-9A-Z]{16}" --all
# Files that ever existed at a suspicious path
git log --all --oneline -- .env config/production.json
# Dedicated scanners check hundreds of known credential formats
gitleaks git -v .
trufflehog git file://. --only-verifiedgitleaks and trufflehog are open-source scanners built for this job. They know the formats of common provider keys, and trufflehog can check whether a found credential is still live. Older gitleaks releases use gitleaks detect --source . instead of the git subcommand. Run a scanner once over the full history, then keep it running on new commits. Expect some false positives, such as test fixtures and example keys in documentation. Review each one, then record the confirmed false positives in the scanner's allowlist or baseline file so the next run only shows new findings.
Do not forget the places around the repository: CI logs that echoed environment variables, issue and pull request comments, wiki pages, Docker image layers built from the repository, and gists or pastes shared while debugging.
Step 3: decide whether to rewrite history
Once the secret is rotated, it is useless, so rewriting history is cleanup rather than containment. It is still worth doing when the repository is public, when the secret reveals something beyond itself (an internal hostname, a customer name), or when compliance requires it. It has real costs: every collaborator must re-clone, open pull requests are disrupted, and commit hashes change. Anything that references a commit by hash, such as release notes, deployment records, issue links or pinned dependencies in other repositories, will point at commits that no longer exist on the branch. Announce the rewrite in advance, pick a quiet moment, and merge or close open pull requests first.
git filter-repo is the tool the Git project recommends for this, in place of the older git filter-branch. It works on a fresh clone and removes the origin remote as a safety measure, so you add it back before pushing.
# Start from a fresh clone
git clone [email protected]:acme/api.git api-clean
cd api-clean
# Option A: remove a file from every commit
git filter-repo --invert-paths --path config/production.env
# Option B: replace secret strings everywhere they appear
# replacements.txt contains one rule per line, for example:
# PASTE_THE_OLD_KEY_HERE==>REDACTED
git filter-repo --replace-text ../replacements.txt
# filter-repo removes origin; add it back and force-push
git remote add origin [email protected]:acme/api.git
git push --force --all
git push --force --tagsWhat rewriting cannot do
- It cannot reach existing clones, forks, CI caches or backups. Anyone who fetched before the rewrite keeps the old commits.
- On GitHub, references created for pull requests are read-only and can keep old commits reachable. Removing cached views and those references requires contacting GitHub Support.
- Collaborators who pull or merge from an old clone can push the old history straight back. Ask everyone to re-clone, and protect the branch while they do.
- It does not remove the secret from anywhere else it was copied: logs, tickets, chat messages or built artifacts.
This is why the order matters. Rotate, then clean. A rewritten history with a live key is a false sense of safety. After the push, run your scanner over a fresh clone to confirm the secret is really gone from every branch and tag.
Step 4: prevent the next one
The goal is to make committing a secret hard and noticing one fast. No single control does both, so layer them.
Keep secrets out of the code path
Read credentials from environment variables or a secret manager at runtime, and validate at startup that the ones you need are present. Commit a .env.example with placeholder values and put .env in .gitignore from the first commit. For production, a secret manager such as AWS Secrets Manager, Google Secret Manager, HashiCorp Vault or your platform's encrypted environment settings gives you access control, audit logs and easier rotation.
Block secrets before they are committed
A pre-commit hook catches the mistake on the developer's machine, before it ever reaches the remote. The pre-commit framework makes this a few lines of config that every contributor can install.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: vX.Y.Z # pin to the latest release tag
hooks:
- id: gitleaks
# Install once per clone
# pip install pre-commit
# pre-commit installHooks are local and can be skipped, so run the same scanner in CI on every push as the backstop. On GitHub, enable secret scanning and push protection where your plan supports them; push protection rejects pushes containing recognized provider tokens on the server side.
Reduce the damage of the leaks you miss
- Scope every key to the minimum permissions and, where the provider allows, to specific IPs or referrers.
- Prefer short-lived credentials, such as OIDC federation from CI to your cloud provider, over long-lived static keys.
- Use separate keys per environment, so a leaked test key cannot touch production.
- Keep a rotation runbook for each critical secret, so rotating under pressure is routine rather than improvised.
Where code review fits
Scanners match known formats well; they are weaker at context, such as a password assembled from two constants or a default credential that ships in a config fallback. A review pass over the code catches those. CodeAuditAgent flags hardcoded credentials as CWE-798 with the quoted evidence when it audits a public repository's default branch or a pasted snippet. It reads the current code, not the commit history, so keep a history scanner in place for past commits.
The short version: a committed secret is a leaked secret. Rotate it today, clean the history if it is worth the disruption, and make the next leak fail at the pre-commit hook instead of on a public page.