I got tired of writing the same 80-line setup script for every AI worktree

Published: (March 3, 2026 at 04:08 PM EST)
4 min read
Source: Dev.to

Source: Dev.to

Parallel AI Agents & Isolated Worktrees

Demo GIF

If you’ve been running parallel AI coding agents — Claude Code, Aider, Codex, anything — you’ve hit this wall.

  • Each agent needs its own worktree.
  • Each worktree needs its own environment.

That means every time you spin one up you either:

  • Manually edit .env files, or
  • Run a setup script like the one below.
#!/bin/bash
# conductor.json worktree script — set up isolated environment

BRANCH_NAME="${CONDUCTOR_BRANCH_NAME}"
SLUG="${BRANCH_NAME//\//_}"
SLUG="${SLUG//-/_}"

BASE_PORT=3000
USED_PORTS=$(cat ~/.worktree-ports 2>/dev/null || echo "")
PORT=$BASE_PORT
while echo "$USED_PORTS" | grep -q "^$PORT$"; do
  PORT=$((PORT + 1))
done
echo "$PORT" >> ~/.worktree-ports

DB_NAME="myapp_${SLUG}"
COMPOSE_PROJECT="myapp_${SLUG}"
REDIS_PORT=$((PORT + 1000))

cat > .env.local /dev/null || true
echo "Set up: PORT=$PORT DB=$DB_NAME"

That’s 30 lines of Bash, with no error handling, no cleanup when you tear the worktree down, and it breaks the moment two worktrees race to write ~/.worktree-ports.

I’ve seen this pattern everywhere – the Phoenix community, 10play, etc. – everyone reinvents the same wheel.


What I built instead

I maintain workz – a Rust CLI for Git worktrees that handles zero‑config dependency syncing (symlinks node_modules, target, .venv automatically) and AI‑agent launching.

Last week I shipped the --isolated flag:

workz start feature/auth --isolated

Output

creating worktree for branch 'feature/auth'
  worktree created at /home/you/myapp--feature-auth
  symlinked node_modules
  isolated environment:
    PORT=3001                 → .env.local
    DB_NAME=feature_auth
    COMPOSE_PROJECT_NAME=feature_auth
    REDIS_URL=redis://localhost:4001
ready!

What it does

  • Picks the next available port starting from 3000 (atomic, no race conditions).
  • Sanitizes the branch name into a safe slug (feature/auth → feature_auth).
  • Writes .env.local with PORT, DB_NAME, DATABASE_URL, COMPOSE_PROJECT_NAME, REDIS_URL.
  • Registers the allocation in ~/.config/workz/ports.json so no two worktrees ever collide.

When you’re done:

workz done feature/auth --cleanup-db
  • Releases the port.
  • Removes the worktree.
  • Optionally drops the database.

No orphaned ports. No stale registry entries.


Running 3 isolated agents in parallel

The reason I care about this: I run multiple Claude Code agents in parallel, each on a different task. Before --isolated they all tried to bind to port 3000. The first one won; the others crashed.

Now:

workz fleet start \
  --task "add OAuth2 login" \
  --task "write integration tests" \
  --task "refactor database layer" \
  --agent claude \
  --isolated

Three isolated worktrees, three unique ports, three separate databases, three .env.local files — all spun up in parallel.

workz status shows the full picture:

  main                /home/you/myapp                     [clean] 342K  2h ago
  feature/auth        /home/you/myapp--feature-auth       89M   5m ago  PORT:3001
  fix/tests           /home/you/myapp--fix-tests          91M   5m ago  PORT:3002
  refactor/db         /home/you/myapp--refactor-db        88M   5m ago  PORT:3003

The Linux angle

I built this partly because Conductor.build – the GUI for parallel Claude Code agents that’s been getting a lot of attention – is Mac‑only (Apple Silicon required).

Every Linux developer who wants to run parallel AI agents has no Conductor. Even on Mac, Conductor’s worktree setup is a Bash script you write yourself – there’s no environment engine built in.

workz fills that gap:

FeatureDetails
PlatformsLinux, macOS (Windows planned)
LicenseMIT – single Rust binary
Installationcargo install workz (or Homebrew)
Zero‑configAuto‑detects Node/Rust/Python/Go/Java projects, symlinks deps automatically
--isolatedThe environment engine Conductor lacks
Not a GUITerminal‑native, exactly what Linux developers want

How the port registry works

A simple JSON file at ~/.config/workz/ports.json:

{
  "base_port": 3000,
  "allocations": {
    "feature_auth": {
      "port": 3001,
      "branch": "feature/auth",
      "db_name": "feature_auth",
      "compose_project": "feature_auth",
      "allocated_at": "2026-03-03T20:00:00Z"
    }
  }
}
  • workz start --isolated – scans allocations for the next unused port, writes the entry, writes .env.local.
  • workz done – removes the entry.

All reads/writes are atomic, so concurrent workz start calls never clash.

The branch‑slug sanitizer handles edge cases (feature/add-auth → feature_add_auth), collapses repeated separators, and lower‑cases everything.


Install

Homebrew

brew tap rohansx/tap
brew install workz

Cargo

cargo install workz

Add shell integration to your ~/.zshrc / ~/.bashrc:

eval "$(workz init zsh)"   # or `zsh` → `bash` as appropriate

This provides a workz shell function that cds into the new worktree automatically after workz start.

Repository:

If you’re on macOS and already using Conductor — workz works as your setup script, giving you the same isolation without the GUI constraints.

Tip: Drop workz sync --isolated into your conductor.json and you get the environment engine without changing your workflow.

0 views
Back to Blog

Related posts

Read more »

Run Your Dev Server Without a .env File

The .env Problem Every project has one – a .env file sitting in the project root with database passwords, API keys, and secrets of varying sensitivity. You hav...