skip to content

A credential was committed to your Git repository months ago — what do you do?

level: seniorimportance: must knowfreq 55%

answer

  1. the rewrite is not the first step
  2. assume it was read the day it landed
  3. search across all refs, not one branch
  4. a string filter, not only a path filter
  5. copies you do not control still exist

basics

~20 s

Treat the credential as compromised and invalidate it first — a history rewrite cannot un-leak it. Then purge it with git filter-repo --replace-text on a fresh clone, force-push every rewritten ref, and have everyone re-clone.

solid answer

~50 s

Order matters. The value has been readable in every clone, fetch and backup since the day it was pushed, so step one is to invalidate it wherever it grants access; nothing you do to Git changes that. Step two is the cleanup: clone fresh, write a replacement file listing the secret (`git filter-repo --replace-text expressions.txt` substitutes each match with `***REMOVED***` by default), and let filter-repo rewrite every commit, branch and tag, prune emptied commits, expire reflogs and gc. Step three is publication: re-add the remote and force-push every rewritten ref, then tell the team to re-clone rather than pull, because their histories are now parallel. Step four is honesty about limits — forks, mirrors, backups and CI caches you do not control still hold the original objects. Then close the loop so it cannot recur.

code

text · 3 lines
text
AKIAIOSFODNN7EXAMPLE
literal:hunter2==>***REMOVED***
regex:api_key\s*=\s*"[^"]+"==>api_key="REDACTED"

go deeper

for a junior

Know that a committed credential must be treated as compromised and reported immediately, and that deleting the line in a new commit does not remove it.

for a middle

Explain the purge mechanics: a fresh clone, a text or path filter, all refs rewritten, empty commits pruned, reflogs expired and objects collected, then a forced publish.

for a senior

Lead with invalidation, scope the exposure across all refs, plan the team coordination, and name the copies a rewrite cannot reach. Show you verify the purge rather than assume it.

for a principal

Own the decision and the aftermath: whether a repo-wide rewrite is worth breaking every clone and stored SHA, who is accountable for invalidation, and what control stops the next one before it is published.

## Reframe the question before answering it Interviewers ask this to see whether you jump straight to `filter-repo`. The correct first move is not a Git command: from the moment the commit was pushed, the value has been present in every clone, every fetch, every backup and every CI checkout. Anyone who ever had read access has had the value. Rewriting history changes what *future* readers of this repository see; it changes nothing about who already read it. So the first action is to make the value worthless — revoke, rotate or disable it — and only then clean the repository. Candidates who lead with the rewrite are optimising the wrong risk. ## Scoping the damage Before rewriting, find every place the value appears. It is rarely one line in one file: the same value tends to appear in a test fixture, a sample config, a commit message, and a file that was later renamed. Useful passes: - `git log --all -S'<secret>' --oneline` — the pickaxe search across all refs, listing commits where the number of occurrences of that string changed. - `git log --all --oneline -- <path>` — commits touching a suspected file. - `git rev-list --objects --all` — an exhaustive listing of reachable objects and paths. Also decide whether you are removing a *file* or a *string*. A file that exists only to hold credentials is a path filter; a value pasted into source that must otherwise survive is a text replacement. ## Doing the rewrite Work in a fresh clone, which `git filter-repo` insists on by default so an intact original remains. For a string, write an expressions file — one pattern per line, optionally with `==>` and a replacement, defaulting to `***REMOVED***` — and run `git filter-repo --replace-text expressions.txt`. Every commit that contained the value is rebuilt without it; every downstream commit is rebuilt because its parent changed; tags and other branches are rewritten too; commits emptied by the change are pruned; reflogs are expired and objects garbage-collected; and the `origin` remote is removed so publishing is deliberate. Verify before publishing: `git log --all -S'<secret>'` should return nothing, and spot-check that the surrounding code still makes sense — a blind replacement can corrupt a file that legitimately contained a similar-looking string. ## Publishing and the human cost Re-add the remote and force-push the rewritten refs — all of them, including tags, not just the default branch. Then tell the team explicitly: the safest instruction is "re-clone", because a pull after an upstream rewrite tries to reconcile two parallel histories and can quietly reintroduce the old commits. Anyone with unpushed work needs to move it onto the new history rather than merge into it. Also enumerate what you cannot reach: forks and mirrors you do not own, backups and archives, CI caches, container images built from the old checkout, and any copy on a machine that is offline. Say this out loud in the interview — it is the difference between "I ran the tool" and "I understand the exposure". ## Closing the loop A rewrite is remediation, not prevention. The follow-through is making the same commit impossible or immediately visible: keep credential material out of the tree entirely, ignore the paths where it tends to land, and add a check that runs before the content can be published. The design of credential storage and rotation is a separate discipline; what belongs to Git here is the purge mechanism and the coordination cost it imposes. ## The summary an interviewer wants Invalidate first; scope with the pickaxe across all refs; rewrite on a fresh clone with `git filter-repo`; verify with the same search; force-push every ref; instruct re-clones; state which copies remain out of reach; then prevent recurrence. Delivering that order, unprompted, is the whole answer.

  • How do you find every commit containing the value, including on branches you never checked out?
    Use the pickaxe across all refs: `git log --all -S'<value>' --oneline` lists commits where the occurrence count of that string changed. Follow with `git log --all --oneline -- <path>` for suspected files, since the same secret often lives in fixtures and samples too.
  • Why tell teammates to re-clone rather than pull after the purge?
    Their local branches still descend from the original commits. A pull tries to reconcile two unrelated histories and can merge the old commits — with the secret — straight back in. A fresh clone has only the rewritten history, and unpushed work is moved onto it deliberately.
  • What remains exposed even after a perfect rewrite?
    Every copy you did not rewrite: forks and mirrors, offline clones, backups and archives, CI caches, and images built from the old checkout. Each is a complete repository. That is precisely why invalidating the credential, not the rewrite, is the control that ends the exposure.

saying these in an interview costs you the question

  • Starts with filter-repo instead of invalidating the value
  • Says rewriting history makes the leak never happened
  • Searches only the default branch for the string
  • Force-pushes one branch and skips tags
  • Tells teammates to pull after the rewrite

context