Skip to content

Run parallel agents in isolated worktrees

A Git worktree gives each parallel agent task its own directory and branch while all tasks share one repository history. Worktrees isolate files only. Two to four parallel agent streams also need a port block and local state per stream, an allowlist for local configuration, a gate each stream passes on its own, and cleanup that keeps unpushed work.

This page is for developers, and the tech leads who set up their repositories, answering Scorecard Q13. You started three agents in three terminals of the same checkout. One agent’s formatter rewrote the file another was editing, the second agent’s dev server quietly took the next free port, and the third agent’s end-to-end tests passed against the second agent’s server. Each report said “done”. None of them proved which code was tested.

Scorecard Q13: How do you run parallel agent sessions without file collisions?

Max-score evidence: a script, not a habit, creates and removes two to four isolated streams, each with an explicit base revision, its own runtime resources, and its own passing gates.

What you get from worktree isolation on this page

Section titled “What you get from worktree isolation on this page”
  • The native worktree command for Claude Code, Codex, and Cursor, with what each one does and does not carry into the new checkout.
  • Two tested repository scripts: one that creates a stream with a port block and development-only configuration, and one that removes a stream only when nothing would be lost.
  • A proof routine that shows which checkout a running server and a test run used, and which files two streams both touched, without reading the diffs.
  • Three copy-paste prompts: split the work, start a stream, and report a stream ready for review.

Parallel streams pay off when the tasks are independent and each has a check a machine can run. They cost you supervision: every stream ends in a diff someone must accept, so the limit is usually your review capacity, not the tool. Two to four streams is where one developer can still check every result against its gate. For ten or more agents with a supervisor terminal and a merge train, see running many agents at once.

Your situationRun it asWhy
Two or three fixes in different modules, each with a failing testParallel streams, one worktree eachSeparate files, separate gates
One hard task with several plausible designsThe same task in two worktrees, then keep the one with the smaller passing diffCompares approaches on equal tests
Two tasks that change the same API, schema, or config fileOne stream, in sequenceParallel work here guarantees a conflict and a second review
Side research or a read-only reviewA subagent inside one sessionNo second checkout needed; see scoped subagents

For the wider choice between worktrees, background agents, subagents, and scripted fan-out, see orchestration patterns for agent work.

What does a worktree isolate, and what does it not?

Section titled “What does a worktree isolate, and what does it not?”

A worktree has its own working files, index, and checked-out branch. It shares the .git directory (objects, refs, and git config) with every other worktree of the repository. Everything outside Git is shared unless you split it yourself:

ResourceSeparate per worktree?What goes wrong when it is shared
Tracked files and branchYesNothing: this is what worktrees solve
Gitignored files (.env, node_modules/)Missing from a new worktreeTests fail on missing config, or the agent invents values
Dev-server and test portsNoA server falls back to the next free port; tests reach another stream’s server
Local database, cache, and queueNoTwo streams migrate or seed the same data
git configNoOne worktree’s config write changes all of them
Claude Code permission approvalsNo“Yes, and don’t ask again” for a Bash command in one worktree is saved to the main checkout’s .claude/settings.local.json and applies everywhere (Claude Code v2.1.211 and later; on Windows the rule stays in that worktree)
Production data, deploys, secrets, and third-party APIsNoOne stream’s “quick check” writes live data

All three tools can create the worktree for you. They differ in where it goes, which branch it gets, and what it copies.

Terminal window
# Terminal, in the repository root. One terminal per stream.
claude --worktree fix-auth # or: claude -w fix-auth
claude --worktree csv-export --tmux # same, opened in a tmux session
claude --worktree "#1234" # worktree .claude/worktrees/pr-1234 at the head commit of pull request 1234

Claude Code creates the worktree under .claude/worktrees/<name>/ on a new branch worktree-<name>. By default it branches from the remote default branch (worktree.baseRef: "fresh"); set "worktree": { "baseRef": "head" } in settings to branch from your current local HEAD instead. Before the first run:

  • Add .claude/worktrees/ to .gitignore.

  • Create a .worktreeinclude in the repository root, in .gitignore syntax. Claude Code copies only files that match it and are gitignored, so list development files only:

    .env.development
    .env.test

On exit, Claude Code checks the worktree for changes and new commits: a clean worktree of an unnamed session is removed with its branch, and one with work in it prompts you to keep or remove it. Headless claude -p --worktree runs have no exit prompt and leave their worktrees behind. A custom subagent with isolation: worktree in its frontmatter always runs in its own temporary worktree. Checked against claude --help 2.1.283 and the Claude Code worktree documentation on 26 September 2026. The desktop app offers the same option per session; see Claude Code Desktop.

The native commands give you a separate checkout. They do not give you ports, a database per stream, or a record of the base revision. That is the repository’s job.

Write the isolation contract as a repository script

Section titled “Write the isolation contract as a repository script”

