AI-tooling

Git Worktrees Are Not Enough: How to Run Multiple Coding Agents Safely in Parallel

Blazity team
19 Sep 2026
19 min. read

Git worktrees are not enough to run multiple coding agents safely in parallel because a worktree isolates Git files, HEAD, the index, and branch state—not the runtime. Ports, processes, Docker resources, databases, queues, test accounts, credentials, caches, and preview deployments remain shared unless the team namespaces them explicitly. The practical fix is an environment contract that derives every mutable resource and its cleanup rule from one agent-specific identifier.

In this guide you will learn:

  • Why Git worktrees stop at the code boundary
  • What it takes to run multiple coding agents in parallel safely
  • How to isolate a Next.js app on one machine
  • How to isolate databases and test accounts
  • How to scope environment variables and preview deployments
  • How to automate cleanup and verify the environment is gone

Key insights

  • An environment is isolated only when its ports, data target, credentials and preview all derive from one identifier.
  • Every environment needs a readable identifier, derived resource names, and ports allocated from defined ranges.
  • Database targets, test accounts, secrets, queues, and preview deployments require isolation because shared state survives separate branches.
  • Package-manager stores and base-image layers can remain shared when their contents are addressable and their writes do not carry application state.
  • Cleanup is incomplete until ports, processes, credentials, database objects, caches, and preview deployments have been checked.

Why Git worktrees stop at the code boundary

Git worktrees solve a source-control problem. They do not solve process ownership, network binding, data ownership, or deployment ownership.

That distinction matters when two agents can start services and mutate the same application state.

What Git isolates: files, indexes, and branches

A worktree gives each checkout its own working directory, HEAD, and index. The Git worktree documentation also describes repository configuration as shared by default, with selected state becoming per-worktree.

That is enough for two agents to edit different branches without overwriting each other's tracked files. It does not create a separate operating-system account, shell, Docker daemon, database server, secret store, or cloud project.

The boundary is narrower than many agent harnesses imply. VS Code's agent harness documentation states that worktree isolation separates code changes but is not a security boundary. Blazity’s AI coding harness guide explains the higher-level harness choice; this article focuses on the runtime boundary beneath it.

It also notes that a worktree contains committed files, excluding uncommitted and untracked files.

Treat the worktree as one input to environment creation, alongside ports, data targets, and credentials.

What remains shared: ports, processes, containers, databases, credentials, caches, and deployments

A process started from one worktree still binds to the host's ports.

Compose still creates its resources under a project name, and DATABASE_URL still points wherever it was set.

The same applies to background workers. Two agents may start consumers against one queue, causing one agent to process or delete the other agent's jobs.

Shared test accounts create the same race through server-side state.

Credentials widen the blast radius. A copied .env.local can give an agent access to a database, object store, or deployment target that belongs elsewhere.

A shared Next.js cache can also reuse compiled output from a different branch when its cache key ignores the environment identifier.

Deployments introduce another boundary. Two previews can have different URLs while still writing to the same database, using the same accounts, and reading the same secret values.

The failure scenario: two Next.js agents with different worktrees changing the same runtime

Suppose myapp-agent-a and myapp-agent-b use separate worktrees. Both run pnpm dev, and both inherit PORT=3000 from a shared environment file.

The first Next.js process binds successfully. The second fails, retries another port, or starts without the port the agent expects.

Any callback, browser test, or worker configuration pointing at 3000 now targets the wrong branch.

Docker fails more quietly. Both worktrees run Compose without a distinct project name.

Their application containers join the same network, and their database volume may resolve to the same named resource. Docker's project-name documentation explains that project names isolate Compose environments, including multiple copies for feature branches.

The data failure is worse. Both agents use myapp_dev, the same test account, and the same queue.

Agent A changes a record that Agent B is asserting. A migration from one branch changes the schema underneath the other branch.

The preview can look healthy while remaining unsafe. Each deployment receives a generated URL under Vercel's preview model, but both previews can still point to one database unless their environment values differ.

The branches stayed separate while the database, accounts, and secrets stayed shared.

What does it take to run multiple coding agents in parallel safely?

The fix begins with an environment contract. The contract turns an agent identifier into every runtime name, credential scope, port, and cleanup handle required by the application. This contract is also the substrate for a governed AI workflow, where checks, approvals, and execution traces remain connected to the environment that produced them.

The environment contract for one worktree

An environment contract should be machine-readable and checked before startup. At minimum, it should define:

  1. The worktree path and branch reference.
  2. Numeric port ranges and the allocated port for each process.
  3. The Compose project name.
  4. The database target, schema, role, and migration owner.
  5. Account and credential namespaces.
  6. Cache names, queue names, preview identifiers, and expiration time.

The contract should be the input to scripts that render .env.local, Compose overrides, migration commands, test credentials, and deployment variables. Agents should not invent these values from directory names.

Make collisions impossible to miss. A startup command should fail if a port is already bound, a preview identifier belongs to another environment, or a database target does not match the manifest.

An environment identifier such as myapp-agent-a, with readable derived names and numeric ports allocated from a defined range

Use myapp-agent-a as the environment identifier. Derive names from it instead of assigning the same string to every resource.

A readable naming scheme makes cleanup auditable:

  • Compose project: myapp_agent_a
  • Database: myapp_agent_a_db
  • Database schema: agent_a
  • Account namespace: myapp-agent-a
  • Worker queue: myapp-agent-a-jobs
  • Preview identifier: myapp-agent-a-preview

Allocate ports from declared ranges. In this example, Next.js uses 4300–4399, workers use 4400–4499, PostgreSQL uses 5500–5599, and Redis uses 5600–5699.

Agent A receives 4311, 4411, 5511, and 5611. Agent B receives different numbers from the same ranges.

The identifier names the environment; the ports describe its local allocation.

The resources that must be unique versus package-manager and base-image caches that can remain shared

Application state must be unique. That includes listening ports, Compose projects, database targets, schemas, migration locks, queues, test accounts, server secrets, preview mappings, and branch-specific build artifacts.

Package-manager stores can remain shared when the package manager addresses content by integrity and does not store application state there. A shared pnpm store is practical when every worktree uses a controlled lockfile and the package manager version is consistent.

Corepack and pnpm provides the versioning context for that setup.

Base-image layers can also remain shared. Docker's build cache saves immutable layers; it should not hold a worktree's generated .next output, database files, credentials, or runtime uploads.

Next.js build caches need a branch-aware namespace when they are shared remotely or mounted outside the worktree. Otherwise, an agent can consume artifacts produced from another branch's source and environment.

How to isolate a Next.js app on one machine

A local application needs a separate process graph with names that let you start, inspect, and remove one environment without touching its neighbors. Teams standardizing that graph across products can treat it as part of Next.js platform development rather than a collection of per-developer shell conventions.

A complete manifest for the environment identifier, port allocations, COMPOSE_PROJECT_NAME, database targets, account namespace, cache names, and preview identifier

Store one manifest beside the automation scripts, outside any secrets an agent generates. This example follows myapp-agent-a through local startup, migrations, preview creation, and cleanup.

environment_id: myapp-agent-a
git:
  worktree_path: ../myapp-agent-a
  branch: agent/a
port_ranges:
  next: "4300-4399"
  worker: "4400-4499"
  postgres: "5500-5599"
  redis: "5600-5699"
ports:
  next: 4311
  worker: 4411
  postgres: 5511
  redis: 5611
compose:
  project_name: myapp_agent_a
  file: infra/compose.dev.yml
database:
  name: myapp_agent_a_db
  schema: agent_a
  role: myapp_agent_a_app
  migration_lock: myapp_agent_a_migrations
accounts:
  namespace: myapp-agent-a
  admin: admin+myapp-agent-a@example.test
  member: member+myapp-agent-a@example.test
  secret_scope: preview/myapp-agent-a
