./ahmedhashim

Agent Worktrees

Coding agents have changed the shape of my work. I hand out a few scoped tasks and spend my attention on what comes back. The snag is that a git clone assumes one person: a single working tree with one branch checked out. Put two agents in the same cloned repo and they type over each other.

Git grew a fix for this back in 2015, long before anyone needed it for robots.

Worktrees

git worktree attaches extra working directories to a repository you already have. Each one has its own checked-out branch and its own files on disk, while history and branches stay shared with the original clone. Creating one costs about as much as a checkout:

git worktree add -b fix-upload-limits /tmp/agents/critter/fix-upload-limits

This creates the branch fix-upload-limits and checks it out into a fresh directory under /tmp. The original clone doesn’t move. Commits made in the worktree show up there without a push or a pull.

Before worktrees, working two branches at once meant a second clone that only stayed in sync through the remote. A worktree can’t drift because it’s the same repo. Git even refuses to check out the same branch in two worktrees, which sounds like a restriction until several agents share a repo. Then it’s a guarantee.

The setup

The repos I’m working on live under ~/Workspace checked out on whatever branch origin/HEAD points at, and git sync keeps them current. Those copies are the clean base everything else stems from.

The actual work happens in /tmp. A task here is a logical, self-contained unit of work. Each one gets a branch, and each branch gets a worktree:

cd ~/Workspace/critter
git sync
git worktree add -b fix-upload-limits /tmp/agents/critter/fix-upload-limits

I start an agent inside that directory and hand it the task, then do the same for the next one. Each agent sees a complete copy of the repo on its own branch. They build and test in parallel without touching each other’s files, and since branches are shared with the main clone, I can follow any agent’s commits from ~/Workspace with plain git log while it works.

When a branch is ready it gets pushed and becomes a pull request like any other. After the merge, git sync back in ~/Workspace pulls the updated default branch and deletes the merged one.

Ephemeral

The tradeoff here is space. Every worktree is a full checkout, and each agent stacks more on top: dependencies, target directories, etc. I don’t bother cleaning it up. Instead the /tmp directory on my machine is a tmpfs filesystem, which is garbage collected at every power cycle.

I’ve come to like what that forces. Anything worth keeping gets committed and pushed early, because the directory it lives in won’t see the weekend. Work environments stop being precious since there’s nothing to maintain and nothing to migrate.

Two pieces of bookkeeping do survive the reboot, both in the main clone. Git still remembers worktrees whose directories are gone, and git worktree prune clears those records out. The branches survive too, since a branch belongs to the repo rather than the worktree. Git refuses to delete a branch it believes is checked out, even when the directory behind it no longer exists, so git sync prunes those records before it sweeps the merged branches.

Build caches

Any language with slow compiles invites a shared build cache, and on my machine that’s Rust. Cargo builds are expensive enough that every checkout of a project points at the same target directory, so a fresh clone reuses what something else already compiled:

# ~/.cargo/config.toml
[build]
target-dir = "/var/cache/cargo-target"

For one human on one branch, a shared cache is a pure win. For several agents on several branches, it goes wrong in two ways.

The first is contention. Cargo takes an exclusive lock on the target directory, so concurrent builds serialize. An agent that looks hung is usually just parked behind a neighbor’s compile, waiting on Blocking waiting for file lock on build directory.

The second is churn. Parts of the cache belong to whichever branch was built last. Dependencies stay warm as long as their versions and features agree across branches. However, the repo’s own crates rebuild on every flip, and debug/ holds whichever binary finished most recently. Two agents alternating builds on different branches will happily redo the same work all afternoon, and each build wipes out the other’s incremental state as it goes.

The fix is to keep the shared cache for the human and give each agent a private one. Cargo’s environment variables override its config files, so it takes one line when the agent starts:

export CARGO_TARGET_DIR="$PWD/target"

The first build in each worktree runs cold. Everything after it is isolated, and since the target directory now sits inside the worktree, it evaporates on the same schedule as the rest of /tmp. The rule of thumb for any shared cache: if it’s keyed by content, like sccache, it shares safely across branches. If it holds whatever was built last, it needs a single owner.

The bottleneck

The speedup on the coding side is real. Tasks that used to run linearly now run in parallel, and an afternoon can produce more branches than a week of hand coding everything.

Review still runs at the old speed. I read every branch before it merges, same as I would a colleague’s, because merging code nobody understands is how a codebase rots. The habits from Effective Code Review apply here too. Small tasks help the agents finish reliably and keep each diff small enough to hold in my head.

So the bottleneck moved from writing code to understanding it, which is probably where it always belonged. One git command plus a directory that empties itself gives every task its own copy of the repo. For me, worktrees sat in the man pages for a decade, waiting for a workload like this.