Book a call
BUILD28mVERIFIED 2026-08-03 · CLAUDE CODE 2.1.220 · CODEX CLI 0.146.0 · GITHUB COPILOT CLI 1.0.77 · CURSOR 2026.07.23-e383d2b · ANTIGRAVITY CLI 1.1.9 · KIMI CODE CLI 0.31.1

Worktree isolation for agents that write the same files

Two agents in one checkout lose each other's edits silently. Give each one its own git worktree, seed the files git won't copy, and review one diff per agent.

Two agents, one checkout, one edit gone

We would say it's a collision within a shared checkout. Let's have a look:

Let's say we have two agents and we ask each of them to append a line to the same file, in a single directory. We start both of these at the same time. Both of these will read the file, wait for some time (one for 1 second, another one for 0.2 seconds) and write back what they've read with their own line.

TEXT
line 0
line from A

The result is that only A's line appears in the final version of the file, even though both processes return status code 0, and no warning is printed. What happened behind the scenes is that A wrote back a snapshot of the file before B's change, so effectively — B's change was lost. To prevent it you could provide these two agents with separate directories, everything else being the same, and they would happily work together.

Both GitHub and Google describe this problem in their documentation. GitHub's fleet-mode docs list what not to use it for, and the second bullet is "Tightly coupled edits where workers would contend for the same files"; their best-practice line is the one to take literally: "Avoid assigning overlapping files to different workers unless the parent agent will reconcile conflicts explicitly." Google say sth similar, more bluntly: "Multiple agents editing code at the same time can lead to conflicts and agents overwriting one another."

So it's like both of them say it's not recommended, but not enforced, as the file system is not aware of the fan-out.

"Isolated" usually means the context, not the disk

If you think about it, it's natural that the word "isolated" is used in regards to the context rather than files. Every tool "guarantees" isolation, but in their own way. For example:

  • Cursor tells you that it uses a separate git worktree for every run — its flag says "Start in an isolated git worktree"

  • Claude Code's documentation says worktrees "isolate file edits"

But often it's about the context, like:

  • Moonshot's docs for Kimi Code say that "Each sub-agent has a fully independent context window. It can only see the task description explicitly passed by the main Agent and cannot see the main Agent's conversation history." Then, under a heading about context isolation, they mention that "Multiple sub-agents can run in parallel without interfering with each other."

  • GitHub says every Copilot subagent has "its own context window, separate from the main agent and other subagents"

It's like these tools ensure isolation of what an agent knows — which is really valuable as the fan-out doesn't flood the main context with new information — but they don't say anything about files. In theory, two sub-agents with "clean" (in terms of context) contexts could have a handle to write to the same src/auth.ts.

So if you see that a tool says it can run things in parallel without any interference — make sure what kind of isolation this is about. If it's not mentioned, most probably it's about the conversation history.

What a worktree is

Git's own definition: "Create a worktree at <path> and checkout <commit-ish> into it. The new worktree is linked to the current repository, sharing everything except per-worktree files such as HEAD, index, etc."

In other words, a worktree is a separate copy of your repository's working files with its own HEAD and index (but sharing a single object store with the main checkout), which you can create using the git worktree add command. For example:

BASH
git worktree add ../wt-a -b agent/a
git worktree add ../wt-b -b agent/b
TEXT
/private/tmp/wt-check/repo  56d899e [main]
/private/tmp/wt-check/wt-a  56d899e [agent/a]
/private/tmp/wt-check/wt-b  56d899e [agent/b]

As you can see, inside the linked worktree, .git is a single line file pointing to the actual gitdir instead of a directory.

TEXT
$ cat ../wt-a/.git
gitdir: /private/tmp/wt-check/repo/.git/worktrees/wt-a

Let's try it — both worktrees rewrite the same a.ts and commit:

TEXT
* 0b83b15 a: bump
| * 9ad5e5a b: bump
|/
* 56d899e init

As you can see, both worktrees modified the same a.ts file and committed it — each to its own branch. The main checkout didn't change. The repository now contains two branches, each at its own commit, containing both lines of history.

Thanks to the shared object store, the work done in the worktree is part of your repository as soon as you commit it — no need to push anything, or clone it again to fetch.

What it shares, and what's simply missing

There are three things that worktrees share:

  • Branches — a branch can be checked out only once, in one worktree. If you try to checkout an already-checked-out branch in another worktree, it will throw an error.

TEXT
$ git worktree add ../wt-dup main
Preparing worktree (checking out 'main')
fatal: 'main' is already checked out at '/private/tmp/wt-check/repo'

git branch --list marks those with a leading +. It's the constraint OpenAI's docs spell out for their own worktree feature too: "a branch represents a single mutable reference", so a branch created on a worktree "can't be checked out in any other worktree, including your local checkout at the same time."

  • Config — if you set anything using git config --local (in the main checkout), it will be read by every worktree. Git has an extension that splits it (git config extensions.worktreeConfig true, then git config --worktree), but the docs warn that "Older Git versions will refuse to access repositories with this extension".

  • Hooks — there's a .git/hooks directory in the common area so a pre-commit script defined in the main checkout will run for every worktree

TEXT
$ (cd ../wt-a && git commit -am "a: b.ts")
pre-commit hook ran in /tmp/wt-check/wt-a

The upside of it is that your agents can also benefit from the linter. The downside is that if you have a pre-commit script in place (in the main checkout) that assumes it runs there, it's now wrong in 5 places at once.

There are also things that worktrees don't share — everything that git ignores by default like .env files, node_modules directory etc. A worktree only checks out files that are being tracked by Git, so you need to make sure these "ignored" things end up there somehow.

TEXT
$ ls -a ../wt-a
. .. .git .gitignore a.ts b.ts

Which is the biggest headache, because what your agents will tell you is that tests don't pass instead of pointing to a missing file.

There are two ways to address it:

  • Creating a .worktreeinclude file with paths of files that should be copied from the main checkout

  • Running a setup script in every new worktree (the tool might provide its own version of it)

The official Gemini documentation says "Remember to initialize your development environment in each new worktree according to your project's setup." for example.

Claude Code documents its own version of this list, and it's worth reading as a general shape — a worktree "shares the repository's .git directory, project-scope plugins, and saved permission approvals with the main checkout":

  • The repository .git directory

  • The project-scope plugins

  • Approvals (you can approve certain things once and they apply in the main checkout and in every other worktree)

Build it: one worktree per agent

We can have a script that creates a worktree and a branch per item, runs the command inside it, gives you one diff per agent at the end, and reaps with a cleanup pass that can't delete work. Four decisions in it are worth explaining — the first one because it was a bug in our own draft until a test run caught it.

The base ref is resolved to a commit before any worktree is created. Pass HEAD through to git worktree add and it means something different in every worktree — and something different again after the agent commits, at which point diff --stat HEAD is empty and the report cheerfully claims the agent changed nothing. One git rev-parse up front fixes it.

Worktrees are created serially, agents run in parallel. Creation is cheap, and doing it up front means a checkout that fails stops the run before a single agent has started. The agents are the part worth overlapping.

Each worktree is locked while its agent works. git worktree lock --reason makes any concurrent cleanup refuse to touch it, including the harness's own periodic sweep.

Nothing is forced. git worktree remove refuses a checkout with modified or untracked files, git branch -d refuses an unmerged branch, and both refusals are the point:

TEXT
$ git worktree remove ../wt-b
fatal: '../wt-b' contains modified or untracked files, use --force to delete it

Add the worktree directory to .gitignore, drop the script in scripts/, and let's try it — two agents told to append to the same file, each committing in its own checkout:

BASH
scripts/worktree-fanout.sh -p agent \
  'printf "// touched by %s\n" {item} >> a.ts; git commit -qam "{item}"; echo done' \
  one two

And a run looks like this:

TEXT
agent/one                      exit 0   /private/tmp/wt-check/repo/.worktrees/one.log
agent/two                      exit 0   /private/tmp/wt-check/repo/.worktrees/two.log

--- agent/one
     a.ts | 1 +
     1 file changed, 1 insertion(+)

--- agent/two
     a.ts | 1 +
     1 file changed, 1 insertion(+)

  branch kept: agent/one
  branch kept: agent/two

Both agents wrote the same file and both edits exist. Both of them exited with code 0 (as it was for the first example). The diffs are there too, one per agent. And if we have a look at the worktrees now — we will see that they're gone, because they were clean once the commits landed. Not so for the branches: they were kept, because git branch -d won't drop something unmerged. The run's output survived the run's cleanup, which is the only cleanup policy worth having.

Review N diffs, not one tree

There are two reasons for which it's better to review N diffs rather than one tree:

  • Avoiding collisions — we've covered it

  • The surface itself — with the fan-out, not only the number of things that need to be reviewed changes but also their shape. Let's say we have a single checkout — in this case, there's a single working tree (the main checkout) containing the sum of what 5 agents decided to do. There's no way you can attribute a hunk to an agent here. With worktrees — we have 5 branches, all against the same base commit, all diffs attributable and mergeable on their own; we can even get rid of 4 of them if they're not good.

For instance:

BASH
git worktree list                       # what's still out there
git diff <base>..agent/one              # one agent's committed work
git cherry-pick agent/two               # keep the good one
git worktree remove .worktrees/one      # refuses if it still holds work
git branch -d agent/one                 # refuses if it isn't merged; -D overrules

Also, the way you ask for a diff is important. If you do:

  • git diff <base>..<branch> — you will see what this agent has committed.

  • If you run diff inside the worktree against base, you will see what it did but didn't commit yet; if it finished but never committed, it won't be visible in the first form

If you have two branches that modify the same lines, you will get a regular merge conflict when trying to combine them. Which is good, because it's visible, located by git and decided by you, instead of the shared checkout silently keeping the last write.

The only thing worth remembering is that if you remove the worktree directory manually (by hand), the registry entry stays behind, marked as prunable. You need to run git worktree prune to get rid of it.

TEXT
$ git worktree list
/private/tmp/wt-check/wt-a  d4db1e2 [agent/a] prunable
$ git worktree prune -v
Removing worktrees/wt-a: gitdir file points to non-existent location

When it isn't worth it

Worktrees are not free — they take actual time and disk space, so you shouldn't be using them by default. There are a few scenarios in which it doesn't make sense to use them:

  • No writes — if you do an audit, or run a review panel, or sweep to do some research — they will all work well as fan-out over single checkout (5 copies of the repo buy you nothing but I/O)

  • Single agent — if you already have a main checkout, you don't need worktrees as you can use it as isolation; a worktree per session would give you an option to run another agent in the future, but not make the first run correct

  • Setup cost — if you need to install dependencies and warm up the build — you will pay for it N times, before every agent writes anything. For instance, the official Cursor documentation says that "We do not recommend symlinking dependencies into the worktree. This can cause issues in the main worktree" so they suggest using a fast package manager instead

  • Submodules — git's own BUGS section says "Multiple checkout in general is still experimental, and the support for submodules is incomplete. It is NOT recommended to make multiple checkouts of a superproject." If your repo is a superproject, you could try it in some disposable place first

IN YOUR HARNESS

Worktrees in Claude Code

The support for git worktrees in Claude Code is accessible via the --worktree flag, you can also shorten it to -w, and you can provide it with an optional name as follows:

BASH
claude --worktree feature-auth

Based on the docs, we checked it in our local repository. The layout is as follows: the worktree lands under .claude/worktrees/<name>/ at your repository root, on a new branch named worktree-<name>. If you do not provide the name, it will automatically generate a three-word label like bright-running-fox. So before using it for the first time, make sure to add .claude/worktrees/ to your .gitignore file.

In terms of how it works behind the scenes, for every custom subagent that you define in .claude/agents/ directory and mark there as isolated with an isolation: worktree key inside frontmatter, Claude Code creates a separate checkout during the run of such a subagent. Here's an example of how it could look for a refactoring subagent:

MARKDOWN
---
name: refactorer
description: Applies mechanical refactors across many files
isolation: worktree
---

Apply the requested refactor across every affected file, then run the tests
and report the results.

You can also tell Claude Code to use worktrees for agents during an ongoing session — the docs' own phrasing for it is to ask Claude to "use worktrees for your agents".

And in terms of the cleanup process, it's all handled automatically and with a bias towards safety:

  • if there are no changes in the subagent's worktree, it gets removed once the subagent is done

  • otherwise, the worktree is kept so that the next scheduled sweep won't remove it before the changes get committed

So generally speaking, the sweep ignores any worktrees containing modified or untracked files or with commits not pushed yet, and it also doesn't touch worktrees created via --worktree flag.

There are two additional options you can set that define how the fan-out will be arranged:

  • worktree.baseRef (set to "fresh" by default) — decides what's the base of the fan-out, "fresh" is pointing to the remote default branch, and "head" to the local HEAD, the latter is useful if you want your agents to work on changes you haven't commited yet

    JSON
    {
      "worktree": {
        "baseRef": "head"
      }
    }
  • .worktreeinclude (located at project root level) — copies the gitignored paths you list in it into every worktree Claude Code creates, "uses .gitignore syntax", and copies "only files that match a pattern and are also gitignored" so it doesn't duplicate any tracked files

We checked that one the direct way: put .env in a .worktreeinclude, ran a headless session with --worktree, and the file was there in the new checkout with its contents intact.

Gotcha: the documentation states that using -p flag causes the run to not clean up after itself so the worktree will remain locked. If you try to remove it with git after such a run, you won't be able to because of the lock:

TEXT
$ git worktree remove .claude/worktrees/wt-run
fatal: cannot remove a locked working tree, lock reason: claude session wt-run
(pid 51507 start Mon Aug  3 16:58:55 2026)
use 'remove -f -f' to override or unlock first

The error message will include information what is the reason for the lock including the session name, PID and start time. So in such scenarios, make sure to unlock first and then remove it. But also as a bonus, thanks to the reason for the lock containing the session and PID, you can combine git worktree list with ps to see which worktrees belong to your agents currently working on something and which are leftovers from previous scripted runs.

Worktrees in Codex CLI

Codex supports worktrees, but not in the CLI tool; the docs say "Worktrees are available only in Codex in the ChatGPT desktop app."

We checked and neither codex nor codex exec have a --worktree flag, for example (both version 0.146.0). So what the desktop app does is still relevant, as it has 2 habits that we could aim to emulate given the CLI doesn't implement any of these:

  • It creates worktrees under $CODEX_HOME/worktrees

  • It has a .worktreeinclude file listing the ignored paths to copy into new worktrees it manages (same as Claude Code). Also, it "automatically copies an ignored AGENTS.override.md into local managed worktrees", without even asking about it.

But we've found one limit here — "a branch represents a single mutable reference", so a branch created in a worktree "can't be checked out in any other worktree, including your local checkout at the same time."

So we would need to implement it by hand. It's -C, --cd <DIR>, which runs the entire session in a different directory. We could for example create a new branch and worktree there at HEAD, copy the env file there, and then run codex exec -C <path-to-the-worktree>:

BASH
git worktree add -b agent/auth .worktrees/auth "$(git rev-parse HEAD)"
cp .env .worktrees/auth/.env
codex exec -C .worktrees/auth "Harden src/auth.ts and commit"

Contrary to --add-dir <DIR>, which declares "additional directories that should be writable alongside the primary workspace", so it does the opposite of what we need — you aim to restrict access with a worktree, not to allow more access. You could combine -C with --ephemeral, which runs "without persisting session files to disk", if you want to do some work in multiple runs but never come back to these.

Gotcha: we wouldn't recommend fanning out your own agents if it comes to writing, even within a session, as it will just create new threads and there's no way of assigning them to their own directories from there — they all share one working tree, which is the "losing changes" setup. You can track an issue in the OpenAI repo about allowing spawn_agent to create sub-agents in a separate workspace or worktree directory, it's 23095, but still open. Until that's done, if you want to do some heavy writing we'd suggest creating a script that runs codex exec -C for each worktree from the command line.

Worktrees in GitHub Copilot CLI

The Copilot CLI doesn't have support for git worktrees. We checked it by using the copilot --help command on 1.0.77 and by examining GitHub's fleet mode documentation and didn't find any information about worktrees, shared filesystems or locking there. What it has instead is /fleet, which is a group of subagents running in parallel that have, in GitHub's words, "its own context window, separate from the main agent and other subagents". But if you have a look at the docs and the help output, neither of them mentions anywhere that these subagents are in a different directory, so going by what's documented they all operate under the working directory from which the command is invoked.

It also documents best practices for usage scenarios that assume 1:1 ratio, like "Multi-file refactors where each worker owns a file, package, or language SDK" or "Batch reviews where each worker checks a separate diff, module, or alert group". The only thing it warns about in terms of the fleet mode is not to create a scenario in which multiple workers operate on the same files — its avoid-list names "Tightly coupled edits where workers would contend for the same files" — but it's the responsibility of the user to make sure they don't do it (the docs literally say: "Avoid assigning overlapping files to different workers unless the parent agent will reconcile conflicts explicitly.").

This means that it's up to the orchestrator, not you, to decide how to split things and the tool doesn't enforce anything in this regard. If you want to have a parallelised write-heavy process, we'd recommend avoiding using /fleet during the session and instead invoking multiple instances of the CLI with a separate checkout per invocation by using the -C <directory> option, which the help text describes as "Change working directory before doing anything else". You could for example set up a shell script that creates a branch and a worktree per target file, copies .env in place, starts the Copilot processes in the background and waits for them to end.

BASH
for f in src/auth.ts src/billing.ts; do
  slug=$(basename "$f" .ts)
  git worktree add -b agent/"$slug" .worktrees/"$slug" "$(git rev-parse HEAD)"
  cp .env .worktrees/"$slug"/.env
  copilot -C .worktrees/"$slug" -p "Harden $f, then commit" &
done
wait

In such scenario, you don't have the .worktreeinclude file or setup hook, so it's your responsibility to include the config by copying it manually as part of the process. If you'd rather go for a read-heavy scenario, you can just use /fleet for reviews and research or cross-module triage (in which case you can't destroy anything anyway).

Gotcha: the headless Copilot runs need the proper tool permissions to be set up beforehand (the most permissive option being --allow-all-tools) so if you were to launch a fan-out this way, all of your worktrees will run with all the tools unlocked simultaneously. It's better to specify only the required tools using --allow-tool and --deny-tool. The worktree isolation only protects other agents from this one, it doesn't protect you from any of them.

Worktrees in Cursor

Of the six harnesses, cursor-agent is the most well-featured one when it comes to worktrees. Its --help shows three flags for them: --worktree, --worktree-base and --skip-worktree-setup.

TEXT
-w, --worktree [name]        Start in an isolated git worktree at
                             ~/.cursor/worktrees/<reponame>/<name>. If
                             omitted, a name is generated.
--worktree-base <branch>     Branch or ref to base the new worktree on
                             (default: current HEAD)
--skip-worktree-setup        Skip running worktree setup scripts from
                             .cursor/worktrees.json (default: false)

The first one creates isolated worktree with an optional name (it'll be automatically generated if not provided) under ~/.cursor/worktrees/<reponame>/<name>. The base is the branch or ref to use for this worktree, defaulting to the current HEAD. And the last flag is about ignoring setup scripts defined in the .cursor/worktrees.json file, by default they are being executed during worktree's creation.

By the way, the base is important because cursor-agent uses HEAD as a default value, which means that if you run it without --worktree-base it starts from whatever commits you have locally (including unpushed ones). Usually, it's what you want, but be mindful it's the opposite of what some other dev harnesses do by default, so it might be worth considering when migrating between them.

The main feature is a setup script defined in .cursor/worktrees.json under the key setup-worktree-unix (or setup-worktree-windows) or setup-worktree if you want to have a fallback for the other system. The value should be either an array of shell commands or path to a file with a script. You can use $ROOT_WORKTREE_PATH variable there pointing to the main checkout. For example:

JSON
{
  "setup-worktree": [
    "pnpm install",
    "cp $ROOT_WORKTREE_PATH/.env .env"
  ]
}

It installs dependencies using pnpm and copies .env from the root checkout. We ran it: the CLI printed a worktree setup panel and "Worktree setup complete.", the .env copy landed, and the marker file our script wrote was there too. The worktree itself was under ~/.cursor/worktrees/<reponame>/<name>, and on a branch called the same as the worktree (without any prefix). The agent automatically removes old worktrees periodically, the interval for that is defined by cursor.worktreeCleanupIntervalHours (defaults to 6h), and cursor.worktreeMaxCount limits it to 25 per machine. The IDE has this feature too, in form of skills: /worktree — continue chatting in another checkout; /best-of-n — run the same task for a few models, in separate worktrees.

Keep in mind though that the docs recommend against symlinking dependencies into the worktree — "We do not recommend symlinking dependencies into the worktree. This can cause issues in the main worktree" — so think twice before writing your setup script.

Gotcha, and we hit it on the first run: the worktree is created before the trust check. In a directory Cursor hadn't been trusted in, the run stopped with "To proceed, you can either: Run 'agent' interactively to decide / Pass --trust, --yolo, or -f if you trust this directory" — and the worktree was already on disk, empty of any agent work. Trust the workspace first (or accept the prompt interactively), or your fan-out leaves a trail of abandoned checkouts to reap.

Worktrees in Gemini CLI

Gemini has a gemini -w or --worktree flag (it is marked as experimental and bound to the feature flag), when this feature is disabled it refuses to create a worktree with an informative message but doesn't ask LLM for anything or create any worktrees:

TEXT
$ gemini -w gem-test -p "Reply ok"
The --worktree flag is only available when experimental.worktrees is enabled in your settings.

The feature can be enabled by setting the experimental.worktrees to true in the settings:

JSON
{
  "experimental": {
    "worktrees": true
  }
}

If it's enabled, then gemini --worktree feature-search creates .gemini/worktrees/feature-search in the repository.

The cleanup is on a user — "When you exit a worktree session (using /quit or Ctrl+C), Gemini leaves the worktree intact so your work is not lost" — so the worktrees are preserved until you remove them, and to remove them you can run:

BASH
git worktree remove .gemini/worktrees/feature-search --force
git branch -D worktree-feature-search

And you need to initialise dev environment in each new worktree: "Remember to initialize your development environment in each new worktree according to your project's setup."

We reckon Google's warning about parallel subagents is the case for turning this feature on: "Exercise caution with parallel subagents for tasks that require heavy code edits. Multiple agents editing code at the same time can lead to conflicts and agents overwriting one another."

Antigravity has no equivalent of this, you can only do agy --add-dir or --project, there's no agy -w there (for example on 1.1.9, agy --help says nothing about worktrees), so the way to work with it is to create worktrees with git and run a separate instance of the CLI in each dir.

Gotcha: we've also found that for example on 0.46.0, setting experimental.worktrees to true in the project's settings file doesn't enable the feature — even with this set to true it still refuses to create a worktree; tried both with and without --skip-trust. So we'd say to rather set it in user's settings as for example if someone clones your repo they might not be able to use it with this set to true in the project settings.

Worktrees in Kimi Code CLI

In the Kimi Code CLI there's no support for worktrees. When you run kimi --help on version 0.31.1 it doesn't list any flag for it, and the Moonshot sub-agent docs are silent about worktrees as well as per-sub-agent working directories (they only describe isolation based on the context window). There's no mechanism to assign a sub-agent its own directory in either the docs or the CLI.

Let's take a closer look at this part of the docs:

"Multiple sub-agents can run in parallel without interfering with each other."

The next paragraph defines what isolation means — that for a sub-agent it's about having its own separate context window (it only sees the task description given by the main agent, not the main agent's entire conversation history), so from that perspective the non-interference between multiple sub-agents is about the main agent's conversation history; if you were to run three coder sub-agents on a single file they would collide exactly like three shell scripts would.

