- Book: Database Playbook
- Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go
- My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools
- Me: xgabriel.com | GitHub
Read the Codex worktree docs and one line stands out: "The worktree isn't checked out as a branch." Each Codex chat you start in Worktree mode begins on a detached HEAD in its own git worktree. Run five of those and every commit the agents make lands on a HEAD that no branch points at.
Now run git gc in your main checkout. Those commits stay. That wasn't always true, and the reason it is true now goes back to a shell script from 2007 that shared one .git directory through symlinks.
2007: one repository, a pile of symlinks
contrib/workdir/git-new-workdir was added to Git on March 31, 2007 (commit history). It gave you a second working directory without a second clone. The script made a fresh .git folder and filled it with links back to the original: config, refs, logs/refs, objects, info, hooks, packed-refs, remotes, rr-cache and svn, each one created like this:
ln -s "$git_dir/$x" "$new_workdir/.git/$x" || failed
Three things were left out on purpose. The comment above the loop says the script would "explicitly exclude index, HEAD and logs/HEAD from the list since they are purely related to the current working" directory. It copied HEAD once as a starting point and then ran git checkout -f on the branch you asked for.
That is a sensible design with a blind spot. Nothing in the original repository recorded that the new directory existed. When git gc ran there, the pruning code marked objects reachable from the refs plus the index and HEAD of the checkout it was running in, which is how Git developer Nguyễn Thái Ngọc Duy described mark_reachable_objects() on the Git list in 2016. A detached HEAD in the workdir, the blobs staged in its index and its HEAD reflog were files gc never opened. And because objects was a symlink, a gc started inside the workdir cleaned the same object store, with the blind spot pointing the other way.
2015: Duy's worktrees put every checkout on record
Duy's nd/multiple-work-trees series started life as git checkout --to. His commit 529fef2 gives the new directory a .git file that points at $GIT_DIR/worktrees/<id>, a private folder inside the main repository. Each linked checkout keeps its own HEAD, index and reflog in that folder, and the main repository can list them all.
The Git v2.5.0-rc2 announcement of July 13, 2015 described the feature as "A replacement for contrib/workdir/git-new-workdir that does not rely on symbolic links and make sharing of objects and refs safer by making the borrowee and borrowers aware of each other." It also said the command would move to its final name, git worktree add.

Symlinks shared the store but left each checkout's HEAD outside it. Worktrees file a record for every checkout inside the repository.
The first version was small. The Git 2.5 manual documents two subcommands, add and prune, and its BUGS section lists remove, mv, list and lock as automation git-worktree "could provide". It called multiple checkout "still experimental", a sentence the current manual still carries.
The replacement had the same hole until Git 2.15
Putting every worktree on record made the fix possible. It didn't make gc use the record. On May 31, 2016, a Git contributor wrote to the list (thread) that they had a large merge going in a separate worktree, ran git gc in the main worktree, and "This removed a lot of objects that were recorded in that separate worktree index." They got them back only because the content was still on disk. Duy replied that the pruning code only looked at the current worktree's index and HEAD, and said it "should be fixed even if it is not the root cause of your problem."
In June 2016, another contributor reposted a series titled "Detached HEADs in new worktrees considered harmful". One of its patches demonstrated that git prune in the main worktree loses the HEADs of other worktrees, and the reverse. Junio C Hamano, the Git maintainer, replied: "You do need to teach the reachability code to consider points that are outside refs/".
Duy's fix series named all three gaps (thread): "Previously per-worktree references in index, detached HEAD or per-worktree reflogs come from current worktree only, not all worktrees." A reworked fix shipped in Git 2.15, announced October 30, 2017, whose notes say gc "did not consider the index and per-worktree refs of other worktrees as the root for reachability traversal, making objects that are in use only in other worktrees to be subject to garbage collection."
So for a little over two years, git worktree had the new-workdir blind spot behind a nicer command. Any machine still on Git older than 2.15 has it today. Run git --version on old CI images and build boxes before you put agents on them.
The script itself outlived its purpose by a decade. Patrick Steinhardt's commit "contrib: remove git-new-workdir" deleted it in May 2025, noting that git worktree now covers separate working directories. Git 2.51's release notes sum up that cleanup as "A lot of stale stuff has been removed from the contrib/ hierarchy."
Five detached agents, one gc
You can watch the fix hold on a throwaway repository. This makes five detached worktrees, the way Codex starts a Worktree-mode chat, and one commit in each:
git init demo && cd demo
git commit --allow-empty -m "base"
for n in 1 2 3 4 5; do
git worktree add --detach "../agent-$n"
git -C "../agent-$n" commit --allow-empty \
-m "agent $n: work on a detached HEAD"
done
git worktree list --porcelain
In the porcelain output, every agent's record ends with a bare detached line instead of a branch refs/heads/... line. The manual's example shows the shape:
worktree /path/to/other-linked-worktree
HEAD 1234abc1234abc1234abc1234abc1234abc1234a
detached
That HEAD line is the only thing pointing at the agent's commit. The worktree's name is usually the folder's base name, and the manual documents a worktrees/<name>/HEAD path for reading another worktree's HEAD from anywhere. Use it to check the commits before and after an aggressive gc:
for n in 1 2 3 4 5; do
git rev-parse --short "worktrees/agent-$n/HEAD"
done
git gc --prune=now
git cat-file -e worktrees/agent-3/HEAD && echo kept
--prune=now drops the usual two-week grace period, so anything unreachable goes at once (gc.pruneExpire). On Git 2.15 or later, each worktree's HEAD counts as a root, so all five commits stay. Keep --prune=now to demos. The git-gc manual warns that gc running while another process writes to the repository carries "some risk of corruption", and five working agents are exactly that.

