DEV Community

Cover image for One .git, Five Checkouts: How git worktree Works Under Your Parallel Agents
Gabriel Anhaia
Gabriel Anhaia

Posted on AI-assisted

One .git, Five Checkouts: How git worktree Works Under Your Parallel Agents


Start five coding agents on five tasks and you get five folders, five branches and five terminals. You still have one repository. Every commit those agents make lands in the same object store, and every branch they create shows up in the same ref list. Install a hook once and it runs for all five.

That is how git worktree is built, and it is the plumbing under most parallel-agent setups. Hermes IDE, which I build, uses it too: each task gets its own hermes/<name> branch in its own worktree (README). Weigh the Hermes parts with that in mind. The git parts are plain git, and you can check every one of them on a scratch repo in a few minutes.

I haven't pasted terminal output below. Each result I describe comes from the git docs, source and release notes I link, so run the commands and compare what you see.

Five checkouts on a scratch repo

Make a throwaway repo with one empty commit, then add five linked worktrees, one branch each:

mkdir -p ~/code/wt-lab && cd ~/code/wt-lab
git init -q -b main acme && cd acme
git commit -q --allow-empty -m "init"
for n in 1 2 3 4 5; do
  git worktree add -q -b "hermes/task-$n" "../task-$n"
done
git worktree list
Enter fullscreen mode Exit fullscreen mode

git worktree list prints six lines. The first is acme, which the manual calls the main worktree: the one git init or git clone made. The other five are linked worktrees. A repository has "one main worktree (if it's not a bare repository) and zero or more linked worktrees" (git-worktree).

-b creates the branch and checks it out in the new folder. Leave -b off and git names the branch after the last part of the path, so git worktree add ../hotfix creates a branch called hotfix (git-worktree).

The .git that is a file

In acme, .git is a directory. In task-1, it is a one-line text file:

cd ../task-1
cat .git
git rev-parse --git-dir
git rev-parse --git-common-dir
Enter fullscreen mode Exit fullscreen mode

The file holds gitdir: and a path. The repository layout docs call this a gitfile, the same trick submodules use. It points into a private folder inside the main repo, acme/.git/worktrees/task-1. That is what --git-dir prints. --git-common-dir prints the main acme/.git.

So every linked worktree has two git directories. Git decides per path which one to use. Look inside the private one:

ls ../acme/.git/worktrees/task-1
cat ../acme/.git/worktrees/task-1/commondir
cat ../acme/.git/worktrees/task-1/gitdir
Enter fullscreen mode Exit fullscreen mode

You'll see this worktree's own HEAD and index, plus two link files. commondir holds ../.., a relative path from the private folder back up to acme/.git. That is the literal string git worktree add writes (builtin/worktree.c). gitdir holds the absolute path back to task-1/.git, so the link runs both ways. Git uses that back-link to notice when a worktree folder has been deleted (gitrepository-layout).

One large vault labelled with a git branch symbol in the centre, with five small identical desks around it, each linked to the vault by a thin line
Five working folders, one store underneath them.

What five agents share, and what each one owns

The rule from the manual: "all pseudo refs are per-worktree and all refs starting with refs/ are shared." The exceptions are refs/bisect, refs/worktree and refs/rewritten, which stay per worktree (git-worktree). Ask git where it resolves each path, from inside task-1:

git rev-parse --git-path HEAD
git rev-parse --git-path refs/heads/main
git rev-parse --git-path objects
Enter fullscreen mode Exit fullscreen mode

HEAD resolves under .git/worktrees/task-1. The branch ref and objects resolve under the main acme/.git. The layout docs list what else lives only in the common dir: config, hooks, packed-refs and info among them (gitrepository-layout).

Each worktree owns:

  • its HEAD, so each folder has its own checked-out branch
  • its index, so git add in one folder stages nothing in another
  • refs/bisect, so one agent can bisect without moving anyone else
  • its files on disk, including the ignored ones, which is why .env and node_modules don't follow you into a new worktree

Everything else is shared. Commit in task-1, then run git log -1 hermes/task-1 in task-2: the commit is there, with no fetch, because both folders read the same refs and objects. The stash follows the same rule. refs/stash starts with refs/, so a stash one agent pushes in its folder shows up in git stash list in all the others, and any of them can pop it.

Config and hooks live in the common dir too. The config file is shared by default (git-worktree), so an agent that runs git config user.email in its worktree changes it for every worktree. For settings that should stay local, turn on extensions.worktreeConfig and use git config --worktree. Older git versions refuse to open a repo with that extension on. Hooks work the same way: one pre-commit hook runs for every agent's commits.

One branch, one worktree

Now try to take another agent's branch:

cd ../task-2
git switch hermes/task-1
git branch --list 'hermes/*'
Enter fullscreen mode Exit fullscreen mode

The switch fails. The message comes from die_if_checked_out in git's source, and its format is '%s' is already used by worktree at '%s' (branch.c). Older tutorials show the earlier "already checked out at" wording. Git changed it in 2023. git branch marks branches that are checked out in linked worktrees with a + (git-branch).

