How worktrees actually work in my setup
Last updated: June 26, 2026
This page is the technical companion to How I got Git worktrees to actually work. The blog post is the story. This page is about the wiring: path layout, Nix and direnv entry, bosun config, agent config frontmatter, a redacted repo-skill excerpt, and the failure mode that made the rules feel non-optional.
Bosun, in this context, is my local agent harness: tmux-backed agent spawning, a sandbox, Nix dev shells, pi-mesh coordination, and repo-specific skills.
Operating rules and evidence
Before getting into the details, here is the short version.
The operating rules are simple:
- clones live at
workspace/code/{host}/{group}/{repo} - worktrees live at
workspace/code/worktrees/{host}/{group}/{repo}/{branch} - the parent repo and the worktree have to live inside the same workspace boundary
The evidence comes from a subset of my internal bosun harness usage, not from every session I've ever run:
- the current workspace contains 114 live worktree directories across 28 repositories
- in the raw Pi session archive for this harness, 467 unique session files contain explicit
git worktreecommands - those raw sessions contain 714
git worktree addhits, 208listhits, 85removehits, and 33prunehits
Method: I counted live worktree directories under workspace/code/worktrees, then searched the raw Pi session archive under .bosun-home/.pi/agent/sessions for explicit git worktree add, list, remove, and prune commands. This is still a subset, not a telemetry system, so wrapper commands and indirect workflows can slip past it.
From blank worktree to runnable task
The part most worktree advice skips is everything between git worktree add and "I can actually continue the task here." In bosun, that path is pretty concrete.
At a high level, it looks like this:
# Main clone in the canonical location
# Create the task branch as a worktree
# Enter the worktree - direnv loads the flake-backed shell
# Typical repo-specific bootstrap sequence
# Later, resume the same directory and pair it with a handoff
The exact commands differ by repo, but the shape stays the same.
1. The path convention is encoded, not remembered
The canonical layout is written into the upstream Git skill itself:
workspace/code/{host}/{group}/{repo}
workspace/code/worktrees/{host}/{group}/{repo}/{branch}
That matters because it stops both humans and agents from improvising path layouts. A repo URL maps to one main clone path. A branch maps to one worktree path.
The repo-level guidance in AGENTS.md is blunt about it too: use workspace/code/worktrees/ for worktrees, not sibling paths like ../../.
A typical worktree creation flow then looks like this:
That example is generic, but the shape matches the actual bosun docs.
2. Entering the directory also enters the shell
The root .envrc in bosun is tiny:
That single line is doing a lot of work. Direnv notices the directory change and hands off to Nix. The repo README says it plainly: nix develop provides the toolchain reproducibly, and direnv auto-loads it via .envrc.
The flake then supplies the actual toolchain. A trimmed excerpt from flake.nix looks like this:
devTools = with pkgs; [
bubblewrap
tmux
git
just
nodejs_22
bun
python312
];
This is why Nix and direnv matter specifically for worktrees. Without them, a new worktree is just another checkout. With them, entering the worktree usually gets me a shell with the expected compilers, interpreters, CLIs, and helper tools already there.
See Nix for Dev Envs for the broader sandbox rationale.
3. Bosun wiring makes the worktree usable by agents
The architecture docs describe the config flow like this:
config.toml (source of truth)
↓ bun node_modules/bosun/scripts/init.ts
.pi/settings.json
.pi/agents.json
.pi/daemon.json
.pi/bwrap.json
.pi/pi-mesh.json
A small config.toml excerpt covers most of the system wiring:
[]
= "workspace"
[]
= "tmux"
= "node_modules/bosun/scripts/sandbox.sh"
[]
= true
[]
= true
In practice, those lines mean:
- all project work lives under one
workspace/root - spawned agents run in tmux, not in a one-off hidden subprocess
- agents auto-join the mesh for coordination
- the sandbox boundary is consistent across sessions
Agent config frontmatter then loads the matching runtime pieces:
- - - - -So the agent landing in a worktree is not starting from scratch either. It already knows about Git conventions, can spawn into tmux, can coordinate over the mesh, and is running inside the same sandbox model that the path rules were designed for.
One small but important detail: config.toml is the source of truth here. Bosun regenerates its .pi/*.json config from that file, so this behavior is configured, not improvised per session.
4. Repo skills carry the last mile of setup
The upstream Git skill gets me to the right path. Repo-specific skills explain what has to happen inside that worktree for a particular codebase.
A redacted but structurally faithful example looks like this:
- ----
- ---The exact commands vary by repo, but the pattern stays the same. The skill is there so the agent does not have to rediscover branch policy, bootstrap order, or path conventions from scratch.
5. Handoffs make resuming cheaper than restarting
A worktree keeps the code state. Handoffs keep the reasoning state.
That is what makes resuming cheap. I'm not just reopening a branch. I'm reopening a branch plus the setup artifacts in that directory plus a short written record of what the task was doing.
Why the sandbox boundary rule is real
The upstream Git skill is unusually explicit about this point:
Both the parent repo and the worktree must be inside
$BOSUN_ROOT/workspace/.
That rule exists because a worktree is not self-contained. Its .git file points back into the parent repo metadata.
A redacted example of what that pointer looks like:
gitdir: /absolute/path/to/parent/.git/worktrees/feature-branch
That is why moving or renaming the parent repo is not a cosmetic change. If the parent path changes, or if the parent repo is outside the sandbox while the worktree is inside it, the pointer stops making sense.
This shows up in more ordinary ways than a repo rename. Move the parent repo, recreate your machine with a different path layout, or keep the main clone outside the environment where the worktree runs, and the setup starts breaking in ways that feel mysterious until you inspect the pointer.
Worktrees are not magic. They are real wiring.
What actually persists inside a useful worktree
Once the path, shell, and setup rules are in place, the worktree stops being just a branch checkout and turns into a task directory.
In practice that often means:
- dependency directories like
node_modules - build outputs
- generated assets
- downloaded local data
.envfiles- the current working directory assumptions of dev servers and scripts
This is one reason worktrees have felt so much better than branch switching for me. When I return to a task, I am often reopening a half-prepared environment rather than reconstructing one from memory.
The evidence is operational, not aspirational
The current usage counts are useful mostly because they show repetition, not because they are perfectly scientific.
If you wanted to reproduce the rough shape of the numbers, the method is not complicated:
# Live worktrees
# Worktree-related session summaries and raw command hits
Here is the rough shape of the command history in that raw archive slice:
git worktree add: 714 hits across 413 unique session filesgit worktree list: 208 hits across 71 unique session filesgit worktree remove: 85 hits across 32 unique session filesgit worktree prune: 33 hits across 4 unique session files
I don't present those numbers as exact telemetry. I am using them for something simpler: to show that worktrees are part of the normal operating model here, not a one-off habit.
A minimal version you can steal
If you wanted the smallest useful version of this without adopting bosun itself, I would start here.
1. Pick one layout and stick to it
~/code/{host}/{group}/{repo}
~/code/worktrees/{host}/{group}/{repo}/{branch}
The exact root does not matter much. Consistency does.
2. Create worktrees the same way every time
# Main clone
# New worktree
If the branch already exists remotely, use that instead of creating a new local one:
3. Make the shell activate on entry
A tiny .envrc is enough to make worktrees feel less disposable:
And the flake can stay simple at first:
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [ git tmux just nodejs_22 bun python312 ];
};
You do not need perfection here. You just want the common tools to show up the moment you enter the directory.
4. Write down the repo bootstrap once
For each repo, keep a small setup note close to the code. It can be a skill, a README section, or a plain markdown file. The important part is that it answers the same questions every time:
- what branch should I base this on?
- where should the worktree live?
- what do I install after checkout?
- what do I build before running?
- what port or support services does the app expect?
That one document saves a surprising amount of drift.
Useful commands once you have a few worktrees
These are the commands I keep coming back to:
# See what already exists
# Remove a finished worktree
# Clean stale metadata if a directory was deleted manually
# Inspect what a worktree points at
That last one is especially useful when something feels broken.
The failure mode worth understanding
If worktrees ever seem flaky, inspect the .git file first.
gitdir: /absolute/path/to/parent/.git/worktrees/feature-branch
That one line explains most of the weirdness.
A worktree is not a self-contained checkout. It is a working directory plus a pointer back to parent-repo metadata. If that parent moves, disappears, or lives outside the environment the worktree can see, the setup breaks in ways that feel mysterious until you look at the pointer.
What I would steal even outside bosun
If I were rebuilding this in a different harness, I would keep these pieces:
- one canonical path scheme for clones and worktrees
- one boundary rule for where parent repos and worktrees are allowed to live
- a reproducible shell that activates on directory entry
- per-repo setup docs or skills that teach the bootstrap order
- a lightweight way to resume task context alongside the directory
- a coordination layer if multiple humans or agents edit in parallel
That is the actual lesson I take from this setup.
Git gives you the primitive. The rest is what makes it hold up day after day.
See also: