Claude Code Loves Worktrees. Your Infrastructure Doesn't.

Claude Code's instinct to parallelize with worktrees is powerful — until your project has Docker Compose, env secrets, and 5 services fighting for the same port.

If you use Claude Code daily, you’ve seen it: you ask the agent to implement a feature and, before you can blink, it’s already spinning up a worktree with three parallel subagents — each one confidently working in isolation, each one about to crash into the same wall.

The first time it happened to me, I was impressed. Three subagents, three worktrees, three features in parallel. It felt like the future. Then the logs started. DATABASE_URL is not defined. EADDRINUSE :4001. A migration that corrupted a shared database. All three agents failing for reasons that had nothing to do with the code they wrote.

That’s when I realized: Claude Code’s instinct to parallelize with worktrees is powerful, but it assumes your project is just code. Most projects aren’t.

The Promise

claude --worktree is one of the best features in Claude Code. Each agent gets an isolated copy of the repo. No merge conflicts. No “who broke main?” No stepping on each other’s code.

For pure code changes, it’s perfect. Create a worktree, implement the feature, open a PR, merge, done. The agent works in isolation and the main branch stays clean.

But then your project grows.

Where It Breaks Down

The moment your project depends on more than just code, worktrees start showing cracks.

1. Environment Variables Don’t Follow You

Your .env file lives in the main repo directory. When you create a worktree, it gets a fresh copy of the git-tracked files, but .env is gitignored. So your worktree has no database connection string, no API keys, no secrets.

main-repo/
├── .env              ← has all your secrets
├── src/
└── ...

.worktrees/feat-auth/
├── src/              ← code is here
└── (no .env)         ← secrets are NOT here

The agent starts the dev server and immediately gets DATABASE_URL is not defined. Now you’re debugging infrastructure instead of building features.

2. Port Conflicts

Your API runs on :4001. Your web app on :5173. Your collab server on :3001.

Now you have two worktrees trying to run the same services. Both want port 4001. One wins, the other crashes.

Main repo:     API :4001  |  Web :5173  |  Collab :3001
Worktree A:    API :4001  ← EADDRINUSE
Worktree B:    API :4001  ← EADDRINUSE

3. Shared Services (Docker Compose)

Your project runs PostgreSQL, Redis, and maybe Elasticsearch via Docker Compose. Do you:

  • Spin up a separate Docker Compose per worktree? That’s 3 extra containers per worktree. Your laptop is now a data center.
  • Share one Docker Compose instance? Works, but now all worktrees hit the same database. If worktree A runs a migration, worktree B breaks.

4. The Migration Problem

This is the nastiest one. Developer A is working on a feature that adds a subscription_tier column. Developer B is working on a feature that renames user_type to account_type.

Both worktrees share the same Postgres instance. Developer A runs their migration. Now Developer B’s code is running against a schema it doesn’t expect. Errors everywhere, but the code is fine. It’s the database that’s out of sync.

Solutions That Actually Work

After hitting all of these in a TypeScript monorepo with 5+ services, here’s what we settled on.

The simplest fix. All worktrees point to the same .env from the main repo:

# Run this after creating a worktree
MAIN_REPO=$(git worktree list --porcelain | head -1 | cut -d' ' -f2)
ln -s "$MAIN_REPO/.env" .env
ln -s "$MAIN_REPO/.env.local" .env.local 2>/dev/null

Every worktree reads the same secrets. No duplication, no drift. If you update a key in the main .env, all worktrees pick it up immediately.

When this fails: when worktrees need different values (like different ports). See Solution 2.

Solution 2: Port Offset Strategy

Each worktree gets its own port range. The trick is making it automatic, not manual.

# .env.worktree (auto-generated per worktree)
# Base ports: API=4001, Web=5173, Collab=3001

# Worktree "feat-auth" gets offset +100
API_PORT=4101
WEB_PORT=5273
COLLAB_PORT=3101

# Worktree "feat-payments" gets offset +200
API_PORT=4201
WEB_PORT=5373
COLLAB_PORT=3201

Your service config reads process.env.API_PORT || 4001. Main repo uses defaults. Worktrees use the offset.

You can automate this with a hash of the worktree name:

