Deleted is not gone
A secret committed once lives on in history, in every clone and in every fork. Leak a fake key, delete it, read it straight back - and learn the one fix that actually works.
It’s late, the payment test won’t pass, and the quickest way to try the real API is to paste the key into config.env. It works. Everything gets committed together. Ten minutes later someone spots the key, deletes the line, and commits again. “Fixed.”
It isn’t. Remember what a commit is: a snapshot of every file, named by the hash of its contents. The fix made a new snapshot without the key. It didn’t touch the old one, and it can’t: change a commit and its name changes, along with every name after it.
- The shop starts clean.
- The key is committed. That snapshot is now permanent, named by a hash of exactly these bytes.
- The "fix" is a new snapshot. Its parent is still the one with the key - history only ever grows.
- Anyone who fetched in between - a teammate, a fork, a CI runner - has their own copy. Nothing you do to your repository reaches theirs.
Leak it, delete it, read it back
The key below is fake, and shaped so that no real scanner will mistake it for a real one. That’s the only difference from the real thing.
git show <commit>:<file> prints a file as it was in any commit. HEAD~1 means “one commit before HEAD”. Anyone with a copy of the repository can do this, for every commit ever made.
A key was pushed in yesterday's commit. Today you delete it and push again. Who can still read it?
Every commit is kept. Yesterday's snapshot still holds the key, and anyone who cloned or fetched has their own copy of that snapshot.
The one fix that works
If a secret was pushed, treat it as stolen. Go to whoever issued it, revoke it, and issue a new one. A revoked key is worthless wherever it ended up. GitHub’s own guide puts this first, and notes that once the secret is revoked or rotated, that “may be sufficient to solve your problem.”
You can rewrite history to take the old commit out (tools like git filter-repo do this). But GitHub’s guide is plain about what that cannot reach: other people’s clones, forks that contain the commit, cached views of the commit by its hash, and pull requests that reference it. Rewriting tidies your copy. Rotating protects you.
You rewrite history so the leaked key is in no commit at all, and force-push. Is the key safe now?
A rewrite changes your repository going forward. Copies made earlier keep the old commits. Only revoking the key makes those copies harmless.
Stop it before it leaves
The cheapest leak is the one that never gets pushed:
- Keep secrets out of the working tree. Read them from the environment or a secrets manager. List local config files like
.envin.gitignoresogit add .can’t sweep them up. - Scan before you push. gitleaks checks a repository’s whole history for known secret formats, and can run as a pre-commit hook and in CI.
- Let the server refuse. GitHub’s push protection blocks a push that contains a secret in a format it recognises, before it reaches the repository. For your own account it’s on by default when you push to public repositories.
Which habit stops this leak from happening in the first place?
If the secret never sits in a tracked file, no commit can capture it. Scanners and push protection are the backstop for the day that habit slips.
Everything above ran in your browser. Now do it on a real cluster withthe tilt platform. You need Docker, kind and Tilt, about 15 GB of free disk and 8 GB of spare RAM. Setup guide · Can my machine run it?
Find out whether a string ever entered a repository’s history. -S lists the commits that added or removed it, and -p shows the change:
git log -p -S "PAYMENT_KEY" --allThen scan a repository of your own with gitleaks:
gitleaks git -v .gitleaks git reads the history, not just today’s files. (Older guides say gitleaks detect; it still works but was deprecated in v8.19.)
Sources
Missions and quizzes above earn their own XP. This marks the lesson done.