Skip to content

Ghost Commit

Secret leakage in git history · SHA-1 / SHA-256 objects · Shannon entropy

Commit an API key, delete it two commits later, then walk the parent chain and watch the real blob hash that still holds it — while a hand-rolled entropy scanner scores every token in the repository.

What this is

Git never edits a file. Every version of every file you have ever committed is stored whole, as an object named by the hash of its own contents. Committing a change does not replace the old object — it adds a new one and points the newest commit at it.

That design is what makes git trustworthy: history cannot be quietly altered. It is also what makes a leaked secret permanent. Deleting the line writes a new object; the old one keeps its name, keeps its contents, and stays reachable from the commit that introduced it.

Work through the four panels below and you will commit a credential, fail twice to remove it, watch a scanner find it in seconds, and end up at the only response that actually helps.

What this does not prove

  • A clean scan is not a safe repository. This scanner reads one branch's parent chain. A real repository also has other branches, tags, stashes, dangling objects from rebases, submodules, and notes — and a secret can hide in any of them. git rev-list --objects --all is the honest starting point.
  • Entropy does not detect secrets. It detects strings that look uniformly distributed. Object ids and UUIDs score at the top of the range and are harmless; a short PIN or a real English passphrase scores low and is not. Every threshold on this page is a trade-off someone chose, not a fact.
  • Not finding it here does not mean nobody found it there. Public-repo crawlers act within minutes. By the time you are reading the diff, the useful question is not "can I remove it" but "what did it have access to, and has it been used".
  • Rewriting history is not erasure. Forks, existing clones, CI caches, and platform-side dangling objects all keep their copy. On GitHub, an unreferenced commit remains fetchable by SHA.
  • SHA-1 is used here because git uses it. It is broken for collision resistance, which matters for git's integrity story but is irrelevant to this demo — nothing here depends on it being collision-resistant.
Glossary — the terms this page assumes
Blob
A file's contents, stored as one object. Its name is the hash of blob <length>\0<contents>, so identical contents always land on the same name — which is why an unchanged file is never stored twice.
Tree
A directory listing: names, modes, and the object id of each entry.
Commit
A pointer to one tree, plus its parent commits, author, timestamp, and message. The timestamp is inside the hashed bytes, which is why rewriting history changes every downstream id.
HEAD
The commit you currently have checked out. One node in the graph, not the graph.
Reachable
An object is reachable if you can walk to it from a ref by following commits, trees, and parents. Reachable objects are never garbage-collected.
Shannon entropy
The average information per symbol, in bits: H = −Σ p·log₂p. A measure of how evenly a string uses its alphabet — not of how hard it is to guess.
Rotation
Revoking a credential at the system that issued it and putting a new one in service. The only step that changes what a leaked value can do.
Verify the hashes yourself

Nothing here needs to be taken on trust. Reproduce any object id on this page with the real thing:

git init --object-format=sha1 demo && cd demo
printf 'hello world\n' | git hash-object --stdin
# 3b18e512dba79e4c8300dd08aeb37f8e728b8dad

# ...and for the newer format
git init --object-format=sha256 demo256 && cd demo256
printf '' | git hash-object --stdin
# 473a0f4c3be8a93681a267e3b1e9a7dcda1185436fe141f7749120a303721813

The demo's own commit chain is pinned the same way: tools/gen-git-kat.ts drives the git binary over the exact scenario in this page and records the ids, and the test suite fails if the browser code disagrees with any of them.