gc sweeps whatever nothing points at. Since Git 2.15, each worktree's HEAD, index and reflog are pins it has to step around.
What still expires
Each worktree's record is a root only while it exists. Three ordinary moves take it away.
Deleting the folder by hand. If an agent's worktree folder is removed with rm -rf, its entry in $GIT_DIR/worktrees stays behind and git worktree list marks it prunable ("gitdir file points to non-existent location", in the manual's example). gc.worktreePruneExpire says gc then calls git worktree prune --expire 3.months.ago. Once the entry is pruned, that agent's HEAD goes with it, and gc's normal grace period is all that stands between the commit and deletion. You can see it on the demo repo by skipping the three-month wait:
a4=$(git rev-parse worktrees/agent-4/HEAD)
rm -rf ../agent-4
git worktree prune --expire now
git gc --prune=now
git cat-file -e "$a4" && echo kept || echo gone
Removing a clean detached worktree. Without --force, git worktree remove refuses an unclean worktree (untracked files or modified tracked files, per the manual), one with submodules, or a locked one. Committed work on a detached HEAD isn't on that list, so the worktree and its administrative files go, and the commit is left unreferenced.
An agent moving on. If an agent checks out something else inside its detached worktree, its earlier commit survives only in that worktree's HEAD reflog. gc.reflogExpireUnreachable removes such entries after 30 days by default.
The fix is the same in every case: give the work a name before the record goes away.
git branch keep/agent-3 worktrees/agent-3/HEAD
git worktree lock --reason "agent 3 under review" ../agent-3
git config gc.worktreePruneExpire never
A branch makes the commit reachable from refs/heads/, which gc has always honoured. lock stops the entry from being pruned and also blocks moving or deleting the worktree. The config line turns off automatic worktree pruning for the repository if you would rather prune by hand. Codex has its own safety net here: its docs say it keeps your 15 most recent Codex-managed worktrees by default, offers Create branch here, and "Before deleting a Codex-managed worktree, Codex saves a snapshot of the work on it."
Where Hermes sits in this
I build Hermes IDE, so weigh this section with that in mind. Hermes takes the branch route: ⌘N (Ctrl+Shift+N on Windows and Linux) starts each task on its own hermes/<name> branch in its own worktree, per the README. The agent's commits sit under refs/heads/, so whether they survive gc doesn't depend on any per-worktree record. They are ordinary git worktrees, so git worktree list --porcelain shows them with a branch refs/heads/hermes/... line where the Codex ones say detached.

The Review Desk's Worktrees tab in Hermes 2.3.4: one Hermes worktree each for orders-api, web-app and orders-types under ~/code, with each worktree's hermes/ branch, task, age and disk size, and the free disk space.
Since 2.2.9, a worktree you choose to keep when closing a session is no longer deleted automatically later. Hermes is free, source-available under BSL 1.1, and runs on macOS, Windows and Linux. It is not affiliated with Nous Research, Anthropic, OpenAI or any agent vendor. Download it or read the code on GitHub.
If this was useful
Git's object store is a database with a garbage collector, and this whole history turns on one question: what does the collector count as live? You ask the same question every time you pick a store and decide how it keeps or expires data. The Database Playbook is my short guide to choosing the right store for each system you build.

Top comments (0)