The Collision Problem: Why Multiple AI Agents Can't Share a Working Directory

When you run git checkout feature-A in a directory where another process is reading files, the filesystem state changes underneath that reader. The other process doesn't see atomic transitions—it sees partial writes, missing files, and inconsistent dependency graphs. TypeScript compilers fail with "Cannot find module" errors. Dev servers crash because watched files disappeared mid-read. Lock files from package managers become corrupted when two agents run npm install simultaneously on different branches with different dependency trees.

The obvious solution—staggering agent execution so only one runs at a time—defeats the purpose of parallel development. Teams that try this pattern end up with AI agents waiting in queue, each one blocking the next until it finishes. The bottleneck shifts from human typing speed to serial execution, and the productivity gains evaporate.

Full repository clones work but waste disk space. A 2GB monorepo cloned five times for five agents consumes 10GB of redundant Git objects. Sparse checkouts reduce working directory size but don't eliminate the duplication. The failure mode here is subtle but expensive: teams hit storage limits on CI runners, local SSDs fill up, and clone times dominate agent startup latency.

What Are Git Worktrees and How They Solve Agent Isolation

A worktree is a working directory linked to a shared .git folder. Run git worktree add ../feature-A feature-A and Git creates a new directory at ../feature-A checked out to the feature-A branch. Both the original working directory and the new worktree share the same object database, refs, and history. Disk usage increases only by the size of checked-out files, not the entire repository.

Each worktree maintains its own HEAD, index, and working directory state. Changes in one worktree don't affect the others. An agent working in feature-A/ can modify src/api.ts, run tests, and commit—all while another agent in feature-B/ edits the same file on a different branch. The filesystem-level isolation prevents the collision patterns that break shared-directory workflows.

The implication here is that worktrees enable true parallel execution without coordination overhead. Agents don't need semaphores, file locks, or message queues to avoid conflicts. The operating system's directory structure provides the isolation boundary. This matters because coordination logic is the first thing AI agents get wrong when racing for shared resources.

Setting Up Your First Parallel Agent Workflow with Worktrees

Start with a base repository in ~/projects/app. Create worktrees for each agent task:

import { execSync } from 'child_process';
import { mkdirSync, existsSync } from 'fs';
import { join } from 'path';

interface WorktreeConfig {
  name: string;
  branch: string;
  agentTask: string;
}

function setupParallelAgents(baseRepo: string, configs: WorktreeConfig[]): string[] {
  const worktreePaths: string[] = [];
  for (const config of configs) {
    const worktreePath = join(baseRepo, '..', config.name);
    // Create worktree on new branch from main
    execSync(
      `git worktree add -b ${config.branch} ${worktreePath} origin/main`,
      { cwd: baseRepo, stdio: 'inherit' }
    );
    // Install dependencies in isolated environment
    execSync('npm install', { cwd: worktreePath, stdio: 'inherit' });
    worktreePaths.push(worktreePath);
    console.log(`✓ Worktree ready for: ${config.agentTask}`);
  }
  return worktreePaths;
}

// Usage
const agents = setupParallelAgents('~/projects/app', [
  { name: 'agent-api', branch: 'refactor/api-layer', agentTask: 'Refactor API routes' },
  { name: 'agent-tests', branch: 'test/integration-suite', agentTask: 'Add integration tests' },
  { name: 'agent-docs', branch: 'docs/api-reference', agentTask: 'Update API docs' }
]);

Each worktree gets its own node_modules, .env.local, and build artifacts. When Agent 1 runs npm install to update a dependency, it doesn't trigger file watchers in Agent 2's worktree. Build tools like tsc or vite write to separate output directories. The isolation is complete at the filesystem level.

The critical detail here is that you must run npm install per worktree. Symlinking node_modules breaks the isolation—multiple agents end up sharing the same dependency tree, and version conflicts resurface. Disk space is cheap. Race conditions are expensive.

Beyond File Conflicts: Database Branching and Port Isolation

File-level isolation solves half the problem. The other half is runtime resources: database connections, dev server ports, and background services. An agent running npm run dev in one worktree defaults to localhost:3000. If another agent starts a dev server in a different worktree, the port collision crashes both processes.

The solution is environment-specific configuration. Each worktree gets its own .env.local file with unique port assignments and database URLs:

import { writeFileSync } from 'fs';
import { join } from 'path';

interface AgentEnv {
  worktreePath: string;
  port: number;
  dbName: string;
}

