Developer tooling · May 2026

Running a team of agents on one repo

Several coding agents on the same project, each building a whole feature on its own branch and opening a PR. None of them stepping on each other.

Here’s the setup I’ve landed on: several coding agents working on the same project at the same time, each one building a whole feature on its own branch and opening a PR when it’s done. I review the PRs, merge the good ones, close the rest.

The agents never step on each other. No fighting over the same files, no killing each other’s dev server, no one agent’s test run wiping another agent’s database. Getting there took three things: a separate checkout per agent, checkouts that actually run (env files and all), and a project designed so each checkout gets its own database and services.

1. One checkout per agent

You can’t point three agents at the same folder. They’ll edit the same files, switch branches under each other, and generally make a mess. Git worktrees fix this. Each worktree is its own folder with its own branch, all sharing the same repo.

But a fresh worktree only has your tracked files. Your .env, .envrc, local secrets, none of that comes along, because git doesn’t track it (and shouldn’t). So every new worktree blows up on a missing env var, and if an agent is the one running it, you might not notice until you read the logs.

That’s the gap wtm fills. You list the local files that make your repo runnable once, and every worktree it creates gets them copied in.

go install github.com/IdrisHanafi/wtm/cmd/wtm@latest

cd ~/code/shop
wtm init          # pick the files every worktree needs
wtm files test    # see what would get copied

That writes a .worktreefiles, using .gitignore syntax:

.env
.env.local
.envrc
config/dev.secret.exs

Commit it. It’s just file names, no contents, and now everyone on the team (and all their agents) gets runnable worktrees too.

$ wtm create fix-billing --from main

Created worktree

Name:   fix-billing
Branch: fix-billing
Path:   /Users/you/code/shop-fix-billing
Copied: 3 file(s) from .worktreefiles

2. One pane per agent

To keep an eye on everyone, I put each agent in its own tmux pane. This little function does the work. Drop it in your ~/.zshrc or ~/.bashrc:

tpane() {
  local session="$1"; shift
  local dir="$PWD"
  [[ "$1" == "-c" ]] && { dir="$2"; shift 2; }
  local cmd="$*"
  if tmux has-session -t "$session" 2>/dev/null; then
    tmux split-window -t "$session" -c "$dir" "$cmd"
    tmux select-layout -t "$session" tiled >/dev/null
  else
    tmux new-session -d -s "$session" -c "$dir" "$cmd"
  fi
}

You give it a session name, an optional -c dir to start in, and a command. The first call creates the session in the background. Every call after that splits a new pane and re-tiles them into a grid.

wtm create --print-path prints only the worktree path (the chatty output goes to stderr), so the two fit together nicely:

tpane agents -c "$(wtm create --print-path fix-billing --from main)" \
  "claude --permission-mode acceptEdits 'fix the rounding bug in invoice totals'"

New branch, new worktree with env files, agent running inside it. One line.

One gotcha: if wtm create fails, $(...) comes back empty and the agent starts in whatever folder you’re in, which might be your main checkout. So I wrap it:

# wtp <name> <command...>
# makes a worktree and a pane, or nothing if the worktree fails
wtp() {
  local name="$1"; shift
  local dir
  dir="$(wtm create --print-path "$name" --from "${WTP_BASE:-main}")" || return 1
  tpane "${WTP_SESSION:-agents}" -c "$dir" "$@"
}

3. Let each agent finish a whole feature

A normal prompt gets you one round of work. The agent does a thing, stops, and waits for you. That’s fine for small fixes, but if you want five agents each shipping a full feature, you don’t want to babysit five panes.

Claude Code’s /goal handles this. You give it a finish line, and the agent keeps going until it gets there. So the goal for each agent ends with “open a PR”:

wtp csv-export "claude --permission-mode acceptEdits '/goal add CSV export to the orders page. done when tests pass and a PR is open for this branch'"

Now you can fan out a whole batch of features:

#!/usr/bin/env zsh
source ~/.zshrc   # or wherever tpane and wtp live

agent="claude --permission-mode acceptEdits"
done_when="done when the tests pass and a PR is open for this branch"

while IFS='|' read -r name feature; do
  wtp "$name" "$agent '/goal $feature. $done_when'" || echo "skipped $name" >&2