Q13’s top score asks for automation that any agent or teammate runs the same way. Commit two scripts. The first creates a stream from an explicit base, assigns the lowest free port block (stride 10, so a server that falls back to the next port stays inside its own block), writes the block to a gitignored .wt-env, and copies an allowlist of development files. Add .wt-env and every allowlisted file to .gitignore first; otherwise wt-rm.sh sees them as untracked and refuses to remove any stream:

#!/usr/bin/env bash
# scripts/wt-new.sh SLUG [BASE]: one agent stream = worktree + branch + port block + local config
set -euo pipefail
slug="${1:?usage: scripts/wt-new.sh SLUG [BASE]}"
base="${2:-origin/main}"
main="$(git worktree list --porcelain | awk 'NR==1 {print $2}')" # the main checkout is listed first
dir="$(dirname "$main")/$(basename "$main")-$slug"
# Lowest index no other worktree holds; index 0 is the main checkout
used="$(git worktree list --porcelain | awk '/^worktree /{print $2}' |
while read -r w; do sed -n 's/^WT_INDEX=//p' "$w/.wt-env" 2>/dev/null || true; done)"
i=1; while grep -qx "$i" <<<"$used"; do i=$((i + 1)); done
git fetch --quiet origin
git worktree add --quiet --no-track -b "agent/$slug" "$dir" "$base" # fails if the branch exists: pick a new slug
cat > "$dir/.wt-env" <<ENV
WT_INDEX=$i
WT_SLUG=$slug
WT_BASE=$(git rev-parse "$base")
APP_PORT=$((3000 + i * 10))
DB_PORT=$((5432 + i * 10))
ENV
for f in .env.development .env.test; do # allowlist: development files only
if [ -f "$main/$f" ]; then cp "$main/$f" "$dir/$f"; fi
done
echo "ready: $dir on agent/$slug from $(git rev-parse --short "$base"), APP_PORT=$((3000 + i * 10))"

The second removes a stream only when the worktree is clean and every commit on its branch exists on origin. Run it from the main checkout:

#!/usr/bin/env bash
# scripts/wt-rm.sh SLUG: remove a stream only when nothing would be lost
set -euo pipefail
slug="${1:?usage: scripts/wt-rm.sh SLUG}"
main="$(git worktree list --porcelain | awk 'NR==1 {print $2}')"
dir="$(dirname "$main")/$(basename "$main")-$slug"
if [ -n "$(git -C "$dir" status --porcelain)" ]; then
echo "refusing: uncommitted or untracked files in $dir" >&2; exit 1
fi
git fetch --quiet origin
if [ -n "$(git rev-list "agent/$slug" --not --remotes=origin)" ]; then
echo "refusing: agent/$slug has commits that exist on no origin branch; push them first" >&2; exit 1
fi
git worktree remove "$dir"
git branch -d "agent/$slug" 2>/dev/null || echo "kept branch agent/$slug (not merged; delete it after the PR lands)"

Both scripts were run against a scratch repository with Git on 26 September 2026: two streams received ports 3010 and 3020, a freed index was reused, and removal refused a branch with an unpushed commit until it was pushed. Replace origin/main with your default branch and the ports with your stack’s, and add the dependency install your project needs after the copy step.

Then make the tools read the file, so nobody has to remember an export. For example, a Vite and Playwright project derives every URL from the one value and refuses a fallback port:

playwright.config.ts
import { readFileSync } from 'node:fs';
import { defineConfig } from '@playwright/test';
const wtEnv = (() => {
try { return readFileSync('.wt-env', 'utf8'); } catch { return ''; }
})();
const port = /^APP_PORT=(\d+)$/m.exec(wtEnv)?.[1] ?? '5173'; // main checkout keeps the default
const baseURL = `http://localhost:${port}`;
export default defineConfig({
use: { baseURL },
webServer: {
command: `npx vite --port ${port} --strictPort`, // fail instead of moving to another port
url: baseURL,
reuseExistingServer: !process.env.CI,
},
});

Tying webServer.url to the stream’s own port matters most: with a hard-coded URL, reuseExistingServer attaches to whatever already listens there, which can be another stream’s server, and the run reports green against code it never tested.

Run two to four streams from split to merge

