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.
When should you run agents in parallel?
Section titled “When should you run agents in parallel?”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 situation | Run it as | Why |
|---|---|---|
| Two or three fixes in different modules, each with a failing test | Parallel streams, one worktree each | Separate files, separate gates |
| One hard task with several plausible designs | The same task in two worktrees, then keep the one with the smaller passing diff | Compares approaches on equal tests |
| Two tasks that change the same API, schema, or config file | One stream, in sequence | Parallel work here guarantees a conflict and a second review |
| Side research or a read-only review | A subagent inside one session | No 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:
| Resource | Separate per worktree? | What goes wrong when it is shared |
|---|---|---|
| Tracked files and branch | Yes | Nothing: this is what worktrees solve |
Gitignored files (.env, node_modules/) | Missing from a new worktree | Tests fail on missing config, or the agent invents values |
| Dev-server and test ports | No | A server falls back to the next free port; tests reach another stream’s server |
| Local database, cache, and queue | No | Two streams migrate or seed the same data |
git config | No | One worktree’s config write changes all of them |
| Claude Code permission approvals | No | “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 APIs | No | One stream’s “quick check” writes live data |
Start an agent in its own worktree
Section titled “Start an agent in its own worktree”All three tools can create the worktree for you. They differ in where it goes, which branch it gets, and what it copies.
# Terminal, in the repository root. One terminal per stream.claude --worktree fix-auth # or: claude -w fix-authclaude --worktree csv-export --tmux # same, opened in a tmux sessionclaude --worktree "#1234" # worktree .claude/worktrees/pr-1234 at the head commit of pull request 1234Claude 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
.worktreeincludein the repository root, in.gitignoresyntax. 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.
# Terminal, in the repository root. One terminal per stream.codex --worktree "Fix the CSV export header bug. Start with a failing test."
# Non-interactive, for a script:codex exec --worktree "Fix the CSV export header bug. Start with a failing test."Inside a running session, /worktree moves the conversation into a new worktree. Worktree support is on by default since codex-cli 0.156.0, but a session uses one only when you opt in. A managed Codex worktree sits under $CODEX_HOME/worktrees/ on a detached HEAD, and it carries no uncommitted edits, untracked files, or gitignored files. Codex 0.157.1 documents no .worktreeinclude equivalent. So create a branch before the agent commits, and provision configuration yourself, or create the checkout with the repository script below and start codex inside it. The measured details are in isolated Codex tasks with Git worktrees. Checked against codex --help and codex features list 0.157.1 on 26 September 2026.
Cursor’s documentation describes Worktrees as letting “Agent work in isolated Git checkouts” (cursor.com, checked 28 August 2026), and the Agents Window runs several agents side by side. The current UI steps could not be re-checked for this page, so follow the Agents Window and Projects in Cursor and Cursor’s Worktrees documentation.
When you need a fixed path, branch, and port block, create the checkout with the repository script below, then open that folder in its own Cursor window. The agent in that window then edits only that checkout.
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 configset -euo pipefailslug="${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 firstdir="$(dirname "$main")/$(basename "$main")-$slug"
# Lowest index no other worktree holds; index 0 is the main checkoutused="$(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 origingit worktree add --quiet --no-track -b "agent/$slug" "$dir" "$base" # fails if the branch exists: pick a new slug
cat > "$dir/.wt-env" <<ENVWT_INDEX=$iWT_SLUG=$slugWT_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"; fidone
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 lostset -euo pipefailslug="${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 1figit fetch --quiet originif [ -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 1figit 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:
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 defaultconst 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”-
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.
-
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. -
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 NAMEinstead, provided.worktreeincludeand a port assignment cover what the script does. Such a stream lives on branchworktree-NAME; the overlap check below already coversworktree-*branches. Remove such a stream on exit or withgit worktree remove .claude/worktrees/NAME, not withwt-rm.sh. -
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.
-
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.
-
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. -
Remove each stream with the script.
scripts/wt-rm.sh fix-authrefuses to delete dirty or unpushed work. Rungit worktree pruneoccasionally 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:
| Question | Check (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-env | The 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 directory | Every 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 checkout | It prints nothing |
# 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 -dAny 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.
Where to go next with parallel agents
Section titled “Where to go next with parallel agents”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.