function configureAgentEnvironment(config: AgentEnv): void { const envContent = PORT=${config.port} DATABASE_URL=postgresql://localhost:5432/${config.dbName} REDIS_URL=redis://localhost:6379/${config.port - 3000} NODE_ENV=development.trim();

const envPath = join(config.worktreePath, '.env.local'); writeFileSync(envPath, envContent);

// Create isolated database execSync(createdb ${config.dbName}, { stdio: 'inherit' }); // Run migrations in isolated DB execSync('npm run db:migrate', { cwd: config.worktreePath, stdio: 'inherit' }); }

// Configure three agents with isolated resources [ { worktreePath: '../agent-api', port: 3000, dbName: 'app_agent1' }, { worktreePath: '../agent-tests', port: 3001, dbName: 'app_agent2' }, { worktreePath: '../agent-docs', port: 3002, dbName: 'app_agent3' } ].forEach(configureAgentEnvironment);


Database branching tools like Neon's branching feature or PlanetScale's deploy requests extend this pattern to production-like databases. Each agent gets a copy-on-write database branch with production data, runs migrations independently, and merges schema changes back to main. The storage overhead is minimal—only changed rows consume space.

This approach scales to background workers, Redis instances, and message queues. The key is deterministic resource naming: `worker-${port}`, `queue-agent-${id}`, `cache-key-prefix-${branch}`. Collisions become impossible when resource identifiers embed the isolation boundary.

## Managing Multiple Worktrees: Lifecycle Patterns and Cleanup

Worktrees accumulate. After three weeks of parallel development, developers end up with 20 stale worktrees consuming disk space and cluttering `git worktree list` output. The cleanup pattern is straightforward but requires discipline:

```javascript
import { execSync } from 'child_process';
import { rmSync } from 'fs';

interface WorktreeInfo {
  path: string;
  branch: string;
  commit: string;
}

function listWorktrees(baseRepo: string): WorktreeInfo[] {
  const output = execSync('git worktree list --porcelain', {
    cwd: baseRepo,
    encoding: 'utf8'
  });
  const worktrees: WorktreeInfo[] = [];
  const lines = output.split('\n');
  let current: Partial = {};
  for (const line of lines) {
    if (line.startsWith('worktree ')) {
      current.path = line.replace('worktree ', '');
    } else if (line.startsWith('branch ')) {
      current.branch = line.replace('branch refs/heads/', '');
    } else if (line.startsWith('HEAD ')) {
      current.commit = line.replace('HEAD ', '');
      worktrees.push(current as WorktreeInfo);
      current = {};
    }
  }
  return worktrees;
}

function cleanupMergedWorktrees(baseRepo: string): void {
  const worktrees = listWorktrees(baseRepo);
  const merged = execSync('git branch --merged main', {
    cwd: baseRepo,
    encoding: 'utf8'
  }).split('\n').map(b => b.trim().replace('* ', ''));
  for (const wt of worktrees) {
    if (merged.includes(wt.branch)) {
      console.log(`Removing merged worktree: ${wt.branch}`);
      execSync(`git worktree remove ${wt.path}`, { cwd: baseRepo });
      execSync(`git branch -d ${wt.branch}`, { cwd: baseRepo });
    }
  }
}

Run this after every merge to main. The pattern prevents the "zombie worktree" problem where directories exist but the branches were deleted remotely. Git's worktree prune command cleans up metadata, but it doesn't remove the directories—teams need explicit filesystem cleanup.

For long-running worktrees, periodic rebasing keeps them current:

cd ../agent-api
git fetch origin
git rebase origin/main
npm install  # Update dependencies after rebase

The implication here is that worktrees aren't fire-and-forget. They require lifecycle management equivalent to long-lived feature branches. Teams that treat worktrees as ephemeral often find themselves with merge conflicts and outdated dependencies.

Worktrees vs Full Clones vs Docker Containers for Agent Isolation

Three patterns exist for isolating parallel AI agents. Each has specific tradeoffs that matter at scale.

Worktrees share the .git directory, so disk usage is minimal (only checked-out files). Full clones duplicate the entire repository, including all Git objects. Docker containers add runtime isolation but introduce image build overhead and volume mount complexity.

For most teams, worktrees offer the best balance: zero duplication of Git history, filesystem-level isolation, and no additional infrastructure. The pattern has existed since Git 2.5, but AI coding workflows finally make it essential.

Next Steps

  1. Set up a test project with two worktrees and run parallel agents. 2. Add environment-specific configuration for ports and databases. 3. Automate cleanup with a post-merge hook. 4. Evaluate if your CI/CD pipeline can leverage worktrees for parallel test execution.