Git Worktrees for the Parallel-Agent Era
Git worktrees let one repository have several branches checked out simultaneously, each in its own directory, sharing a single object database. That property is what makes them the right primitive for running multiple coding agents in parallel, and the rest of this post covers how the mechanism works, how to wire it up, and where it stops scaling.
1. What a worktree actually is
A git worktree is a second working directory that shares the underlying object database of your repository. One .git/objects store, one set of refs, but two (or ten) checked-out branches sitting in separate directories on disk, each with its own files, its own HEAD, its own index.
This is not a clone. A clone copies the object database, and pushes and fetches travel over a remote. A worktree is the same repository viewed through a second window. Commit something in worktree A and worktree B sees it immediately under git log. Nothing to push or fetch; they’re reading the same objects.
It’s also not the same as git checkout. Checkout mutates the working directory you’re already standing in: the files in front of you change, your editor’s open tabs go stale, any process watching the filesystem fires. Worktrees instead create a new working directory and leave the original one untouched. You end up with one repository and N coexisting branches checked out, each in its own folder and each editable independently.
The physical layout is the giveaway. In your main checkout, .git is a directory. In a linked worktree, .git is a single-line file containing something like gitdir: /path/to/repo/.git/worktrees/feature-x. That gitdir is where the worktree’s private state lives: its own HEAD, its own index, its own logs/HEAD. Everything else (objects, refs, hooks, config) is shared back at the original .git directory. Branches, tags, and reflog entries are global; what you have checked out and what you’re staging is local to each worktree.
2. The commands that matter
There are five worktree commands you’ll actually use: add, list, remove, prune, and lock. The rest are corner cases.
git worktree add <path> <branch> creates a new worktree at <path> with <branch> checked out. If the branch doesn’t exist, -b <new-branch> creates it from the current HEAD:
git worktree add ../repo-feat-x -b feat-xConventionally the new worktree lives as a sibling directory rather than nested inside the main checkout. Nesting works, but it confuses find, IDE indexers, and your own muscle memory.
git worktree list shows every worktree, its path, the commit it’s on, and the branch:
/home/me/code/repo a1b2c3d [main]
/home/me/code/repo-feat-x d4e5f6a [feat-x]
/home/me/code/repo-feat-y b7c8d9e [feat-y]git worktree remove <path> deletes a worktree cleanly. It refuses if the worktree has uncommitted changes, which is usually what you want. --force overrides.
git worktree prune cleans up the administrative state under .git/worktrees/ after you’ve manually rm -rf’d a worktree directory, which you will, because the directory is just files and rm works, and git’s bookkeeping stays stale until you prune.
git worktree lock <path> prevents pruning. Useful when the worktree lives on a removable drive or a network mount that’s intermittently absent, since git won’t garbage-collect its metadata while it can’t see the directory.
The one rule that catches people: the same branch cannot be checked out in two worktrees at once. If feat-x is checked out in ../repo-feat-x, trying to git checkout feat-x in the main repo errors out. Two working trees racing to modify the same branch’s index would corrupt it. If you genuinely need to inspect a branch that’s checked out elsewhere, create a detached-HEAD worktree at that commit: git worktree add --detach ../inspect <sha>.
3. What changes when you have multiple working trees
Most of git keeps working exactly as you’d expect. Branches and tags are repository-global, so git branch -a shows the same list from every worktree. A git fetch in one worktree updates refs that every other worktree sees instantly. Commits made in worktree A are reachable from worktree B’s git log the moment they exist; same object database.
What’s local to a worktree is small but important: the checked-out files, the index (staging area), the per-worktree HEAD, the per-worktree reflog, and any untracked files sitting in that directory. Stashes are per-worktree too in modern git; older versions shared them globally.
The shared-vs-local split has a few sharp edges worth knowing.
Hooks are shared. All worktrees run the same pre-commit, pre-push, etc. from .git/hooks/. If your hook assumes a specific working directory layout or reads .env.local, it’ll behave differently depending on which worktree triggered it.
.gitignore’d build artifacts don’t carry across. Anything not tracked (node_modules/, .venv/, target/, and so on) is genuinely separate per worktree. A fresh git worktree add gives you a directory with no installed dependencies and no compiled output. You’ll re-run install and build commands in the new tree.
Tooling that caches “the repo’s location” gets confused. IDE indexers, language servers, and file watchers usually assume one working directory per repo. Open two worktrees of the same project in the same VS Code window and the TypeScript server will pick one and ignore the other. Open them in two separate windows and you’re fine. Same story for tmux sessions, shell direnv configs, and anything else that keys off a path.
Submodules need extra care. Each worktree has its own submodule working directories, but the submodule’s .git is again shared. Run git submodule update --init in any new worktree before expecting submodule code to be present.
4. Why this matters for parallel agents
Parallel coding agents need isolated working directories because each agent’s loop assumes the filesystem it just wrote to is the filesystem it’ll read next. Worktrees give each agent that isolation without the cost of N clones.
An agentic coding harness (Claude Code, Cursor’s agent mode, whatever you prefer) is an actor that owns a working directory. It edits files, runs the dev server, runs tests, and reads back the resulting state to decide what to do next. That ownership is real: the agent’s loop depends on files being where it last left them, processes it started still running, and the dev server’s HMR firing when it saves a file.
Running two such agents in one working directory breaks all of that. Agent A edits src/foo.ts; agent B, mid-task on a different feature, sees the file change and either picks it up (wrong context) or its tests fail (because the file no longer compiles in isolation). Worse, both agents are pointing the same dev server at the same source tree. One rebuild invalidates the other’s mental model of what’s on disk right now.
Branches alone don’t fix this. git checkout other-branch mutates the working directory the other agent is editing. Even if you discipline the agents to never touch each other’s branches, the act of switching branches in a single checkout is itself a destructive operation from the other agent’s perspective: its open files change underneath it.
The clean answer is one working tree per agent. Each agent gets its own directory, its own branch, its own dev server on its own port, its own running test process. Git’s object store is shared, so branches and fetches are free, and merging back happens through normal git operations. The working directories never collide because they aren’t the same directory.
Worktrees give you exactly that, without the cost of N full clones. Three parallel agents on a repo with a 2 GB object database use 2 GB of objects plus three small working trees, not 6 GB. Cross-branch operations like rebasing onto main or cherry-picking from another agent’s branch work normally because all the history sits in one place.
5. A workable layout for N parallel agents
The layout that holds up in practice is one sibling directory per active task, alongside the main checkout:
~/code/
myapp/ # main checkout, usually on main
myapp-auth-rewrite/ # worktree for the auth rewrite task
myapp-perf-fix/ # worktree for the perf investigationEach sibling is its own worktree, its own branch, its own agent session. The main checkout stays on main and is where you do merges, pulls and any human-driven work that needs the “canonical” view of the repo.
The three things that need per-tree handling are dependencies, environment files, and ports. A small shell helper takes care of all three at creation time:
#!/usr/bin/env bash
# nw: "new worktree" — create a sibling worktree, wire it up, hand it back
set -euo pipefail
branch="$1"
repo_root="$(git rev-parse --show-toplevel)"
repo_name="$(basename "$repo_root")"
parent="$(dirname "$repo_root")"
worktree_path="$parent/$repo_name-$branch"
git worktree add "$worktree_path" -b "$branch"
# per-tree env file with a free port
port="$(python3 -c 'import socket; s=socket.socket(); s.bind(("",0)); print(s.getsockname()[1])')"
cp "$repo_root/.env.local" "$worktree_path/.env.local"
echo "PORT=$port" >> "$worktree_path/.env.local"
# per-tree deps
(cd "$worktree_path" && pnpm install)
echo "ready: $worktree_path on port $port"pnpm install is cheap here because pnpm’s content-addressed store deduplicates packages across worktrees; installing into a new tree is mostly symlink creation. uv does the same for Python. Vanilla npm does not, and you’ll pay full disk for each tree’s node_modules; if you’re running five agents in parallel, that adds up fast. Switch to pnpm if you’re going to run more than two agents in parallel on the same Node project.
The port assignment matters because each agent will start its own dev server, and two agents binding the same port silently break the second one’s loop. Reading the port from .env.local per worktree means every agent’s dev server lands somewhere unique without coordination.
Branches and fetches are shared, so the workflow stays normal: each agent commits to its own branch, you pull/merge into main from the main checkout, and git fetch in any worktree updates the refs that all of them see. When an agent finishes, git worktree remove ../myapp-auth-rewrite cleans up, and the branch sticks around in the shared ref database until you delete it explicitly.
For Claude Code specifically, pointing each parallel session at its own worktree directory is the entire setup. The session inherits the working directory it was launched from; nothing else needs to change. Two sessions, two terminals, two cds into two different worktrees, and you have two agents that genuinely can’t interfere with each other’s files or processes.
6. Where worktrees stop scaling cleanly
Worktrees scale to “a handful of active branches per developer” cleanly. Past that, the failure modes start to show.
Orphaned worktree metadata accumulates. Manually deleting a worktree directory with rm -rf is the obvious move when an agent crashes or a task gets abandoned, but it leaves the administrative entries under .git/worktrees/ behind. git worktree list keeps showing the ghost until you git worktree prune. Adding git worktree prune to a shell startup or a periodic cron is a reasonable habit.
Stuck branch locks when an agent crashes mid-task. If an agent’s process dies with the branch checked out in a worktree, the branch is still considered “in use” by git and you can’t check it out elsewhere until the worktree’s gone. git worktree remove --force <path> clears it.
Disk cost compounds with per-tree build artifacts. Five worktrees of a Next.js app means five .next/ directories, five node_modules/ (unless you’re on pnpm), five sets of compiled TypeScript output. On a tight SSD this matters. The cheap fix is aggressive cleanup of inactive worktrees; the structural fix is package managers that share storage across trees the way pnpm and uv do.
IDE confusion is real. Opening the same project in two windows of the same IDE works for most editors, but a few (notably some JetBrains setups) get confused about which window owns the language server. Two separate user profiles or two separate editors avoid it. This is usually a one-time annoyance, not a recurring one.
Hooks that assume the main checkout’s path break in worktrees. A pre-commit hook that hardcodes /home/me/code/myapp/scripts/lint.sh will run the main checkout’s script regardless of which worktree triggered the commit. Use $(git rev-parse --show-toplevel) inside hooks instead, which returns the current worktree’s root.
These are the kind of friction that shows up after a week of heavy use and gets papered over with one or two shell aliases. The payoff is being able to run multiple coding agents in parallel without them corrupting each other’s state, and that’s large enough that the friction is easy to absorb. Once the per-worktree dev-server port and dependency story is sorted, scaling from one agent to three or four is mostly a matter of opening more terminals.