Recording the Gaps Between Commits
Git remembers decisions. It does not remember the states between them. A commit says what I chose; it rarely says how many times the work was already correct before that choice, or what the worktree looked like five minutes before a refactor went wrong.
Tachograph exists for that gap. It turns every meaningful change in a worktree into a snapshot, and keeps the snapshots out of the project’s own history.
The first design question was not how to detect changes. It was where to put them.
Why Not a Branch
Git already has the tools: tags, stashes, notes, branches. None of them fit. A tag pins a commit I already made; the
interesting state is uncommitted. A stash is one slot and rewrites the working tree. Notes attach to existing commits. A
snapshot branch inside the project would work technically, but it would put thousands of snap: commits into the graph
I read every day, and every git log, git gc and push would have to ignore them.
I wanted the history to be complete and the project to stay clean. Those are not the same requirement, and only one of them is negotiable.
A Second Repository
Tachograph keeps one history repository per project under its own data directory. For every real worktree, that
repository gets a linked-worktree placeholder: a directory with its own .git file, its own index, its own HEAD and
a branch named wt/<worktree>. The placeholder’s core.worktree points at the real directory, so Git stages and
commits the real files without ever opening the project repository for writing.
A snapshot is then almost boring:
git -C <placeholder> add -A --ignore-errors
git -C <placeholder> diff --cached --quiet # exit 1 means there is something to record
git -C <placeholder> commit -m "snap: 2026-11-06T20:00:23Z [main@f9d5c82]"
The first commit is base: history initialized. The project repository never learns that any of this exists.
The pointer file is the fragile part. Git writes the linked-worktree metadata itself, and the history store only reads it. When a worktree is deleted, the placeholder and its branch are deleted too; when the project’s last worktree disappears, the whole history repository goes with it. The durable registry is the history store, not a database: every known worktree is read from its per-worktree config file at startup.
That clean separation is the foundation. It is also where the trouble starts. A second repository is still a repository, and Git does not care that this one is supposed to be invisible. The next entries are about the places where the abstraction leaked: a staging command that refused to run, a compaction pass that rewrote hashes, three writers sharing one lock, a diff that had to be readable, a worktree that showed up two minutes late, and the price of keeping all of it local.
The project graph stayed clean. Keeping it that way took more than moving the commits.
A recorder is defined by what it refuses to do: it does not commit into the project, it does not branch it, and it does not ask the project to know that its own history exists. The second repository is not a workaround for a missing Git feature. It is the feature—the one that keeps the graph readable while the record stays complete.
