Jun 18, 20267 min read/2026/06/18/fanning-out-coding-agents-across-git-worktrees/

A Real Agent Fan-Out: One Task, N Worktrees, Pick the Winner

In the last post I made a claim: git
worktrees are the substrate that makes safe parallel coding agents possible, because each agent gets a
real, isolated working directory backed by one shared history. That post was the why. This one is the
how — a small bash script you can actually run, with the real output pasted in, that turns the
"generate several attempts and keep the best one" idea into something concrete.

The pattern has a name in the AI world — best-of-N, or generate-and-select. Instead of asking one
agent to solve a task and hoping, you launch several agents on the same task in parallel, let them
diverge, then compare what they produced and merge the one you like. It works because a good attempt and a
bad attempt cost you the same wall-clock time when they run at once, and because choosing between
finished diffs is far easier than predicting which prompt will land. The thing that makes it clean and
safe is exactly what the last post was about: give each attempt its own worktree.

The script

Here's the whole thing. It takes a count N and a command — the command is your agent invocation, but
it can be literally anything that edits files — and it does the rest:

#!/usr/bin/env bash
# fan-out.sh — run the same task in N isolated git worktrees, in parallel,
# then show a diff summary so you can pick a winner. Usage:
#   ./fan-out.sh <n> <command...>
set -euo pipefail

N="${1:?usage: fan-out.sh <n> <command...>}"; shift
[ "$#" -gt 0 ] || { echo "need a command to run in each worktree"; exit 1; }
TASK=("$@")

repo_root="$(git rev-parse --show-toplevel)"
base="$(git rev-parse --abbrev-ref HEAD)"
stamp="$(date +%Y%m%d-%H%M%S)"
runs_dir="$(dirname "$repo_root")/$(basename "$repo_root")-fanout-$stamp"
mkdir -p "$runs_dir"

echo "fanning out $N runs of: ${TASK[*]}"
echo "base branch: $base"

pids=()
for i in $(seq 1 "$N"); do
  branch="fanout/$stamp/$i"
  dir="$runs_dir/run-$i"
  git worktree add -q -b "$branch" "$dir" "$base"
  (
    cd "$dir"
    FANOUT_RUN="$i" "${TASK[@]}" >task.log 2>&1 || echo "[task exited $?]" >>task.log
    git add -A
    git commit -q -m "fanout run $i" --allow-empty
  ) &
  pids+=($!)
done
for pid in "${pids[@]}"; do wait "$pid"; done

echo
echo "=== results ($base vs each run) ==="
for i in $(seq 1 "$N"); do
  branch="fanout/$stamp/$i"
  echo "--- run $i  [$branch] ---"
  git -C "$repo_root" diff --stat "$base".."$branch" | sed 's/^/    /'
done

echo
echo "to inspect:  git diff $base..fanout/$stamp/<i>"
echo "to keep:     git merge fanout/$stamp/<i>"
echo "to clean up: $runs_dir  (and the fanout/$stamp/* branches)"

The scripts from this whole series — fan-out.sh, judge.sh, and orchestrate.sh — live in one
place if you'd rather clone than copy-paste: github.com/egarim/agent-fanout
(MIT, includes a runnable demo).

Let me point out the few lines that carry the whole idea:

  • base="$(git rev-parse --abbrev-ref HEAD)" — every run branches from the same starting point, so the
    attempts are genuinely comparable. They all begin from an identical, consistent tree.
  • git worktree add -q -b "$branch" "$dir" "$base" — the heart of it. Each run gets its own new branch
    and its own directory. This is the isolation: run 3 cannot see or stomp on run 1's edits.
  • The ( … ) & subshell — each task runs in a background subshell, so all N go at once. We collect the
    PIDs and wait for the lot.
  • FANOUT_RUN="$i" — a small courtesy: each run knows its own index, in case your agent wants to seed
    itself differently (a different temperature, a different prompt variant, a different approach hint).
  • The commit at the end snapshots whatever the task did, so the result is a branch you can diff and merge
    like any other.

Running it for real

I didn't want to just describe this, so I ran it against a throwaway repo. To stand in for a real agent
— which would be non-deterministic — I used a trivial "agent" that appends its run index to a file, so
each run diverges the way real attempts would. Here's the actual output:

$ ./fan-out.sh 3 /tmp/fake-agent.sh
fanning out 3 runs of: /tmp/fake-agent.sh
base branch: main

