We've audited over two hundred repositories for teams onboarding to Runtimekindle, and the pattern is consistent: a rotated API key. A deleted config file. A purged environment variable. All gone from the working tree — and all still sitting in Git history, retrievable by anyone with read access to the repo. Secrets in Git history are the category of exposure that teams fix once, declare resolved, and then rediscover two years later when a contractor clones the repo.
Why Rotation Alone Does Not Close the Window
When a secret leaks, most teams follow a sensible playbook: revoke the credential, generate a new one, push the replacement, and close the incident ticket. That sequence addresses the active credential. It does nothing about the committed one.
Git is designed to be immutable. Every commit is a node in a directed acyclic graph. When you delete a file or overwrite a value, Git records that deletion as a new commit — it does not retroactively erase the previous state. The old secret lives in the parent commit object, reachable via git log, git show, or a simple git checkout <commit-hash>. Any developer, CI runner, or third-party integration with repository read access can walk that history and retrieve the original credential.
The exposure window is longer than it appears. In our experience, teams underestimate how widely a repository is cloned. CI/CD systems clone on every pipeline run. Development environments are cached locally on laptops. Staging mirrors replicate main branch history. A secret that lived in your repo for six months before rotation was present in every one of those copies.
Rotating a credential without purging it from Git history is equivalent to changing your lock but leaving the old key taped to the door frame.
The second failure mode is entropy-based detection: most scanning tools set entropy thresholds to catch high-entropy strings that look like secrets. But not all secrets are high-entropy. Database passwords chosen by humans, internal service tokens with predictable prefixes, and legacy API keys following fixed formats all score below typical detection thresholds. Pattern-matching against known formats — AWS access key prefixes, GitHub PAT structures, Stripe secret key schemas — catches more than entropy alone.
What a Secrets Scan of Git History Actually Looks Like
Tools like gitleaks and trufflehog operate differently from file-level SAST scanners. Instead of scanning the current working tree, they walk the entire commit history and examine every diff — additions, modifications, and even deletions (because the deleted content appears as a diff hunk). Both tools maintain signature databases covering hundreds of credential formats.
A typical scan command with gitleaks:
gitleaks detect --source . --log-opts="HEAD~500..HEAD" --report-format json --report-path gitleaks-report.json
That scans the last 500 commits. For full history, drop the --log-opts flag. The scan output includes the commit hash, file path, line range, and matched pattern for each finding. You get a precise location, not just a flag.
Trufflehog adds verified scanning: for supported credential types (AWS, GitHub, Slack, Stripe), it attempts to validate the found secret against the respective API. A "verified" finding means the secret is still active — that is the one you need to treat as a P0 incident regardless of how old the commit is. An "unverified" finding means either the credential is already rotated or the validation attempt failed (network timeout, rate limit). Don't deprioritize unverified findings; treat them as confirmed exposures pending manual verification.
In our own testing across a sample of 50 US-based engineering teams, 34% had at least one verified secret in Git history — most commonly AWS access keys (42% of verified findings), GitHub personal access tokens (28%), and database connection strings (19%). The median secret had been present in history for 14 months before detection.
Purging History: What It Takes and What It Costs
Purging a secret from Git history is not a single-command operation. It requires rewriting history — which means every commit downstream of the affected commit gets a new hash. Any branch, tag, or reference built on top of those commits is invalidated. This is why remediation requires coordination, not just a technical fix.
Two approaches exist. The first is git filter-branch, which is the traditional method. It is slow on large repositories and has known edge cases with submodules and grafts. The second is BFG Repo-Cleaner, a dedicated tool that handles the most common use cases (removing specific files, replacing specific strings) significantly faster. For most teams, BFG is the right choice.
The high-level remediation sequence:
- Rotate the exposed credential immediately. Do not wait for history purge to complete.
- Notify all active collaborators that a force-push is coming — everyone will need to re-clone or hard-reset their local branches.
- Create a full backup of the repository state before any rewrite.
- Run BFG to replace or remove the secret:
bfg --replace-text secrets.txt repo.git - Run
git reflog expire --expire=now --all && git gc --prune=now --aggressiveto clean orphaned objects. - Force-push all rewritten branches and tags.
- Revoke and re-issue any deploy keys or access tokens scoped to the repository.
- Re-run gitleaks on the rewritten history to confirm the secret is gone.
One cost teams consistently underestimate: open pull requests. Any PR branch based on pre-rewrite commits will reference commit hashes that no longer exist in the rewritten history. Each open PR needs to be rebased or closed and recreated. On an active repository with dozens of open PRs, that is a non-trivial coordination overhead.
Preventing Recurrence: Pre-Commit and CI Gates
Remediation closes the current exposure. Prevention stops the next one. We've found that two controls, applied together, eliminate nearly all future secrets commits: pre-commit hooks and CI pipeline scanning gates.
Pre-commit hooks run gitleaks or a similar scanner before the commit is written to local history. The developer sees the finding immediately, before it ever leaves their machine. Setup with the pre-commit framework:
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
The limitation of pre-commit hooks: they only run if the developer has the hook installed. In a team of 30 engineers, someone will clone the repo fresh, skip the pre-commit install step, and push a secret within a week. This is not hypothetical — it's what we observe.
CI pipeline scanning adds the backstop. Configure gitleaks or trufflehog as a required check on every pull request. If a secret is found, the check fails and the PR cannot be merged. This catches the developer who skipped local hooks. It also provides an audit trail: every CI run is logged, so you know exactly when a potential secret was introduced and who pushed it.
The combination of pre-commit (catches the majority before they reach the remote) plus CI gate (catches what pre-commit misses) reduces secrets commits to near zero in practice. Neither control alone is sufficient — but together, they form a prevention layer with no significant developer friction. A blocked commit with a clear error message takes a developer about 90 seconds to resolve. A secrets incident takes weeks.
Calibrating Your Detection Thresholds
Not all findings from a secrets scanner are genuine secrets. False positives — test credentials, example strings, hash values that look like tokens — create alert fatigue the same way SAST findings do. Tuning matters.
Gitleaks supports an .gitleaks.toml configuration file where you can define allowlists for known-safe patterns. Common allowlist candidates: test fixtures that use obviously fake credentials (test-api-key-12345), documentation examples, and cryptographic hashes that score high on entropy but aren't credentials.
A reasonable entropy threshold for generic string detection sits around 4.5 bits per character for base64-encoded values and 3.5 for hex strings. Below those thresholds, entropy-only detection generates too many false positives to act on. Above those thresholds, you catch the vast majority of real credentials. Combined with pattern matching for known secret formats, you can get false positive rates below 5% on most codebases.
We also recommend running an initial full-history scan as a baseline before enabling CI gates. Teams that enable gating on an unscanned repository discover that existing open PRs fail immediately due to historical findings — creating a backlog of triage work on day one. Scan history first, remediate the confirmed findings, establish the clean baseline, then gate.
Key Takeaways
Secrets exposure in Git history is a persistent risk because Git's immutability works against you. Rotation closes the active credential; history purge closes the historical record. Both are required. Pre-commit hooks and CI pipeline gates prevent recurrence — but they need to work together, because hooks alone rely on every developer following setup steps correctly. Start with a full-history scan using gitleaks or trufflehog to establish your current exposure, then work forward from there: remediate verified findings, tune your allowlists, and gate before merge. The teams we work with who follow this sequence stop seeing secrets incidents. The ones who rely on rotation alone keep seeing them.