Skip to content

Running AI coding agents in parallel with git worktrees

Three agents on one checkout means trampled files and lost work. Giving each task its own git worktree, its own model and a deterministic merge step fixed that for me.

I regularly have several coding agents working on the same codebase at once. One refactors a module, one writes tests, one bumps dependencies. The first time I tried this on a single checkout, two agents edited the same file within a minute of each other, one of them ran a formatter over the whole project, and I spent an hour reconstructing what each had meant to do.

The fix turned out to be a git feature that has existed since 2015: worktrees. This post covers how I use them by hand, and what I am building on top of them in worktree-orchestrator.

One repository, many working directories#

A worktree is an extra working directory attached to the same repository. It shares the object database and refs, but has its own checked-out branch, its own index and its own files on disk.

# from the main checkout
git worktree add -b agent/refactor-auth ../app-refactor-auth main
git worktree add -b agent/update-deps ../app-update-deps main

git worktree list
# /home/me/app                  a1b2c3d [main]
# /home/me/app-refactor-auth    a1b2c3d [agent/refactor-auth]
# /home/me/app-update-deps      a1b2c3d [agent/update-deps]

Each agent gets started in its own directory. They cannot see each other's uncommitted changes, they cannot overwrite each other's files, and each one's work ends up as a normal branch with normal commits. Compared with cloning the repo three times, worktrees are faster to create, use far less disk, and every branch is immediately visible from the main checkout.

A few rules I learned the hard way:

  • A branch can only be checked out in one worktree. That is a feature. It stops two agents from committing to the same branch.
  • Give every task a branch name with a prefix, like agent/, so cleanup is easy to script.
  • Install dependencies per worktree. node_modules and vendor are per directory. Some package managers can share a cache, which helps.
  • Watch shared resources. Worktrees isolate files, not ports, databases or caches. Two agents running a dev server on port 8000 will still collide. I give each worktree its own .env with a different port and SQLite file.
  • Clean up. git worktree remove ../app-update-deps when a task is merged, and git worktree prune for directories that were deleted by hand.

Isolation is half the problem#

Worktrees solve the "trampled files" problem. They do not solve the merge. Three branches that each touched routes/web.php will still conflict when you bring them back together.

What I wanted was the workflow CI gave us years ago: isolated work, visible status, and a merge strategy that is not "hope". That is what worktree-orchestrator does:

two plan            # define tasks and models
two run             # one worktree per task, agents run in parallel
two status          # every task, its worktree, model and state
two merge --task 2  # reconcile and merge a finished task
two clean           # prune merged or abandoned worktrees

Declaring intent up front#

Tasks live in a plan file. The important field is paths: the files a task is expected to touch.

tasks:
  - id: refactor-auth
    prompt: "Refactor the auth module to use the strategy pattern"
    model: strong
    paths:
      - src/auth/**

  - id: update-deps
    prompt: "Bump dependencies and fix breaking changes"
    model: fast
    paths:
      - package.json
      - package-lock.json

Declaring paths does two things. It lets the orchestrator warn you before starting if two tasks plan to touch the same files, which is usually a sign they should be one task or run in sequence. And after the run, it lets the orchestrator compare what each agent actually changed against what it said it would change. An agent that was asked to bump dependencies and edited src/auth/session.ts deserves a closer look.

Routing models per task#

Not every task needs the most capable model. Bumping a version number and fixing an import is mechanical. Redesigning an authentication flow is not. The model field lets you route by task: a fast, cheap model for boilerplate and a strong model for the hard part. The names map to whatever CLI agents you have configured, so the same plan can drive different tools.

In practice this is also how I split work between different agents. Some are better at large refactors, some at careful, small edits, some at writing tests. Parallel worktrees let me use each for what it is good at without them interfering.

The reconciler is not an LLM#

This is the design decision I feel strongest about. When finished branches come back, merging is done by a neutral reconciler with ordered, explainable rules, not by a model deciding how to combine two diffs.

The rules today are simple:

  1. If a task's changed files do not overlap with any other unmerged task, merge it.
  2. If they overlap, stop and put it in a review queue with a diff-first report.
  3. Never rewrite an agent's changes silently.
function planMerge(finished: Task[], merged: Set<string>): MergeDecision[] {
  const touched = new Map<string, string>();

  return finished.map((task) => {
    const overlap = task.changedFiles.filter((file) => touched.has(file));
    task.changedFiles.forEach((file) => touched.set(file, task.id));

    if (overlap.length === 0) {
      return { task: task.id, action: "merge" };
    }

    return { task: task.id, action: "review", reason: `overlaps ${overlap.join(", ")} with ${touched.get(overlap[0])}` };
  });
}

Why not let a model resolve conflicts? Because a conflict is exactly the moment when two intentions disagree, and I want a person to see that. A model that merges confidently will sometimes produce code that compiles, passes the tests it knows about, and quietly drops one agent's change. Deterministic rules can be wrong too, but they are wrong in the same way every time, and you can read them.

The habits that make it work#

Tooling helps, but most of the value comes from a few habits that apply with or without an orchestrator:

  • Small tasks. An agent task should be something you could review in ten minutes.
  • Commit early inside the worktree. Each milestone becomes a commit, so a crash or a bad turn loses minutes, not hours.
  • Never let one agent clean up another's work. No git stash, reset or checkout across worktrees. If something looks abandoned, ask before discarding it.
  • Share memory, not files. Agents in separate worktrees still need to know what the others decided. I keep that in a shared, git-backed memory (sessions, task leases, decisions), which is the subject of my next post.
  • Verify before you trust "done". Run the tests in the worktree yourself before merging. An agent reporting success is a claim, not evidence.

Status#

worktree-orchestrator is v0.x. Worktree creation, parallel execution and clean merging of disjoint path sets work. The live conflict watcher, the TUI dashboard, the review queue for overlapping merges and router plugins for more CLI agents are in progress.

If you already run multiple agents, start with plain git worktree add today. It costs nothing, and it removes the most common way parallel agents lose work.