caches:
  next_build: myapp-agent-a-next
  worker_jobs: myapp-agent-a-jobs
  pnpm_store: shared-pnpm-store
  base_image: shared-nextjs-base
preview:
  identifier: myapp-agent-a-preview
  branch: agent/a
  expires_at: "2026-09-24T18:00:00Z"
files:
  env_local: .env.local
  pid_directory: .runtime/myapp-agent-a
  cleanup_record: .runtime/myapp-agent-a/cleanup.json

The indentation is part of the data model. Quoting the port ranges and timestamp prevents YAML parsers from coercing those values. The manifest separates human-readable names from numeric allocations, giving automation stable lookup values without forcing every environment to use port 3000.

Explicit ports for Next.js, background workers, and service dependencies, without hard-coded container_name values

Compose should receive explicit host ports from the manifest. The container ports can remain conventional, while the host ports vary per environment.

services:
  web:
    build:
      context: .
      cache_from:
        - type=local,src=.build-cache
    command: ["pnpm", "exec", "next", "dev", "--hostname", "0.0.0.0", "--port", "3000"]
    ports:
      - "${NEXT_PORT:?NEXT_PORT is required}:3000"
    env_file:
      - .env.local
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
  postgres:
    image: postgres:16
    ports:
      - "${POSTGRES_PORT:?POSTGRES_PORT is required}:5432"
    environment:
      POSTGRES_DB: "${DATABASE_NAME:?DATABASE_NAME is required}"
      POSTGRES_USER: "${DATABASE_ADMIN_USER:?DATABASE_ADMIN_USER is required}"
      POSTGRES_PASSWORD: "${DATABASE_ADMIN_PASSWORD:?DATABASE_ADMIN_PASSWORD is required}"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
      interval: 5s
      timeout: 5s
      retries: 10
  redis:
    image: redis:7-alpine
    ports:
      - "${REDIS_PORT:?REDIS_PORT is required}:6379"
volumes:
  postgres_data:

The top-level postgres_data declaration is intentionally unnamed. Compose therefore prefixes the real volume with the project name, producing a volume such as myapp_agent_a_postgres_data. Do not add a fixed name: value or a hard-coded container_name, because either can bypass the project namespace and create collisions.

Use service names such as postgres and redis inside the Compose network. Use the manifest’s allocated ports only from the host.

Unique Compose projects so Compose namespaces containers, networks, and volumes automatically

Render the environment file, read the project and Compose-file paths from the manifest with yq v4, and pass both values explicitly when starting the stack:

./scripts/render-env myapp-agent-a > .env.local
chmod 600 .env.local
compose_project="$(yq -r '.compose.project_name' .agent-environment.yml)"
compose_file="$(yq -r '.compose.file' .agent-environment.yml)"
docker compose --project-name "$compose_project" --project-directory . --env-file .env.local --file "$compose_file" up -d --build

Using --project-name avoids dependence on the current directory. Using --env-file ensures Compose resolves every ${...} value from the generated environment file. The Docker Compose project-name documentation lists -p, COMPOSE_PROJECT_NAME, a top-level name, and directory naming as possible sources; this workflow deliberately uses one explicit source.

Project naming gives Compose a namespace for containers, networks, and named volumes. It does not isolate databases hosted outside Compose, cloud queues, credentials, or preview deployments.

Those require their own manifest fields.

Compose's project namespace must come from the manifest, not the current directory. A script launched from the wrong worktree can otherwise create resources under an unexpected project.

How to isolate databases and test accounts

Two agents using myapp_dev, the same test account, and the same queue can change each other's state. A database-per-agent, branch-per-agent, or schema-per-agent design determines whether they can validate changes independently.

Database-per-agent, branch-per-agent, and schema-per-agent trade-offs

Strategy

Isolation boundary

Cost and speed

Failure mode

Database per agent

Separate database target and credentials