#!/bin/bash
# generate-ports.sh
WORKTREE_NAME=$(basename $(pwd))
HASH=$(echo -n "$WORKTREE_NAME" | md5sum | tr -d -c '0-9' | head -c 2)
OFFSET=$((HASH + 100))

echo "API_PORT=$((4000 + OFFSET))" > .env.worktree
echo "WEB_PORT=$((5100 + OFFSET))" >> .env.worktree
echo "COLLAB_PORT=$((3000 + OFFSET))" >> .env.worktree

Solution 3: Shared Infra, Isolated Code

The key insight: services are stateful, code is not. Your Postgres and Redis don’t change between features (usually). Your code does.

Run Docker Compose once from the main repo. All worktrees connect to the same services:

Main repo:     docker compose up -d postgres redis
                        ↑         ↑
Worktree A: ────────────┘         │
Worktree B: ──────────────────────┘

This works for 90% of cases. The exception is migrations.

Solution 4: The Migration Strategy

For features that don’t touch the schema (most features), share the database. Done.

For features that add migrations, create a temporary database:

# Before starting work on a migration-heavy feature
createdb keept_feat_auth
export DATABASE_URL=postgresql://localhost:5432/keept_feat_auth

# Seed it from the main DB
pg_dump keept_dev | psql keept_feat_auth

# When done, clean up
dropdb keept_feat_auth

This gives you an isolated schema without duplicating all your infrastructure.

The Setup Script

Putting it all together into a single script that runs when you create a worktree:

#!/bin/bash
# setup-worktree.sh — run after creating a new worktree

set -e

MAIN_REPO=$(git worktree list --porcelain | head -1 | cut -d' ' -f2)
WORKTREE_NAME=$(basename $(pwd))

echo "Setting up worktree: $WORKTREE_NAME"

# 1. Symlink env files
ln -sf "$MAIN_REPO/.env" .env
echo "Linked .env from main repo"

# 2. Generate port offsets
HASH=$(echo -n "$WORKTREE_NAME" | md5sum | tr -d -c '0-9' | head -c 2)
OFFSET=$((HASH + 100))
cat > .env.worktree << EOF
API_PORT=$((4000 + OFFSET))
WEB_PORT=$((5100 + OFFSET))
COLLAB_PORT=$((3000 + OFFSET))
EOF
echo "Generated ports with offset +$OFFSET"

# 3. Install dependencies
pnpm install
echo "Dependencies installed"

# 4. Verify shared services are running
if ! docker compose -f "$MAIN_REPO/docker-compose.yml" ps --quiet postgres > /dev/null 2>&1; then
  echo "Warning: Postgres is not running. Start it from main repo:"
  echo "  cd $MAIN_REPO && docker compose up -d"
fi

echo "Worktree $WORKTREE_NAME ready!"
echo "  API: http://localhost:$((4000 + OFFSET))"
echo "  Web: http://localhost:$((5100 + OFFSET))"

You can hook this into Claude Code as a PostToolUse hook that triggers after worktree creation, or call it manually.

When NOT to Use Worktrees

Worktrees are not always the answer. Skip them when:

  • The feature is trivial (1 file, no tests needed). Just commit on main.
  • You need a completely different infrastructure setup (different Docker services, different DB version). Use a full clone instead.
  • Your team is 1-2 people. The coordination overhead worktrees solve doesn’t exist yet.
  • The setup cost exceeds the isolation benefit. If it takes 10 minutes to set up a worktree for a 15-minute feature, something’s wrong.

The Decision Framework

Is the change trivial (1 file)?
  └─ Yes → Commit on main. No worktree needed.

Does it touch the database schema?
  └─ Yes → Worktree + temporary DB
  └─ No  → Worktree + shared DB

Does it need to run services locally?
  └─ Yes → Symlink .env + port offsets + shared Docker
  └─ No  → Just the worktree, code-only changes

TL;DR

Git worktrees give you code isolation, not environment isolation. For projects with Docker Compose, env secrets, and multiple services, you need a strategy on top:

  1. Symlink .env from the main repo
  2. Port offsets per worktree (auto-generated)
  3. Shared Docker services (one instance, all worktrees connect)
  4. Temporary databases for migration-heavy features
  5. A setup script that handles all of the above

The worktree itself is the easy part. The infrastructure around it is where teams lose time.

Plan your worktree strategy before your third developer joins. By then it’s too late to retrofit. 🏗️