done <<'EOF'
fix-billing|fix the rounding bug in invoice totals
search-index|add a trigram index for product search
feat/csv-export|add CSV export to the orders page
cart-tests|raise test coverage of the cart service
EOF

tmux attach -t agents

Attach and you’ve got four agents tiled in a grid, each on its own branch, each heading for its own PR. Branch names with slashes are fine. feat/csv-export stays as the branch name and the folder becomes shop-feat-csv-export.

Heads up: for an agent to push and open a PR on its own, it needs permission to run git push and gh pr create. Add those to your Claude Code permissions, or it’ll sit there waiting for you to approve them.

4. The part people skip: isolated databases

Separate folders and env files get you most of the way. But there’s one more place agents collide, and it’s sneaky: shared state.

Your .env probably says something like DATABASE_URL=postgres://localhost:5432/app_test. wtm copies that into every worktree, which is correct, but now every agent’s integration tests hit the same database. Agent A truncates tables while agent B’s tests are halfway through. You get flaky failures that have nothing to do with the code, and an agent will happily spend twenty minutes “fixing” a bug that doesn’t exist.

The fix is in how the project is designed. Every copy of the project should be able to spin up its own database and services without knowing about any other copy.

At The Grid, this is how we ran tests for parallel agents. One make target spun up Postgres and the test runner itself in containers, under a compose project named after a hash of the worktree’s path. Each worktree got its own containers, its own network and its own build cache volume, and the database lived in RAM, so there was nothing to clean up. No host ports at all: the tests ran inside the same private network as the database. Ten worktrees could run their test suites at once and none of them could see each other.

Here’s a smaller version of that with Docker Compose, where the tests still run on your machine. First, don’t pin host ports, so Docker picks a free one each time:

# docker-compose.test.yml
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: app_test
    ports:
      - "5432"          # no fixed host port, docker picks a free one
    healthcheck:
      # -h forces TCP, so the init-time socket-only server doesn't count as ready
      test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U postgres -d app_test"]
      interval: 1s
      retries: 30
  redis:
    image: redis:7
    ports:
      - "6379"

Then the test script names the compose project after the worktree and looks up whatever ports Docker handed out:

#!/usr/bin/env bash
# scripts/integration-test.sh
set -euo pipefail

compose="docker compose -f docker-compose.test.yml"

# one compose project per worktree: shop-3f9c2a1b7d04, ...
# (hash of the worktree path, so it's unique even if a branch is renamed)
export COMPOSE_PROJECT_NAME="shop-$(git rev-parse --show-toplevel | shasum | cut -c1-12)"

trap '$compose down -v' EXIT   # set before up, so a failed start still cleans up
$compose up -d --wait

# `compose port` prints e.g. 0.0.0.0:55012; keep just the port
db_port="$($compose port db 5432)";       db_port="${db_port##*:}"
redis_port="$($compose port redis 6379)"; redis_port="${redis_port##*:}"
export DATABASE_URL="postgres://postgres:postgres@127.0.0.1:$db_port/app_test"
export REDIS_URL="redis://127.0.0.1:$redis_port"

go test -tags=integration ./...

Because the project name comes from the worktree’s path, every worktree gets its own set of containers automatically. The DATABASE_URL from .env gets overridden with the real one, so even though wtm copied the same file everywhere, every agent ends up talking to its own database. And down -v throws it all away when the tests finish.

Tell your agents to run tests through this script (put it in your CLAUDE.md or AGENTS.md) and they can’t step on each other even if they try. The same idea works for dev servers. Don’t hardcode ports, derive things from the branch, and make sure everything a worktree spins up is namespaced to it.

5. Reviewing and cleaning up

When PRs start landing, review them like you would anyone else’s. Merge the good ones, close the ones that missed. Then clean up:

wtm list                                  # see all your worktrees
tmux kill-session -t agents               # stop the agents

wtm delete fix-billing                    # merged, drop the folder
wtm delete search-index --delete-branch   # didn't make it, toss everything

wtm delete asks before removing anything. Pass --force if you’re scripting it.

6. The takeaway

Running agents in parallel turned out to be mostly a workspace problem. Once every agent gets its own folder, its own env, and its own database, you can spin up five of them, let them work, and keep the PRs that turn out well.

wtm handles the folder and the env. Your project design handles the database. tpane and /goal take care of the rest. It’s open source at github.com/IdrisHanafi/wtm. Give it a try and let me know how it goes.