Strong separation, with more provisioning and storage

Cleanup can leave abandoned databases

Branch per agent

Database branch with independent writes

Fast when the provider supports branching

Application isolation still fails if DATABASE_URL points elsewhere

Schema per agent

Shared server and database, separate schema

Low infrastructure overhead

Extensions, global tables, locks, and misconfigured search paths can remain shared

A database per agent is the clearest local default. It makes the target visible in the manifest and lets cleanup drop one named database.

A branch per agent works well when the provider supports isolated writes. Neon's branching documentation, updated December 11, 2025, describes independent writes and dedicated compute resources for parallel tests.

Branch expiration can also remove an ephemeral branch after its expiration timestamp, according to Neon's branch expiration documentation.

Schema per agent is a compromise. It can reduce provisioning time, but it demands strict schema qualification and careful handling of global database objects.

Migration and seed commands that target the manifest’s database

Migration commands must consume the rendered manifest, not a developer's default shell environment.

./scripts/render-env myapp-agent-a > .env.local
chmod 600 .env.local
set -a
. ./.env.local
set +a
pnpm exec prisma migrate deploy
pnpm run db:seed -- --account-namespace "$ACCOUNT_NAMESPACE"

The renderer resolves DATABASE_NAME, DATABASE_SCHEMA, DATABASE_ROLE, and credentials for myapp_agent_a_db, and it emits POSIX-shell-compatible dotenv values because this sequence sources the file. It should refuse to run when the target is marked production or when the environment identifier is missing.

The same rule applies to reset commands. A script that accepts only the manifest identifier has less room for an agent to paste the wrong database target.

Namespaced test accounts, least-privilege credentials, and the shared-account failure mode

Tests that mutate server-side state need separate accounts. Playwright's authentication guidance recommends different accounts for parallel tests and describes one account per parallel worker.

Its stored authentication state can contain cookies and headers capable of impersonating that account.

Use account names derived from the environment namespace:

admin+myapp-agent-a@example.test
member+myapp-agent-a@example.test

Give the application role only the database permissions it needs. Give seed scripts a separate role, and keep migration credentials out of the browser test environment.

Playwright runs test files in parallel by default with worker processes, according to its parallelism documentation. A shared account turns independent tests into a race over sessions, carts, permissions, and records.

Shared accounts surface as flaky tests long before anyone traces them back to shared state.

How to scope environment variables and preview deployments

Environment variables connect source code to runtime resources. Copying a shared file hides that connection, which makes a worktree appear isolated while it still targets shared infrastructure.

Generate .env.local from the manifest instead of copying a shared file

Generate the file at startup and remove it during cleanup:

./scripts/render-env myapp-agent-a > .env.local
chmod 600 .env.local
./scripts/check-env \
--environment myapp-agent-a \
--file .env.local

The renderer should derive local values from the manifest:

NEXT_PORT=4311
WORKER_PORT=4411
POSTGRES_PORT=5511
REDIS_PORT=5611
COMPOSE_PROJECT_NAME=myapp_agent_a
DATABASE_NAME=myapp_agent_a_db
DATABASE_SCHEMA=agent_a
ACCOUNT_NAMESPACE=myapp-agent-a
QUEUE_NAME=myapp-agent-a-jobs
PREVIEW_IDENTIFIER=myapp-agent-a-preview

Keep secret values in a secret store or isolated local input. The generated file should contain only the values intended for that environment.

According to the Next.js environment variables guide, updated April 24, 2025, Next.js checks five environment-file locations and supports three NODE_ENV values. Rendering one file from one manifest keeps that loading behavior predictable.

Keep server-only values separate from NEXT_PUBLIC_ values frozen at build time

Server-only variables belong in runtime configuration. Database credentials, signing keys, provider tokens, and internal service addresses must not use the NEXT_PUBLIC_ prefix.

The same Next.js guide documents that NEXT_PUBLIC_ values are inlined during next build and remain frozen after that build. A preview built with Agent A's public API origin can keep that value even after deployment settings change.