The reason follows from the split above. HEAD and the index are per worktree, but the branch ref is shared. If two worktrees had hermes/task-1 checked out, a commit in one would move the branch under the other, and the second folder's files and index would no longer match the commit its HEAD names. Git refuses that setup. git worktree add refuses it too, unless you pass --force. Even git branch -f will not move a branch that is checked out in another linked worktree (git-branch).

A task you start with ⌘N in Hermes gets a new hermes/<name> branch, so two such tasks never hold the same one. If you try to check out an agent's branch in your main checkout while it works, git stops you before you pull the floor out from under it.

Hermes's worktree isolation stops at one place: extra folders. Since 2.3, you can give a Claude task up to five extra folders next to the main project. Only the main project gets the task's new branch. The extra folders are edited where they are, on whatever branch they're on (2.3.0 release notes). The 2.3.0 notes don't describe any lock on extra folders across tasks, so keep other agents out of them while the task runs.

The New task sheet in Hermes 2.3 for a cross-repo rename, with a project chip reading orders-api +2, a new hermes/ worktree, Accept edits, the Also chips for the extra folders, and a preview of the command Hermes will run
The ⌘N New task sheet for a cross-repo rename: project chip "orders-api +2", a new hermes/ worktree, Accept edits, the "Also:" extra-folder chips, and the "Hermes will run" preview.

lock, prune and repair: the private folder's life cycle

The private folder under .git/worktrees outlives mistakes, and three commands manage it.

prune. Delete a worktree folder by hand and its private folder stays behind:

cd ~/code/wt-lab/acme
rm -rf ../task-3
git worktree list --verbose
git worktree prune --dry-run --verbose
git worktree prune
git branch --list 'hermes/task-3'
Enter fullscreen mode Exit fullscreen mode

list --verbose flags the entry as prunable, with a reason such as "gitdir file points to non-existent location" (git-worktree). prune removes the private folder. It doesn't touch the branch, which lives in the shared refs, so anything an agent committed on hermes/task-3 survives. Uncommitted work in the deleted folder is gone. If you never prune, git gc does it for you: it calls git worktree prune --expire 3.months.ago, and gc.worktreePruneExpire changes the grace period (git-gc). Next time, use git worktree remove, which refuses a folder with modified or untracked files unless you add --force.

lock. A worktree on a USB disk or network share looks deleted whenever it is unmounted. Lock it and prune leaves it alone:

git worktree lock --reason "on usb disk" ../task-4
cat .git/worktrees/task-4/locked
git worktree unlock ../task-4
Enter fullscreen mode Exit fullscreen mode

The lock is just a file named locked in the private folder, holding the reason as plain text. Git uses the same file while it builds a worktree: git worktree add writes locked containing "initializing" before the checkout and removes it afterwards, so a prune running at the same moment can't delete a half-built entry (builtin/worktree.c). If you want a worktree to stay locked from the start, git worktree add --lock does it "without a race condition" (git-worktree).

repair. Both link files hold paths, so moving folders breaks them. Move a linked worktree with git worktree move and git updates the links. Move the main repo with plain mv and the linked worktrees can no longer find it. Run git worktree repair in the main worktree to reconnect them (git-worktree):

cd ~/code/wt-lab
mv acme acme-moved
cd acme-moved && git worktree repair
Enter fullscreen mode Exit fullscreen mode

The links are absolute by default. Git 2.48 added relative ones, through git worktree add --relative-paths or worktree.useRelativePaths (git-worktree), plus a repository extension so older git versions don't misread them (2.48.0 release notes). They help when the repo is mounted at different paths, say on your machine and inside a container. repair --relative-paths converts existing links.

A small paper folder on a desk with a padlock on its tab, a broom leaning on one side and a wrench on the other, connected by a thin line to a large vault
Lock keeps the private folder, prune sweeps it away, repair reconnects it.

Hermes runs into the same life cycle, and its release notes show where. In 2.2.9, a worktree you choose to keep when you close a session is no longer deleted automatically later. A worktree that could not be removed when its session closed is retried at the next launch. Turn-history snapshots from tasks that ran in their own worktree are removed from the repository 14 days after the session closes. My reading is that the last one comes back to the shared object store: whatever any worktree writes lands in the one .git that all of them share.


If this was useful

Run the scratch repo once and the five folders stop being magic. I build Hermes IDE, which puts each task you start with ⌘N on its own hermes/<name> branch and worktree for you; it is free, source-available under BSL 1.1, and runs on macOS, Windows and Linux. Download it, and if it saves you a tangled checkout, star it on GitHub.

Once five agents commit side by side, git tells you what changed, but not what each agent did to get there or what its tokens cost. The LLM Observability Pocket Guide is a short read on picking the tracing and evals tools that answer those questions for your team. It is one of 8 short Pocket Guides covering agents, RAG, prompts, observability, events, databases and system design.

LLM Observability Pocket Guide: Picking the Right Tracing & Evals Tools for Your Team

Top comments (0)