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. And that means every time you spin one up, you're either manually editing .env files or you've got a setup script that looks something like this:
#!/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 << EOF
PORT=$PORT
DB_NAME=$DB_NAME
DATABASE_URL=postgres://localhost/$DB_NAME
COMPOSE_PROJECT_NAME=$COMPOSE_PROJECT
REDIS_URL=redis://localhost:$REDIS_PORT
EOF
createdb "$DB_NAME" 2>/dev/null || true
echo "Set up: PORT=$PORT DB=$DB_NAME"
That's 30 lines of bash, no error handling, no cleanup when you tear the worktree down, and it breaks the moment two worktrees race to write to ~/.worktree-ports at the same time.
I've seen this pattern everywhere. The Phoenix community wrote up their version. The folks at 10play wrote theirs. Everyone reinvents the same wheel.
What I built instead
I maintain workz — a Rust CLI for Git worktrees that handles zero-config dep syncing (symlinks node_modules, target, .venv automatically) and AI agent launching. Last week I shipped --isolated:
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!
That's it. It:
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
And 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'd all try to bind to port 3000. First one wins, the other two crash on startup.
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. Each agent gets a fully isolated environment without touching the others.
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. Linux support is "hopefully soon-ish, but not sure."
Every Linux developer who wants to run parallel AI agents has no Conductor. And even on Mac, Conductor's worktree setup is a bash script you write yourself — there's no environment engine built in.
workz is the answer for that gap:
Linux, Mac (Windows planned)
Open source — MIT, single Rust binary, cargo install workz
Zero-config — auto-detects Node/Rust/Python/Go/Java projects, symlinks deps automatically
--isolated — the environment engine Conductor doesn't have
It's not trying to be a GUI. It's terminal-native, which is what Linux developers actually want.
How the port registry works
The implementation is straightforward. A 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"
}
}
}
On workz start --isolated: scan allocations for the next unused port, write the entry, write .env.local.
On workz done: remove the entry. Atomic reads and writes, no race conditions between concurrent workz start calls.
The branch slug sanitizer handles edge cases: feature/add-auth → feature_add_auth, collapses repeated separators, lowercases everything.
Install
Homebrew
brew tap rohansx/tapbrew install workz
Cargo
cargo install workzAdd shell integration to your .zshrc / .bashrc:
eval "$(workz init zsh)"
This gives you a workz shell function that auto-cds into the new worktree after workz start.
Repo: https://github.com/rohansx/workz
If you're on Mac and already using Conductor — workz works as your setup script. Drop workz sync --isolated into your conductor.json and you get the environment engine without changing your workflow.

Top comments (0)