Use separate names for browser-visible configuration:

NEXT_PUBLIC_APP_NAME='MyApp Agent A'
NEXT_PUBLIC_API_ORIGIN=/api
DATABASE_NAME=myapp_agent_a_db
SESSION_SIGNING_SECRET=from-secret-scope

Public values ship inside the build artifact, while server-only values stay runtime inputs.

Map each Vercel preview to the matching database, accounts, secrets, derived resource names, and expiration policy

A preview identifier should map to one complete environment record:

preview:
identifier: myapp-agent-a-preview
branch: agent/a
database: myapp_agent_a_db
schema: agent_a
account_namespace: myapp-agent-a
secret_scope: preview/myapp-agent-a
cache_namespace: myapp-agent-a-next
expires_at: 2026-09-24T18:00:00Z

As of 2026-09-17, Vercel's environments documentation, published August 14, 2026, describes Preview deployments for non-production branches, pull requests, and CLI deployments without --prod. Each deployment receives a generated URL, but that URL does not create database isolation.

Create the preview through a wrapper that renders environment values, deploys the branch, records the generated deployment identifier, and stores the expiration time. Vercel's environment-variable documentation, published August 20, 2026, states that variables are scoped by environment and changes apply only to new deployments.

As of 2026-09-17, Vercel's environment-variable storage changelog, published August 4, 2022, lists 64 KB of total storage per deployment. Its July 27, 2026 guide, updated September 10, 2026, lists up to 1,000 variables per environment per project.

Those limits do not replace namespace design.

Collision matrix: shared resource, likely failure, and isolation strategy

Compose project behavior follows Docker's project-name rules. Environment loading follows Next.js's documented variable behavior, and preview mappings follow Vercel's environment model.

Shared resource

Likely failure

Isolation strategy

Next.js host port

One process fails, or tests hit the wrong branch

Allocate a unique port from the Next.js range

Worker host port

Health checks reach another worker

Allocate a unique worker port

Compose project

Containers, networks, and volumes overlap

Set a unique COMPOSE_PROJECT_NAME

Database target

Migrations and writes affect another agent

Use a database, branch, or schema derived from the manifest

Migration lock

One agent blocks or alters another migration run

Use an environment-specific lock name

Test accounts

Sessions and records race across tests

Namespace accounts by environment and worker

Server secrets

An agent reads or signs data for another environment

Scope secrets by preview or environment identifier

Next.js build cache

Branch-specific artifacts are reused incorrectly

Namespace generated build artifacts

pnpm store

Dependency downloads duplicate or corrupt cache entries

Share a content-addressed store with pinned tooling

Base-image layers

Builds consume unnecessary disk and network

Share immutable Docker layers

Worker queue

One agent consumes another agent's jobs

Use an environment-specific queue name

Vercel preview

Separate URLs write to one database

Map each deployment to matching data and secrets

Cleanup record

Resources remain after worktree removal

Record every created resource before startup

How to automate cleanup and verify the environment is gone

Cleanup is part of environment creation. If startup records only the worktree path, cleanup cannot know which processes, volumes, accounts, or previews belong to it. The same resource inventory should feed AI agent observability so operators can tie processes, deployments, usage, and failures back to one agent run.

Stop Next.js and background-worker processes by recorded PID or resource name

Record process IDs when starting host processes. For Compose services, record the project name and service names.

./scripts/start-host-process \
--environment myapp-agent-a \
--name next \
--command "pnpm exec next dev --hostname 127.0.0.1 --port 4311"
./scripts/start-host-process \
--environment myapp-agent-a \
--name worker \
--command "pnpm run worker:start"

Stop by the recorded PID, then verify the process command still matches the environment before sending a signal. Do not kill every node process on the machine.

For Compose services, select the project explicitly:

docker compose \
  --project-name myapp_agent_a \
  --project-directory . \
  --env-file .env.local \
  --file infra/compose.dev.yml \
  down --volumes --remove-orphans