There's also no flag like -C that would change the current directory in the style of cd, so what's left is --add-dir <dir> which defines an extra workspace directory, but doesn't change the session's current directory. What it means is that with Kimi Code you achieve isolation by using cd and launching a separate process per worktree. This for example is how you could create a worktree and a branch for every file, copy .env there and start an instance of kimi in the background for each worktree:

BASH
for f in src/auth.ts src/billing.ts; do
  slug=$(basename "$f" .ts)
  git worktree add -b agent/"$slug" .worktrees/"$slug" "$(git rev-parse HEAD)"
  cp .env .worktrees/"$slug"/.env
  (cd .worktrees/"$slug" && kimi -p "Harden $f, then commit" --output-format stream-json) &
done
wait

If you have slices that require different profiles (like some for read access and others for write access), you can use --agent <name> flag to pass a specific sub-agent per run, for example using explore for read-only slices and coder for the ones that need writing, so it's only the latter ones that require worktrees.

Gotcha, and it's the useful kind: if fan-out of sub-agents doesn't work as intended, it's possible to inspect what went wrong. The runtime state of sub-agents is saved in the agents/ subdirectory of the current session directory, with a separate directory per instance, and each of these directories contains wire.jsonl file which keeps chronologically-ordered prompts, messages history and the final state, so that's where you can find out if two sub-agents operated on the same file. The thing is that they don't raise an error in such scenarios, so it won't be visible from the transcript.

Three habits that outlive the tool

Whichever tool creates the worktree, the same three things go wrong:

  • Base ref — if you don't resolve it to a concrete commit before creating worktrees, HEAD will resolve differently in every worktree (and even differently after an agent commits), so if you run git diff --stat HEAD at the end, the report will say no changes for all agents that committed. So you need to do git rev-parse once

  • Ignored files — a fresh checkout doesn't have an .env file, so if you don't copy it (via .worktreeinclude, a setup script, or a couple of copy commands) on purpose, you will see the test suite being broken instead of a missing-file message

  • Cleanup — both git worktree remove and git branch -d refuse to remove things that contain work, and that refusal is what you want. Forcing it (--force, -D) is a human overruling them, so it belongs at a keyboard, not as a default in a script nobody is watching

THE FILEscripts/worktree-fanout.sh
BASH
#!/usr/bin/env bash
# One git worktree per agent, so parallel writes can't land on the same file.
#
#     scripts/worktree-fanout.sh 'claude -p "harden {item}" --output-format json' \
#         src/auth.ts src/billing.ts src/webhooks.ts
#
# Every item gets its own checkout on its own branch, the command runs in there,
# and you review one diff per branch instead of one tangled working tree.
#
#     -b <ref>     base every worktree on this ref   (default: HEAD)
#     -p <name>    branch prefix                     (default: fanout)
#     -d <dir>     where the worktrees go            (default: .worktrees)
#     -k           keep the worktrees when the run ends
#     --reap       remove finished worktrees under <dir> and exit
#
# {item} is substituted into the command. Gitignored files listed in
# .worktreeinclude get copied in, because a fresh checkout has no .env.
# Add the worktree directory to .gitignore before the first run.