=== results (main vs each run) ===
--- run 1  [fanout/20260618-005926/1] ---
    data.txt | 1 +
    task.log | 1 +
    2 files changed, 2 insertions(+)
--- run 2  [fanout/20260618-005926/2] ---
    data.txt | 1 +
    task.log | 1 +
    2 files changed, 2 insertions(+)
--- run 3  [fanout/20260618-005926/3] ---
    data.txt | 1 +
    task.log | 1 +
    2 files changed, 2 insertions(+)

to inspect:  git diff main..fanout/20260618-005926/<i>
to keep:     git merge fanout/20260618-005926/<i>

And git worktree list confirms three independent checkouts, each parked on its own branch, all backed by
the one repo:

$ git worktree list
/private/tmp/fanout-demo               81bbd2a [main]
…/fanout-demo-fanout-…/run-1  9ff249e [fanout/20260618-005926/1]
…/fanout-demo-fanout-…/run-2  af124ce [fanout/20260618-005926/2]
…/fanout-demo-fanout-…/run-3  c01f4a6 [fanout/20260618-005926/3]

The isolation is real, not theoretical — diffing two runs against each other shows they diverged
independently, exactly as competing agent attempts would:

$ git diff fanout/…/1..fanout/…/3 -- data.txt
-run 1 did some work
+run 3 did some work

Picking a winner, and cleaning up

Now you do the part no script can do for you: look at the diffs and choose. For three small attempts,
git diff main..fanout/<stamp>/<i> on each is enough — read them, decide which solved the task best, and
merge it:

git merge fanout/20260618-005926/2     # run 2 wins

That brings the chosen attempt onto your branch as an ordinary merge. The other two are just branches you
never merged — harmless, and about to be deleted.

Cleanup matters, and it's the step people forget. Worktrees are cheap but not free: each one is a real
directory on disk, and git keeps a bookkeeping record of every one until you tell it otherwise. Leave them
lying around and you'll have a yard full of half-finished checkouts and fanout/* branches within a week.
The teardown is three lines:

stamp=20260618-005926
for i in 1 2 3; do git worktree remove --force "../<repo>-fanout-$stamp/run-$i"; done
git worktree prune                       # clear any stale records
for i in 1 2 3; do git branch -D "fanout/$stamp/$i"; done

I ran exactly this against the demo and confirmed it leaves git worktree list showing only main and no
fanout/* branches behind. Clean slate.

The honest caveats

This is a teaching script, not a framework — and the gaps are instructive, because they're the same ones
any real harness has to handle:

  • Dependencies aren't free per worktree. As I covered last time, a fresh worktree has no
    node_modules, no bin/obj, no .env — those are gitignored and never travel with the checkout. If
    your task needs to build or run, add an install step inside the subshell (npm ci, dotnet restore,
    etc.) before the agent runs. For N runs that's N installs; a shared package cache (pnpm, a warm NuGet
    cache) takes most of the sting out.
  • Non-determinism is the whole point — and the whole cost. Best-of-N only helps if the attempts
    actually differ, which means real agents with some variation between them. And N attempts cost roughly N
    times the tokens and compute of one. Fan out to 3–5 for a gnarly task where getting it right matters;
    don't fan out to 10 for a one-line fix.
  • Judging is the hard part, and it's still on you. The script gets you N clean diffs to compare; it
    can't tell you which is correct. For small diffs you eyeball them. As they grow you'll want tests as
    the referee — run the suite in each worktree and let green/red do the first cut — or even an
    LLM-as-judge pass. But a human (or a test) deciding the winner is load-bearing; don't automate it away
    blindly.
  • It's single-machine and synchronous. Everything runs on your box and the script blocks until all
    runs finish. That's fine for a handful; beyond that you're reinventing a job queue, which is your cue to
    reach for a real harness.

And that's the punchline. What this forty-line script does by hand is exactly what a mature agent harness
does for you automatically — Claude Code's workflow orchestration, for instance, fans tasks out across
isolated worktrees, runs them concurrently, and tears down the ones that changed nothing, all without you
writing a line of bash. Seeing the manual version is worth it anyway, because now the magic isn't magic:
it's git worktree add in a loop, a wait, and a diff. Once you've watched it work on a throwaway repo,
you understand precisely what your tools are doing on your behalf — and that is the best position from
which to trust them with real work.