Learn/Semantic · 6 min · updated 2026-08-03

Shadow branches: keeping tool metadata out of your history

A shadow branch stores a tool’s own data in the repository without putting it on the branches you push. How the pattern works with Git refs, and what breaks when it goes wrong.

A shadow branch is a Git ref that holds a tool's own data next to your history without appearing on the branches you work on. Git allows refs outside refs/heads, and objects stored under them are ordinary Git objects: content-addressed, replicated by the same plumbing, and invisible to git log unless you ask for them.

Why not just commit the files

Because a semantic index changes on every edit. Committing it into the working branch means every commit carries a large derived artefact, every merge conflicts on it, and every diff is polluted by it. Keeping it in a separate ref gives durability and history for the index while leaving the branch you push containing only your source.

How the pattern works

  • The tool writes its data as a tree, commits it, and points a custom ref at the commit.
  • The ref lives outside refs/heads, so it does not show up as a branch and is not pushed by default.
  • Because the objects are normal Git objects, they are addressed by content and are cheap when unchanged.
  • Garbage collection keeps them alive: a reachable ref is a root.

Git's own notes feature is the same idea with a fixed shape, and Git LFS and replace refs use adjacent mechanics.

What goes wrong

SymptomCause
Index looks stale after a rebaseThe shadow ref still points at the pre-rebase commits
Data missing on a fresh cloneCustom refs are not fetched unless the refspec asks for them
Repository growsOld shadow commits stay reachable and never get collected
Worktree confusionA per-worktree ref written where a shared one was expected

Every one of these is recoverable, because the source of truth is the source code: the index can always be rebuilt by re-parsing. That is the property to preserve when designing this kind of storage. Anything that cannot be regenerated should not live in a shadow ref.

In Aura

Aura keeps its semantic state this way, and aura doctor checks it: stale refs after a rebase, orphaned snapshots, missing hooks and shadow-branch drift are the four things it repairs. If the index and the working tree disagree, re-indexing is the fix, and it is fast because unchanged subtrees hash the same.

See also: the content-addressed code graph and the audit record that sits on top of it.

Questions

What is a shadow branch?

A Git ref a tool uses to store its own data inside the repository without putting it on the branches you push. It travels with the repo, survives a clone if you ask for it, and never appears in the history a reviewer reads.

How do you store tool metadata in Git without polluting history?

Write it to a ref outside refs/heads — a custom namespace holding its own commits. The objects live in the same store, so it is cheap and consistent with the code it describes, but git log on your branch never shows it.

Why not just commit tool data into the repository?

Because it then appears in every diff, every blame and every review, and it conflicts on merge for reasons nobody cares about. Data that describes the history should not be part of the history it describes.