AI Coding Agent Worktrees: Isolate Ports, Test Data, and Background Jobs
Running two coding agents in separate Git worktrees feels safe - until both start the app on port 3000, seed the same database, or drain the same test queue. The branches are isolated, but the running system is not.
That is how an agent can pass a test against another agent's data and leave you debugging a failure neither diff contains.
The fix is to give every worktree a small, deterministic environment identity. Derive ports, database namespaces, queue names, object-storage prefixes, and temporary directories from it. Then make the environment visible in logs and easy to destroy.
This guide builds that pattern with ordinary Git, shell scripts, and environment variables. It works whether a human, one coding agent, or several agents start the worktrees.
A worktree protects files, not shared state
A Git worktree gives each task a checkout and branch. That prevents an agent from overwriting another agent's uncommitted edit. It does not change resources outside that directory.
| Resource | Typical collision | Isolation target |
|---|---|---|
| Dev server | second process cannot bind port 3000 |
derived port |
| Database | one test deletes another test's rows | database or schema namespace |
| Queue | worker consumes another task's job | key and queue prefix |
| Object storage | fixtures overwrite each other | object prefix |
| Temp files | snapshot or upload collision | worktree temp directory |
| Worker | stale process keeps processing | named PID and lease |
This distinction matters with AI coding agents. They run setup commands, seed fixtures, start watchers, and retry failed tests quickly. A successful command is not evidence that it used the intended environment.
One task, one branch, one worktree, one environment ID. Worktrees do not replace task contracts or a serial merge queue. They make one task's experiments trustworthy. Plan dependencies before dispatching agents, and validate the combined change before merging.
Derive an environment ID that survives a restart
Do not let an agent invent a random port or database name in its prompt. The next session cannot reproduce it, and cleanup becomes guesswork. Instead, derive a readable ID from the branch plus a stable path hash.
Create scripts/worktree-env.sh:
#!/usr/bin/env bash
set -euo pipefail
root="$(git rev-parse --show-toplevel)"
branch="$(git -C "$root" branch --show-current 2>/dev/null || true)"
if [ -z "$branch" ]; then branch=detached; fi
slug="$(printf '%s' "$branch" | tr '[:upper:]' '[:lower:]' | sed -E 's#[^a-z0-9]+#-#g; s#(^-|-$)##g' | cut -c1-32)"
hash="$(printf '%s' "$root" | cksum | awk '{print $1}')"
short_hash="$(printf '%s' "$hash" | cut -c1-4)"
offset=$(( hash % 400 ))
export WORKTREE_ID="$slug-$short_hash"
export APP_PORT=$(( 4100 + offset ))
export TEST_DB="app_test_$WORKTREE_ID"
export QUEUE_PREFIX="wt:$WORKTREE_ID:"
export OBJECT_PREFIX="worktrees/$WORKTREE_ID/"
export TMPDIR="$root/.tmp/$WORKTREE_ID"
mkdir -p "$TMPDIR"
The identifiers are readable, while the port is stable for that worktree. If your organization creates identical branch names in multiple linked copies, include the worktree path in the hash. Determinism is the key property: the same worktree should receive the same names until it is removed.
Load the file before local commands:
source scripts/worktree-env.sh
printf 'Environment: %s on port %s\n' "$WORKTREE_ID" "$APP_PORT"
pnpm dev --port "$APP_PORT"
Keep this script in the repository, not only in an agent instruction file. Reviewers and human contributors need the same behavior.
Route every dependency through the environment manifest
An ID helps only when application configuration consumes it. Avoid a half-isolated setup where the HTTP server has a unique port but the worker still reads the default queue.
const required = (name: string) => {
const value = process.env[name];
if (!value) throw new Error("Missing " + name + "; load worktree-env.sh");
return value;
};
export const runtime = {
worktreeId: required("WORKTREE_ID"),
port: Number(required("APP_PORT")),
databaseUrl: required("DATABASE_BASE_URL") + "/" + required("TEST_DB"),
queuePrefix: required("QUEUE_PREFIX"),
objectPrefix: required("OBJECT_PREFIX"),
tempDir: required("TMPDIR"),
};
Use the prefix at every boundary. Queue names should be built from runtime.queuePrefix rather than hard-coded. Storage helpers should prepend runtime.objectPrefix. Small wrappers are safer than expecting every agent-produced call site to remember a convention.
For an external development database, prefer an ephemeral database per worktree or a namespace per worktree. A namespace can be enough for read-heavy tests. A separate database is safer when tests migrate schemas, truncate tables, or start workers. Never point parallel agent worktrees at a shared staging database merely because it is convenient.
Run a preflight before processes start
Two tasks can still collide if the naming scheme is flawed, a port is already busy, or another process owns a scarce shared resource. Put a preflight check in the normal start command instead of relying on an agent to inspect the machine manually.
scripts/worktree-preflight.sh:
#!/usr/bin/env bash
# scripts/worktree-preflight.sh
set -euo pipefail
source scripts/worktree-env.sh
if lsof -nP -iTCP:"$APP_PORT" -sTCP:LISTEN > /dev/null 2>&1; then
echo "Port $APP_PORT is already listening; refusing $WORKTREE_ID" >&2
exit 1
fi
if ! [[ "$TEST_DB" =~ ^app_test_[a-z0-9_-]+$ ]]; then
echo "Unsafe test database name: $TEST_DB" >&2
exit 1
fi
echo "preflight ok: $WORKTREE_ID"
Call it from development, integration-test, and worker commands. For a scarce resource such as a hardware device or migration-capable database, an ID is not enough. Use a lease with an owner, expiry, and renewal time. Agents should wait or choose a different task when the lease is held; they should not kill an unknown process.
Isolate integration tests, not only application processes
The costliest collision is often invisible. Agent A creates a customer called Acme while Agent B's cleanup assumes it is the only test customer. Each test passes alone; together, one removes the other's fixture.
import { runtime } from "../src/config/runtime";
export async function createTestAccount(db: Database) {
return db.accounts.insert({
email: runtime.worktreeId + "-" + crypto.randomUUID() + "@example.test",
displayName: "test-" + runtime.worktreeId,
});
}
The UUID prevents collisions within one run. The worktree ID makes stray records attributable later. Use the same approach for webhook IDs, storage keys, cache keys, and event streams.
Migrations need a special rule. Multiple worktrees should not auto-migrate a common database. Either create the database from a known schema image, use a container per worktree, or put migration tests behind a single lease. A worktree is a unit of file isolation - not permission to alter shared infrastructure concurrently.
Make workers disposable
Background jobs outlive the terminal that started them. That can make an old agent look like a new test failure. Store a PID file in the worktree temp directory and include the environment ID in worker logs.
scripts/start-worker.sh:
#!/usr/bin/env bash
# scripts/start-worker.sh
set -euo pipefail
source scripts/worktree-env.sh
pidfile="$TMPDIR/worker.pid"
pnpm worker --queue-prefix "$QUEUE_PREFIX" > "$TMPDIR/worker.log" 2>&1 &
echo $! > "$pidfile"
echo "worker started for $WORKTREE_ID"
Cleanup must stop only the process recorded for that environment, then delete only exact namespaced resources:
scripts/worktree-cleanup.sh:
#!/usr/bin/env bash
# scripts/worktree-cleanup.sh
set -euo pipefail
source scripts/worktree-env.sh
pidfile="$TMPDIR/worker.pid"
if [ -f "$pidfile" ] && kill -0 "$(cat "$pidfile")" 2> /dev/null; then
kill "$(cat "$pidfile")"
fi
echo "delete database or schema: $TEST_DB"
echo "delete queue keys with prefix: $QUEUE_PREFIX"
echo "delete object prefix: $OBJECT_PREFIX"
rm -rf "$TMPDIR"
Keep deletion commands narrow. A cleanup script that accepts an arbitrary database name or broad storage prefix is a capability an AI agent can misuse after a misunderstood instruction. Validate the expected prefix before deleting.
Give agents a short environment contract
The task prompt should state the operating boundary, but the repository scripts remain the source of truth:
Worktree environment contract:
- Load
scripts/worktree-env.shbefore dev, tests, or workers. - Use only values derived by the script.
- Run preflight before integration tests.
- Put logs and generated fixtures under
TMPDIRorOBJECT_PREFIX. - Run cleanup when the task is complete.
- Report
WORKTREE_ID, commands run, and cleanup status in the handoff.
This is deliberately short. A handoff that says only "tests passed" is weak. A handoff with the environment ID and commands lets a reviewer reproduce the result.
Add isolation to the definition of done
Start two worktrees and prove they cannot see each other's state:
- Verify IDs, ports, database/schema, and queue prefixes differ.
- Start both servers and call each health endpoint.
- Insert a recognizable fixture through worktree A.
- Query from worktree B and assert that fixture is absent.
- Enqueue a job in A and verify only A's worker receives it.
- Stop A, run cleanup, and confirm B remains healthy.
This exposes the common hidden-default bug: a value was derived correctly but an SDK client still read a fallback configuration.
Know when a worktree needs a container
A worktree does not automatically need a container. For a small API with one local database, derived names and a preflight script are usually quicker to understand and easier to debug. Containers are useful when the stack has native dependencies, several services, or a test setup that needs to match CI closely.
If you use Compose, give the project a worktree-specific name. This isolates container names and networks as well as ports:
source scripts/worktree-env.sh
export COMPOSE_PROJECT_NAME="app_$WORKTREE_ID"
docker compose up --build -d
Do not put a random project name in an agent command. Deriving it from the same environment ID means Compose status, logs, and teardown point to the correct task after an interruption. Map container ports through the derived APP_PORT, and pass TEST_DB and QUEUE_PREFIX into the relevant services.
Containers are not a blanket safety guarantee. They can still mount the same host directory, use the same external database, or reach the same cloud bucket. Treat them as one layer in the environment contract.
The right question is not "does each agent have a container?" It is "can this task write, consume, bind, or delete a resource owned by another task?" If the answer is yes, add an explicit name, prefix, or lease.
Common shortcuts that fail
- Different branches. Branches do not separate databases, ports, caches, or queues.
- Transactional tests. Transactions help, but workers, external APIs, and migrations often run outside them.
- An unused port per agent. This is unrepeatable and isolates nothing else.
- Delete everything with test in the name. That is not a boundary. Cleanup should use an exact, validated environment ID.
- The merge queue will catch it. A merge queue catches integration problems late. Worktree environments make each agent's evidence honest first.
Start small, then automate
Begin with two concurrent worktrees and isolate four things: application port, test database or schema, queue prefix, and temporary directory. Add object-storage prefixes and leases when the stack needs them. Once the convention is stable, run preflight automatically in local scripts and CI integration jobs.
Parallel coding agents increase the need for operational discipline; they do not remove it. A worktree gives every diff a home. A worktree environment gives every running process, test fixture, and background job a home too.
FAQ
Are Git worktrees enough for parallel AI coding agents?
No. They isolate tracked files and branches, but ports, databases, caches, queues, containers, and credentials can still be shared. Add a deterministic environment ID and route each dependency through it.
How do I avoid port conflicts across AI coding agent worktrees?
Derive a stable port from a worktree identifier and check that it is free before starting. Do not ask each agent to select a port manually; reproducible allocation makes restarts and debugging easier.
Should each worktree use a separate test database?
Prefer a separate ephemeral database when tests migrate schemas, truncate tables, or run workers. A per-worktree schema can be adequate for simpler cases, provided every connection and cleanup task uses it.
Can parallel worktrees share Redis or a message broker?
They can share the service process, but not logical keys or queues. Prefix every key, stream, consumer group, and queue with the worktree environment ID, and make workers subscribe only to that prefix.
How should an AI agent clean up its worktree environment?
Use a repository-owned cleanup script that validates the environment ID, stops only recorded processes, and removes only exact database, queue, storage, and temporary-resource prefixes.
Does environment isolation replace a merge queue or code review?
No. Isolation proves an individual task ran against its own resources. You still need task boundaries, review, and fresh integration validation for changes that land together.
Comments
No comments yet. Start the discussion.