Section titled “Run two to four streams from split to merge”
  1. Split the work and check independence. Paste the planning prompt below into one session in the main checkout. Keep only tasks that touch disjoint files and have their own failing test or acceptance check; sequence the rest.

  2. Create one stream per task. Run scripts/wt-new.sh fix-auth, scripts/wt-new.sh csv-export, and so on, from the main checkout. The base revision is now recorded in each .wt-env.

  3. Start one agent per worktree. Open a terminal (or a Cursor window) in each directory and start the agent there with the stream prompt below. With Claude Code you can use claude --worktree NAME instead, provided .worktreeinclude and a port assignment cover what the script does. Such a stream lives on branch worktree-NAME; the overlap check below already covers worktree-* branches. Remove such a stream on exit or with git worktree remove .claude/worktrees/NAME, not with wt-rm.sh.

  4. Let the gates, not you, judge each stream. Each agent finishes only when the project’s type check, lint, and tests pass inside its worktree. Agent hooks can enforce this at the end of every turn.

  5. Prove each stream before review. Run the checks in the next section. A stream that cannot show its evidence goes back to its agent, not to a reviewer.

  6. Integrate one stream at a time. Push the branch with git push -u origin agent/SLUG, open a pull request, and let CI run. After the first pull request merges, rebase the next branch on the new default branch and rerun its gates before it merges. A review queue keeps this order when several people do it.

  7. Remove each stream with the script. scripts/wt-rm.sh fix-auth refuses to delete dirty or unpushed work. Run git worktree prune occasionally to drop records of worktree directories that were deleted by hand.

How do you prove a stream without reading its diff?

Section titled “How do you prove a stream without reading its diff?”

Ask for evidence a machine produced, then read only what the evidence flags. Four checks cover the ways parallel streams lie:

QuestionCheck (run in the terminal)Passes when
Did the stream start where you think?git -C ../myapp-csv-export merge-base HEAD origin/main against WT_BASE in .wt-envThe base is the recorded revision, or a later one you rebased onto on purpose
Did the gates run in this checkout?The agent’s report lists each command with its exit code; rerun the test command yourself in that directoryEvery exit code is 0 and your rerun matches
Is the running server this checkout’s?. ../myapp-csv-export/.wt-env; lsof -a -d cwd -p "$(lsof -tiTCP:"$APP_PORT" -sTCP:LISTEN | paste -sd, -)"The cwd shown is the stream’s own directory
Did two streams touch the same files?The overlap check below, from the main checkoutIt prints nothing
Terminal window
# Files changed by two or more stream branches since they left main
# (agent/* from the script, worktree-* from claude --worktree)
for b in $(git for-each-ref --format='%(refname:short)' refs/heads/agent/ 'refs/heads/worktree-*'); do
git diff --name-only "origin/main...$b"
done | sort | uniq -d

Any file the overlap check prints means two streams changed the same file. Stop one of them, merge the other first, and rebase the stopped stream before it continues. Before a pull request, ask each agent for the readiness report below and attach it; the pull request then carries its own proof, as in the evidence bundle. Who signs off stays the same as for human work: the reviewer named by your layered pull request review, reviewing the evidence and the files it flags.

What breaks when you run agents in parallel worktrees?

Section titled “What breaks when you run agents in parallel worktrees?”

Tests pass against another stream’s server. The dev server fell back to a free port, or reuseExistingServer attached to a neighbour. Recovery: stop every dev server, add --strictPort (or your framework’s equivalent) and derive the test URL from .wt-env, then rerun the stream’s gates. Treat any earlier green run from that stream as unproven.

The new worktree cannot build. Gitignored files and dependencies do not exist in a fresh checkout. Recovery: add the file to .worktreeinclude or the script’s allowlist and install dependencies in the worktree. Never widen the allowlist to production files to make it work.

Codex commits vanish after cleanup. Commits made on a detached HEAD belong to no branch. Recovery: before removing the worktree, run git -C DIR reflog -5, then git branch rescue/csv-export SHA from the main checkout. Prevent it with the git switch -c line in the stream prompt.

Git says the branch is already checked out. Git refuses to check out one branch in two worktrees. Recovery: run git worktree list to find the owner; if its directory was deleted by hand, run git worktree prune, then retry.

A worktree will not be removed. Headless claude -p --worktree runs have no exit prompt and leave their worktree, and the lock Claude Code took on it, in place (a later Claude Code session’s stale-lock sweep also releases it). Recovery: confirm with git -C .claude/worktrees/NAME status that nothing is left to keep (push any commits you want first), then run git worktree unlock .claude/worktrees/NAME, git worktree remove .claude/worktrees/NAME, and git branch -d worktree-NAME. For a stream the script created, use scripts/wt-rm.sh SLUG instead.

A permission granted in one stream applies to all of them. Claude Code saves “don’t ask again” rules to the main checkout’s .claude/settings.local.json. Recovery: review that file after a parallel session and move the rules you meant to keep into the committed .claude/settings.json.

Two streams changed the same contract. Independent tests both pass, but the merged result does not. Recovery: merge one, rebase the other, rerun its full gate, and add the contract to the planning prompt’s “must run in sequence” list next time.

Worktrees sit inside the Build stage of the AI-native lifecycle; the cross-tool map compares the tools’ parallel features side by side. The previous harness playbook is scoped subagents, and the next Scorecard question, Q14, is giving the session a feedback loop. All playbooks are listed in the Developer Scorecard guide.