Rotating credentials after a history rewrite Jump to heading

The rewrite finished, the value no longer appears in git log -S, and the temptation is to consider the incident closed. It is not, and the reason is arithmetic rather than security theory: the number of copies of a repository is large, it includes places nobody enumerated, and a rewrite updates exactly one of them. Every fork, every developer clone, every CI cache, every backup and every forge-side object still contains the original commit. This recipe covers what remains to be done, within secret scanning and remediation.

When to use this approach Jump to heading

  • A history rewrite has been performed to remove a credential.
  • Someone has asked whether rotation is still necessary after a rewrite.
  • You are planning a rewrite and want to know what it will and will not achieve.
  • A repository that contained credentials is becoming public.
  • If you rotated first, as the response order recommends, this page is a checklist rather than a rescue.

Step 1 β€” Enumerate where copies actually exist Jump to heading

The list is longer than anyone expects, and writing it down is what makes the point.

# Forks, which each hold their own copy of the pre-rewrite objects
gh api repos/:owner/:repo/forks --jq 'length'
# Local clones β€” approximate, but the recent-contributor list is a good proxy
git shortlog -sne --since='1 year ago' | wc -l
# CI caches, mirrors and backups
gh cache list --limit 100 --json key | jq 'length'
# Verification: the pre-rewrite commit may still be fetchable from the forge
git fetch origin "$OLD_COMMIT" 2>&1 | tail -1
Copies of a repository that a rewrite does not touchA rewrite updates the repository you rewrote. Forks, developer clones, CI caches, mirrors and backups each hold an independent copy of the original objects, and none of them is updated by anything you do to the origin.copies holding the original commitdeveloper clones23forks11CI caches and mirrors6repositories you rewrote1the green bar is the entire scope of what a rewrite achieves

Step 2 β€” Rotate, if it has not already happened Jump to heading

# Test whether the credential is still live β€” this is the whole question
curl -s -o /dev/null -w '%{http_code}\n' \
  -H "Authorization: Bearer $VALUE" https://api.example.com/v1/me
# If that is not 401: rotate now, and confirm afterwards
# 1. issue  2. deploy  3. revoke  4. verify
curl -s -o /dev/null -w '%{http_code}\n' \
  -H "Authorization: Bearer $VALUE" https://api.example.com/v1/me     # expect 401
# Verification: record the rotation outside the repository
printf '%s\trotated after rewrite\t%s\n' "$(date -u +%FT%TZ)" "INC-4471" >> ~/incidents/rotations.tsv

SAFETY WARNING β€” a rewrite performed instead of rotation gives a false sense of completion that is worse than doing nothing, because the incident is recorded as resolved while the credential remains valid. If you find yourself in this position, treat it as an open incident from the moment you notice, not from the moment of the original leak.

Step 3 β€” Understand what the forge still holds Jump to heading

Most forges keep unreferenced objects reachable for a period, and some keep them indefinitely through pull request refs.

# Pull request refs survive branch deletion and rewrites
git ls-remote origin 'refs/pull/*/head' | wc -l
# A specific old commit may still be fetchable by id
git fetch origin "$OLD_COMMIT":refs/tmp/probe 2>&1 | tail -1
git rev-parse refs/tmp/probe 2>/dev/null && echo "STILL PRESENT on the forge"
git update-ref -d refs/tmp/probe 2>/dev/null
# Ask the provider to garbage-collect, where that is offered
# (support request on most providers; some expose it in settings)
What survives a rewrite, and whereThe rewritten repository no longer contains the value. The forge may still serve the old commit by id, every fork holds it independently, and each developer clone keeps it until that person reclones. None of these are reachable by any command you can run.Rewritten originvalue removedForge objectsold commit by idForksindependent copiesDeveloper clonesuntil they reclonerotation is the only action that reaches all four

Step 4 β€” Tell people what to do, precisely Jump to heading

A rewrite that people pull rather than reclone reintroduces the history you removed.

# The instruction, which must go out before the force-push, not after
cat <<'MSG'
We have rewritten history on main to remove a credential (already rotated).

DO NOT pull. Re-clone:

    mv app app-old
    git clone [email protected]:acme/app.git

If you have local work: create a patch first, then apply it to the fresh clone.

    cd app-old && git format-patch origin/main --stdout > /tmp/mine.patch
    cd ../app  && git am /tmp/mine.patch
MSG
# Verification: the default branch has no duplicated history a week later
git log --oneline --all | wc -l
git log --format='%s' -100 | sort | uniq -d | head

Duplicated subjects in the recent history are the signature of someone having merged the old history back in, which is worth checking for deliberately in the days after a rewrite.

Step 5 β€” Record what was achieved, honestly Jump to heading

The incident record should distinguish the two actions, because they answer different questions.

cat >> ~/incidents/INC-4471.md <<'DOC'
## Actions

- 2026-09-18 10:12Z  Credential confirmed live (200 from the API).
- 2026-09-18 10:26Z  Rotated; old value confirmed revoked (401).
- 2026-09-18 11:40Z  Audit log reviewed for the exposure window (2026-03-02 to 2026-09-18).
                     No unrecognised source addresses. Log retention covers the window.
- 2026-09-19 09:00Z  History rewritten on main; commit map published internally.
                     NOTE: 11 forks and an unknown number of clones retain the original
                     objects. The rewrite removes the value from new clones only.

## Outcome

Exposure ended at 10:26Z by rotation. The rewrite is hygiene, not remediation.
DOC
# Verification: the record states the exposure window and what ended it
grep -c 'Rotated' ~/incidents/INC-4471.md
Two actions, two different achievementsRotation ends the credential's validity for everyone holding a copy, immediately and irreversibly. A rewrite removes the value from future clones of one repository and leaves every existing copy untouched.History rewriteRotationreaches existing clonesnoyesreaches forksnoyesreaches CI cachesnoyesends the exposurenoyesimproves hygieneyesnot directlyboth are worth doing; only one of them is remediation

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

If rotation is what matters, why rewrite at all? Jump to heading

For hygiene and for future exposure. A repository about to become public should not ship a history full of former credentials, even dead ones, because they reveal formats, naming conventions and infrastructure details. It is a legitimate reason; it is not a security remediation for the credential itself.

Can we ask the provider to purge the old objects? Jump to heading

Most will, on request, and it is worth doing when the repository is public or the exposure was significant. It closes the by-id fetch route on the forge and does nothing about forks or clones, so it belongs in the same category as the rewrite: useful cleanup, not the thing that ended the exposure.

How do we know whether a fork still has it? Jump to heading

You generally cannot, and forks are not under your control. That is precisely why the enumeration in Step 1 is worth doing in advance: knowing that eleven forks exist is what makes the case for rotation concrete when someone argues the rewrite was sufficient.