A secret scanner flags a credential in a commit from fourteen months ago. It was removed in the very next commit, by someone who noticed immediately. The current file is clean.
The instinct is to breathe out. The credential is not in the file anymore, so the problem looks solved. It is not, and the reason is the part of Git that most people never think about: the object it was written into still exists, still resolvable, and still readable by anyone who can fetch the repository.
This post is what actually needs to happen. For whoever just got that alert.
Deleting the line does not delete the object
Git does not diff commits in the sense of storing changes. Each commit is a full snapshot of the tree, and a blob, the object holding a file's content, is content-addressed and immutable. When you remove the credential and commit again, Git creates a new blob for the new content. The old blob, containing the secret, is untouched and still reachable from the earlier commit.
Every clone anyone has taken carries it. Every fork carries it. Every CI cache that pulled the repository has a copy. git log -p on that path, from anyone with read access, shows the exact value.
None of this requires special tooling to find. It requires the same access that reading your repository already requires.
Rotate before you do anything else
This is the part that gets the ordering backwards most often. Teams reach for history rewriting first, as though scrubbing the commit undoes the exposure. It does not, because the secret was live and readable for however long it sat in history before anyone noticed, and rewriting history changes nothing about who already has a copy.
Rotate the credential first, always, regardless of what you do to history afterward. A rotated secret found in an old commit is a historical curiosity. An unrotated one is a live credential, and no amount of history surgery changes that fact while it remains unrotated.
Then check what the credential could do and whether it was used: authentication logs for that key, unusual API calls, anything in the window between when it was committed and when it was rotated.
History rewriting, and what it actually buys
Once rotated, cleaning history has real but narrower value: it stops the credential from continuing to appear in every future clone, in scanner output, and in anyone auditing the repository going forward. It does not undo any past exposure.
Tools for this: git filter-repo is the current recommended tool, faster and safer than the older filter-branch. BFG Repo-Cleaner is a simpler option built specifically for stripping secrets and large files.
git filter-repo --path path/to/file --invert-paths
# or, to replace a specific string everywhere it appears
git filter-repo --replace-text expressions.txt
This rewrites every commit after the point the secret was introduced, which means every commit hash after that point changes. That is disruptive, and it is worth understanding rather than discovering afterward:
Every fork and every local clone is now diverged, and everyone with one needs to re-clone or hard-reset, because their history no longer matches. Pull requests referencing old commits break. Anything pinning a commit SHA, a submodule, a dependency lockfile, a deployment record, now points at a commit that no longer exists in the rewritten history. Tags need to be re-pointed or they still reference the old objects.
Coordinate it: announce a time, have everyone push anything pending, do the rewrite, force-push, and have everyone re-clone rather than try to reconcile their existing checkout.
The objects can survive the rewrite
Rewriting history creates new commits; it does not automatically remove the old, now-unreferenced objects from the repository, and depending on your host, they can survive in caches, in the reflog, or in objects that remain reachable through a reference you did not think to check.
On your own host, run garbage collection with the reflog expired:
git reflog expire --expire=now --all
git gc --prune=now --aggressive
On a hosted platform, you frequently need to contact support to purge cached views, force a garbage collection on their end, and confirm forks are handled, because a rewrite on the origin does nothing to a fork the platform is still serving from its own storage.
Whether the rewrite is worth doing at all
For a private repository with a small, known set of collaborators, rotation alone may be sufficient: the credential is dead, the exposure window is understood, and the disruption of rewriting history may cost more than the residual risk of an old, invalid secret sitting in a commit nobody looks at.
For a public repository, or one with forks you do not control, rewriting is close to pointless on its own: you cannot reach every fork, and the secret is likely already indexed by scanners and archives outside your control regardless of what you do to your own copy.
Prevent the recurrence
Pre-commit secret scanning, so this is caught before it reaches a commit rather than after. Cheap, and it is the control that actually prevents the next one.
Push protection at the host level, where available, which refuses a push containing a recognisable credential pattern before it lands anywhere.
A documented incident procedure for exactly this situation, written once, calmly, rather than improvised under the pressure of an active alert: rotate, assess usage, decide on history rewriting, coordinate if so, notify anyone who needs to know.
The concession
History rewriting is genuinely disruptive to a team's workflow, and for a low-consequence secret in an old commit of a private, tightly held repository, the argument for leaving history alone after rotation is reasonable. Not every finding justifies the coordination cost.
The one step that is never optional is rotation. Everything after that is a judgement about residual risk versus disruption, and it is a judgement worth making deliberately rather than skipping because rewriting sounded like the harder, more thorough option and therefore the right one.
The implication
The moment a secret is committed, the question of whether it is exposed is settled. Everything after that is response, not prevention, and response starts with rotation regardless of what you do to the commit that held it.
If you have an old alert you dismissed because the file looked clean, check whether the credential was ever actually rotated. That is the question that matters, not whether the line is still there.