According to Docker’s docker compose down reference, the command removes Compose-created containers and networks. Named volumes require --volumes, while external volumes remain untouched. Reusing the same project name and Compose file makes the teardown target unambiguous.

Remove the matching Compose project, containers, networks, volumes, database target, accounts, env files, and previews

Use the manifest as the deletion authority:

./scripts/cleanup myapp-agent-a \
--stop-pids \
--compose-down \
--drop-database \
--delete-accounts \
--delete-env \
--delete-preview

The cleanup script should perform these operations in order:

  1. Stop recorded Next.js and worker processes.
  2. Remove the matching Compose project and its named volumes.
  3. Drop the manifest's database target or delete its database branch.
  4. Delete namespaced test accounts and revoke their credentials.
  5. Remove generated environment files and local PID records.
  6. Delete the recorded preview and its environment variables.
  7. Remove the environment's cache namespace.

Do not delete shared pnpm stores or base-image layers during agent cleanup. Those resources belong to the host and outlive any single worktree.

Verify ports, processes, credentials, database objects, caches, and deployment URLs before removing the worktree

Run verification before deleting the checkout. The worktree is the easiest place to recover the manifest and cleanup record.

./scripts/verify-gone myapp-agent-a \
--ports 4311,4411,5511,5611 \
--compose-project myapp_agent_a \
--database myapp_agent_a_db \
--schema agent_a \
--accounts myapp-agent-a \
--preview myapp-agent-a-preview

The verifier should confirm:

  • No recorded PID remains alive.
  • Allocated ports are free.
  • No container, network, or volume uses the Compose project.
  • The database target, schema, migration lock, and role are absent.
  • Namespaced accounts, tokens, and queue credentials are revoked.
  • Generated .env.local and cleanup records are removed.
  • Next.js and worker cache namespaces are gone.
  • The preview identifier and deployment URL no longer resolve to the environment.

Only then should the worktree be removed; a clean directory does not prove a clean runtime.

Where parallel agent work becomes safe

Git worktrees remain useful because they separate source changes and indexes. They become safe for concurrent agent work only when an environment contract extends that boundary across runtime resources, data, identity, and cleanup. The broader operating model belongs inside an AI governance maturity model and should be verified through architecture and code review before concurrent agents are allowed to touch production-connected resources.

If your Next.js application needs that contract designed around its architecture, talk to us. Next.js platform development turns the naming, deployment, and isolation model into working automation.

FAQ on run multiple coding agents in parallel

Can two worktrees safely use the same database?

They can share a database only when their writes, migrations, accounts, and schemas are deliberately coordinated. Independent agents should use separate databases, branches, or schemas. A shared target turns valid test failures into ambiguous state races.

Should each agent receive a separate Compose project?

Yes, each agent should receive a distinct Compose project name. Compose can then namespace containers, networks, and named volumes without hard-coded container names. External databases, queues, and secrets still need separate identifiers.

What belongs in a generated local environment file?

The file should contain values derived from the manifest for ports, service targets, namespaces, and environment-scoped secrets. Generate it during startup and remove it during cleanup. Keep browser-visible variables separate because public values are included in the build artifact.

How should preview deployments be cleaned up?

Record the preview identifier, deployment identifier, resource mapping, and expiration time at creation. Cleanup should delete the preview, revoke its environment variables, remove its data target, and verify that its deployment URL no longer points to live resources.

Sources

Subscribe to our newsletter

Get Next.js tips, case studies, and frontend insights delivered to your inbox.

By clicking Sign Up you request to receive newsletters from us in accordance with Website Terms. The Controller of your personal data is Blazity Sp. z o.o. with its registered office at Warsaw, Poland, who processes your personal data for marketing purposes. You have the right to data access, rectification, erasure, restriction and portability, object to processing and to lodge a complaint with a supervisory authority. For detailed information, please refer to the Privacy Policy.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.