set -uo pipefail

base=HEAD prefix=fanout root=.worktrees keep=0 reap=0

while [ $# -gt 0 ]; do
    case "$1" in
        -b) base=$2; shift 2 ;;
        -p) prefix=$2; shift 2 ;;
        -d) root=$2; shift 2 ;;
        -k) keep=1; shift ;;
        --reap) reap=1; shift ;;
        --) shift; break ;;
        -*) echo "unknown option: $1" >&2; exit 2 ;;
        *) break ;;
    esac
done

cd "$(git rev-parse --show-toplevel)" || exit 1

# Neither `git worktree remove` nor `git branch -d` is forced, and that refusal is
# the whole safety property: uncommitted work keeps its checkout, unmerged commits
# keep their branch. Whatever survives, survived on purpose.
reap() {
    git worktree list --porcelain |
        awk -v r="/$root/" '$1 == "worktree" && index($2, r) { print $2 }' |
        while read -r dir; do
            branch=$(git -C "$dir" branch --show-current)
            if git worktree remove "$dir" 2>/dev/null; then
                git branch -q -d "$branch" 2>/dev/null || echo "  branch kept: $branch"
            else
                echo "  worktree kept: $dir"
            fi
        done
    git worktree prune
}

[ "$reap" = 1 ] && { reap; exit 0; }

cmd=${1:-}; shift 2>/dev/null
[ -n "$cmd" ] && [ $# -gt 0 ] || { sed -n '2,19p' "$0"; exit 2; }

# Resolve the base once, here. Passed through as "HEAD" it would mean a different
# commit in every worktree — and a different one again after the agent commits.
base=$(git rev-parse --verify "$base") || exit 1

# The gitignored files a fresh checkout doesn't have. One path or glob per line, in
# the .worktreeinclude file Claude Code and Codex also read — and a path is only
# copied if git actually ignores it, so tracked files are never duplicated.
seed() {
    [ -f .worktreeinclude ] || return 0
    while read -r pattern; do
        case "$pattern" in ''|'#'*) continue ;; esac
        for path in $pattern; do
            [ -e "$path" ] || continue
            git check-ignore -q "$path" || continue
            mkdir -p "$1/$(dirname "$path")"
            cp -R "$path" "$1/$path"
        done
    done < .worktreeinclude
}

slug() { printf '%s' "$1" | tr -C '[:alnum:]._-' '-' | sed 's/^-*//;s/-*$//'; }

# Serial, because `git worktree add` writes to the shared .git and there's nothing
# to win by racing it. The agents are the part worth running at once.
for item in "$@"; do
    git worktree add --quiet -b "$prefix/$(slug "$item")" "$root/$(slug "$item")" "$base" || exit 1
    seed "$root/$(slug "$item")"
done

for item in "$@"; do
    branch=$prefix/$(slug "$item")
    dir=$PWD/$root/$(slug "$item")
    (
        # Locked while the agent works, so a cleanup pass — ours, or the
        # harness's own — can't pull the checkout out from under it.
        git worktree lock --reason "agent running" "$dir"
        cd "$dir" && eval "${cmd//\{item\}/$item}" >"$dir.log" 2>&1
        status=$?
        git worktree unlock "$dir"
        printf '%-30s exit %-3d %s\n' "$branch" "$status" "$dir.log"
    ) &
done
wait

# One diff per agent, all against the commit they started from. Uncommitted work
# included, since an agent that didn't commit still changed files.
for item in "$@"; do
    echo
    echo "--- $prefix/$(slug "$item")"
    git -C "$root/$(slug "$item")" --no-pager diff --stat "$base" | sed 's/^/    /'
done

[ "$keep" = 1 ] || { echo; reap; }
j / k to move between lessons