Every worktree gets its own ports, database, cache, and containers — automatically.
worktree-compose.com · Changelog · Agent skill · GitHub
The full wtc workflow: an isolated Docker stack per worktree, four AI agents building in parallel, compared live on their own ports — then promote the winner and clean up. Watch the video.
npm install -g worktree-composewtc list
┌───────┬──────────────┬────────┬────────────────────────┬────────────────────────────────────────────────────────────┐
│ Index │ Branch │ Status │ URL │ Ports │
├───────┼──────────────┼────────┼────────────────────────┼────────────────────────────────────────────────────────────┤
│ - │ main │ - │ - │ postgres:5434 backend:8000 frontend:5173 │
├───────┼──────────────┼────────┼────────────────────────┼────────────────────────────────────────────────────────────┤
│ 1 │ feature-auth │ up │ http://localhost:25174 │ POSTGRES_PORT=25435 BACKEND_PORT=28001 FRONTEND_PORT=25174 │
├───────┼──────────────┼────────┼────────────────────────┼────────────────────────────────────────────────────────────┤
│ 2 │ fix-billing │ exited │ http://localhost:25175 │ POSTGRES_PORT=25436 BACKEND_PORT=28002 FRONTEND_PORT=25175 │
└───────┴──────────────┴────────┴────────────────────────┴────────────────────────────────────────────────────────────┘
Type
wtc, notnpx wtc. An unrelated npm package is namedwtc, so without the global installnpx wtcdownloads and runs that package instead. For a one-off run without installing, usenpx worktree-compose <command>(for examplenpx worktree-compose list).
- Why wtc?
- How is this different from
docker compose -p? - Quick Start
- Usage
- Selecting worktrees
- Preparing your compose file
- Monorepos & custom env files
- How It Works — stable identity, ports, project names, file sync, env files
- Commands — what each one does to your data
- Configuration (Optional)
- Docker, Podman, OrbStack, Colima
- MCP Server
- Agent Skill
- Full Example
- Upgrading from 0.2
- Requirements
- Troubleshooting
- Contributing
- License
Spin up multiple developers or AI agents on the same repo — each in its own git worktree — and they immediately fight over a single Docker Compose setup: port conflicts, a shared database, a shared cache, colliding containers. You can't run two stacks side by side.
wtc reads your existing compose file and turns every worktree into a fully isolated, side-by-side environment — unique host ports, separate containers, networks, and volumes — with zero configuration.
- ⚡ Zero config — reads your existing
docker-compose.yml/compose.yaml, no new files required - 🔌 Automatic ports — unique, collision-free host ports per worktree
- 📦 Full isolation — separate containers, networks, and volumes for each stack
- 📌 Stable identity — a worktree keeps its index, ports, and project name; removing one never renumbers the others
- 🧪 True side-by-side — run
Nstacks at once and compare them in the browser - 🔄 One-command promote — pull a worktree's changes back into your branch
- 🐳 Docker or Podman — Docker Compose v2, Podman, OrbStack, Colima
- 🩺
wtc doctor— finds what would break isolation before it bites - 🤖 Built-in MCP server + agent skill — let AI agents start, stop, and manage their own stacks
Without wtc | With wtc |
|---|---|
git worktree add ../app-feat feat
# hand-pick non-colliding host ports
# edit ../app-feat/.env per service
# copy compose file + Dockerfiles over
docker compose -p app-feat up -d --build
# ...remember to tear it all down later |
git worktree add ../app-feat feat
wtc start
# ...and when you're done:
wtc clean |
docker compose -p only namespaces the project. You'd still hand-pick free host ports, hand-edit each worktree's .env, copy infra files in, remember which project belongs to which worktree, and tear everything down yourself. wtc builds on that same project-name isolation and automates the rest: collision-free port allocation, a stable index and project name per worktree, per-worktree env file injection (including COMPOSE_PROJECT_NAME, so plain docker compose in the worktree hits the right stack), infra-file sync, one-command promote, safe cleanup, and a built-in MCP server so AI agents can drive it all themselves.
# 1. Install once, globally
npm install -g worktree-compose
# 2. Create a worktree for some parallel work
git worktree add -b feature ../myapp-feature
# 3. Boot an isolated stack for it ("." = the worktree you're in)
cd ../myapp-feature
wtc start .
# 4. See every stack, its status, URL, and ports
wtc list
# 5. Something off? Ask the doctor
wtc doctorwtc works from the main checkout or from any worktree: it always reads the compose file, config, and env files from the main checkout.
wtc start # start every worktree's stack
wtc start . # just the worktree you're in
wtc start 1 fix-billing # by index, branch, directory name, or path
wtc start --no-build # skip the image rebuild
wtc start --env .env.development --workspace server # monorepo env files
wtc list # status, URL, and ports (alias: wtc ls; --json for scripts)
wtc stop 1 # stop the containers; data is kept and start resumes them
wtc restart . # re-sync files and env, recreate what changed
wtc promote fix-billing # copy a worktree's changes into the current checkout
wtc prune # tear down stacks of worktrees you already deleted
wtc clean --dry-run # preview the full cleanup
wtc clean # tear down every stack (deletes volumes), remove worktrees that hold no work
wtc doctor # check git, engine, ports, isolation, orphans
wtc skill --install # install the agent skill
wtc mcp # MCP server for AI agents (stdio)start, stop, and restart take any number of worktrees (none means all); promote takes exactly one. Each one can be given as:
| Selector | Example | Matches |
|---|---|---|
| index | 2 |
the worktree's stable index (the Index column of wtc list) |
| branch | fix-billing |
the branch checked out in the worktree |
| directory name | myapp-fix-billing |
the basename of the worktree's directory |
| path | ../myapp-fix-billing |
a path, resolved against the current directory (symlinks such as macOS /tmp are followed) |
. |
. |
the worktree that contains the current directory |
- A name that matches several worktrees (say, two directories with the same basename in different places) is an error listing them; use the index instead.
- A number is an index, unless it also names a different worktree (a branch or directory called
2). Thenwtcrefuses and asks for the branch, the directory name, or a path — the message suggests one that works from where you are (e.g.../2). - Through MCP, a JSON number is always an index; a string follows the rules above.
wtc looks for compose.yaml, compose.yml, docker-compose.yaml, or docker-compose.yml (in that order) at the root of the main checkout.
For wtc to isolate a service's port, the host port must use the ${VAR:-default} pattern, with an upper-case variable name:
# wtc CAN isolate this
ports:
- "${BACKEND_PORT:-8000}:8000"
# wtc CANNOT isolate this (hardcoded)
ports:
- "8080:8080"If wtc finds hardcoded ports, it warns you and suggests the fix:
⚠ Service "nginx" uses a raw port mapping (8080:80). To enable port isolation, change it to: "${NGINX_PORT:-8080}:80"
# Standard
- "${BACKEND_PORT:-8000}:8000"
# Same var for host and container
- "${FRONTEND_PORT:-5173}:${FRONTEND_PORT:-5173}"
# IP-bound
- "127.0.0.1:${API_PORT:-3000}:3000"
# With protocol
- "${BACKEND_PORT:-8000}:8000/tcp"
# Multiple ports per service
- "${BACKEND_PORT:-8000}:8000"
- "${DEBUG_PORT:-9229}:9229"
# Long-form syntax
- target: 8000
published: "${BACKEND_PORT:-8000}"
protocol: tcpwtc start and wtc doctor warn about compose settings that make worktree stacks collide or share data (wtc list only prints how many there are):
container_name: db— every stack would use the same container name, so the second one fails to start. Remove it, or make it per-stack:container_name: ${COMPOSE_PROJECT_NAME:-myapp}-db.- Top-level volumes or networks with a fixed
name:orexternal: true— every stack shares them. Aname:that contains${COMPOSE_PROJECT_NAME}is per-stack and fine. - Hardcoded host ports — see above.
- Cross-worktree port collisions — see
portStride.
If your compose file pulls in others via the top-level include: directive, wtc follows them recursively — services defined in included files get the same port isolation as top-level ones, and the included files are copied into each worktree alongside the main one.
include:
- ./db/compose.yml
- path: ./services/api/compose.ymlBuild contexts in an included file resolve relative to that file's own directory, just as Docker Compose does. (Included files that live outside the repo still have their ports isolated, but aren't copied into the worktree.)
In a monorepo, local env files usually live next to each app (apps/server/.env.development) instead of at the repo root, and the filename isn't always .env. Use envFile plus workspace selectors to keep every worktree's env files consistent:
# Copy + inject apps/server/.env.development and apps/homepage/.env.development
# into the worktree, in addition to the root env file
wtc start --env .env.development --workspace server --workspace homepageOr make it the project default in .wtcrc.json:
{
"$schema": "https://unpkg.com/worktree-compose/wtc.schema.json",
"envFile": ".env.development",
"workspaces": ["server", "homepage"],
"workspaceEnvOverrides": {
"server": {
"DATABASE_URL": "postgresql://postgres:postgres@localhost:${POSTGRES_PORT}/postgres"
}
}
}How workspace selection works:
- Workspaces come from
package.json#workspaces(npm/bun/yarn) orpnpm-workspace.yaml#packages. Only directories with apackage.jsoncount. In a repo that declares no workspaces,--workspace serverlooks for apackage.jsoninserver,apps/server,packages/server, orlibs/server. --workspacematches by package name, directory name, or repo-relative path, and may be repeated. On the command line it replaces the configuredworkspacesfor that run;--envlikewise replacesenvFile.- The repo root is always processed — docker compose interpolates
${POSTGRES_PORT:-5434}from the root env file, so skipping it would break port isolation. Only the root env file getsCOMPOSE_PROJECT_NAME. - Every target gets the allocated port variables. A selected workspace with no env file (and no
.env.example) is skipped with a warning. workspaceEnvOverrideskeys match a workspace by package name, then by directory name (only when no other workspace shares it), then by repo-relative path.restartchecks--envand--workspacebefore stopping anything, so a typo never leaves stacks stopped.
Why
--env, not--env-file? Node.js intercepts--env-filein its own argv parsing — even after the script path — so the flag never reacheswtc.--envis the safe spelling; the config key staysenvFile. It must be a bare filename such as.env.development.
The first time wtc acts on a worktree (start, stop, …), it records the worktree's index (which decides its ports) and its compose project name in .git/wtc/state.json: inside the git directory every worktree shares, never committed. From then on they don't change. wtc list, wtc doctor, and dry runs only read: they show the identity a new worktree will get without recording it.
- Removing a worktree never renumbers the others. (0.2 derived the index from the position in
git worktree list, so removing worktree 1 silently moved worktree 2 onto new ports and a new project, orphaning its running stack — #9.) - A new worktree gets the lowest index no record holds.
- Renaming the branch keeps the project name, and
git worktree movekeeps the index, ports, and project:wtckeeps an id in the worktree's git admin directory (.git/worktrees/<name>/wtc-id), which a move keeps andgit worktree removedeletes. Restart a moved worktree so bind mounts follow the new path. - A worktree you delete becomes an orphan. After
git worktree removeorrm -rf,wtc listshows it as(removed). Its index stays reserved, because its stack may still be running on those ports, untilwtc prune(orwtc clean) tears the stack down and forgets it. - A worktree git still lists is never an orphan, even when
wtccan't start it: a locked worktree whose directory is missing (an unmounted drive) shows as(locked, missing on disk)and keeps its index, record, and stack until it is back;pruneandcleanleave it alone.
git worktree remove ../myapp-spike-cache
git worktree add -b docs ../myapp-docs
wtc list
┌───────┬───────────────────────┬────────┬────────────────────────┬────────────────────────────────────────────────────────────┐
│ Index │ Branch │ Status │ URL │ Ports │
├───────┼───────────────────────┼────────┼────────────────────────┼────────────────────────────────────────────────────────────┤
│ - │ main │ - │ - │ postgres:5434 backend:8000 frontend:5173 │
├───────┼───────────────────────┼────────┼────────────────────────┼────────────────────────────────────────────────────────────┤
│ 1 │ feature-auth │ up │ http://localhost:25174 │ POSTGRES_PORT=25435 BACKEND_PORT=28001 FRONTEND_PORT=25174 │
├───────┼───────────────────────┼────────┼────────────────────────┼────────────────────────────────────────────────────────────┤
│ 2 │ fix-billing │ exited │ http://localhost:25175 │ POSTGRES_PORT=25436 BACKEND_PORT=28002 FRONTEND_PORT=25175 │
├───────┼───────────────────────┼────────┼────────────────────────┼────────────────────────────────────────────────────────────┤
│ 3 │ spike-cache (removed) │ up │ - │ POSTGRES_PORT=25437 BACKEND_PORT=28003 FRONTEND_PORT=25176 │
├───────┼───────────────────────┼────────┼────────────────────────┼────────────────────────────────────────────────────────────┤
│ 4 │ docs │ down │ http://localhost:25177 │ POSTGRES_PORT=25438 BACKEND_PORT=28004 FRONTEND_PORT=25177 │
└───────┴───────────────────────┴────────┴────────────────────────┴────────────────────────────────────────────────────────────┘
(removed) = the worktree is gone but its stack/record remains. Run 'wtc prune' to tear it down.
Coming from 0.2? On the first run, wtc gives existing worktrees the same positional indices 0.2 used, and a worktree whose 0.2 stack is still running adopts that stack's index and project (matched by the directory compose ran in). Running stacks are picked up, not duplicated. If state.json is ever lost or unreadable, it is rebuilt the same way (an unreadable file is first moved aside as state.json.bak-<time>).
Each worktree gets unique ports from its index: 20000 + default_port + (worktree_index × portStride)
portStride defaults to 1, so by default the table below holds:
| Service | Main (default) | Worktree 1 | Worktree 2 | Worktree 3 |
|---|---|---|---|---|
| postgres | 5434 | 25435 | 25436 | 25437 |
| backend | 8000 | 28001 | 28002 | 28003 |
| frontend | 5173 | 25174 | 25175 | 25176 |
The table is illustrative — every value follows the formula above. (If an allocation would exceed 65535, wtc falls back to default_port + 100 × index.)
With the default stride of 1, two worktrees collide on a port whenever a pair of services has default host ports whose difference equals the worktrees' index difference. Projects with clustered default ports (e.g. mocks on 8081–8086) therefore collide even between adjacent worktrees, and the second docker compose up fails to bind.
When wtc detects such a collision across the indices in use (live worktrees and orphans) it warns, naming the colliding ports and the recommended stride. Set portStride in your config (.wtcrc.json or package.json#wtc) to spread each worktree's ports far enough apart:
{ "portStride": 100 }A stride of 100 gives every worktree a 100-port band, which is collision-free unless some pair of default ports differs by an exact multiple of the stride. The recommended value in the warning is the smallest stride that is collision-free over a generous worktree horizon. The canonical docker compose up in the main checkout is unaffected — only the worktrees' ports change.
Each worktree's stack runs as the compose project {repo}-wt-{index}-{branch}, lowercased with anything other than a-z, 0-9, and - turned into - — for example myapp-wt-1-feature-auth. Compose names containers, networks, and volumes after the project, so nothing is shared. {repo} is the main checkout's directory name (for a bare repository, the repository's own name), so it is the same from every worktree.
wtc passes -p <project> on every compose call, exports COMPOSE_PROJECT_NAME and the allocated ports into compose's environment, and writes COMPOSE_PROJECT_NAME=<project> into the worktree's root env file. With the default .env, a plain docker compose ps / logs / exec (or a Makefile) run inside the worktree therefore reaches the same stack on the same ports.
Compose only reads a file named .env on its own. With a custom envFile such as .env.development, pass it explicitly (docker compose --env-file .env.development ps) or pass -p <project> (the project field of wtc list --json).
${COMPOSE_PROJECT_NAME} is also available in envOverrides and workspaceEnvOverrides values, e.g. "QUEUE_PREFIX": "${COMPOSE_PROJECT_NAME}-jobs".
Before every start, wtc copies infrastructure files from the main checkout into the worktree, so every worktree runs the latest Docker setup:
- the compose file, plus every file it pulls in via
include:(inside the repo); - every Dockerfile a service's
buildpoints to (remote build contexts anddockerfile_inlinehave none); - the extra paths listed in
sync. Entries must stay inside the repository; absolute or../entries are skipped with a warning.
Sync copies over what is at those paths and removes nothing else: files that only the worktree has stay. Symlinks are recreated as symlinks, never followed. wtc never writes through a symlink it finds at a destination (it replaces the link) and skips any destination whose directory resolves outside the worktree. Where main has a symlink but the worktree has a real directory, the directory is left alone with a warning.
On every start and restart, for the root and each selected workspace:
- Copy fresh from main:
<envFile>(default.env), falling back to.env.example. The root gets an empty file if neither exists; a workspace with neither is skipped. - Inject:
COMPOSE_PROJECT_NAME(root only), every allocated port variable, thenenvOverrides(root) orworkspaceEnvOverrides(that workspace). A key that already exists is rewritten in place — comments, ordering, and all other lines are preserved. Keys that don't exist are appended in a marked block, which is replaced on every run and never accumulates.
With "envOverrides": { "DATABASE_URL": "postgresql://app:app@localhost:${POSTGRES_PORT}/app" } and a main .env of
POSTGRES_PORT=5434
DATABASE_URL=postgresql://app:app@localhost:5434/appworktree 1 gets:
POSTGRES_PORT=25435
DATABASE_URL=postgresql://app:app@localhost:25435/app
# --- wtc port overrides ---
COMPOSE_PROJECT_NAME=myapp-wt-1-feature-auth
BACKEND_PORT=28001
FRONTEND_PORT=25174
# --- end wtc ---Two consequences of copying fresh every time:
- Edits made only in a worktree's env file are lost on the next start. Put lasting values in main's env file, or in
envOverrides/workspaceEnvOverrides. - Only the keys above are written. A value with a port inside it, like
DATABASE_URLabove, keeps main's port unless you add an override for it.
If a worktree's env file is a symlink (for example to main's .env), wtc replaces the link with a regular copy and never writes through it.
The allocated ports are also exported into the environment of the compose process wtc launches. Compose resolves ${VAR:-default} from the process environment, --env-file, and the project .env — never from a service's env_file:, and never from a custom filename like .env.development. Exporting the ports keeps published host ports correct when the project uses a custom envFile.
Besides a plain clone, wtc handles bare repositories with worktrees (the worktree on the repository's default branch acts as the main checkout and is never started, synced into, or removed), submodules, and --separate-git-dir checkouts. Locked worktrees work normally; a locked worktree whose directory is missing (an unmounted drive) is skipped with a warning, and its stack and index are kept.
Two layouts don't tell from every worktree which checkout is the main one: a bare repository whose default-branch worktree is mid-rebase, mid-bisect, or on another branch for now, and a --separate-git-dir repository seen from a linked worktree. wtc remembers the main checkout (in .git/wtc/state.json) once it has run there or found it, and uses that. When it can't tell at all, it warns, uses the current checkout's compose and config files for list, doctor, stop, promote, and prune, records nothing, and refuses start, restart, and clean — they would sync files into, start, or remove checkouts based on the wrong one. The error says how to fix it: check the default branch out in your main worktree, or run wtc start from the main checkout.
wtc start [worktrees...] [--env <name>] [--workspace <name>]... [--no-build]
For each selected worktree (all when none are given): sync infrastructure files, copy and inject env files, then run compose -p <project> up -d --build. Stops at the first failure; on success it prints wtc list. A worktree whose directory is missing is refused (see wtc prune). The main checkout's own stack is not managed by wtc: run docker compose there as usual.
| Flag | |
|---|---|
--env <name> |
Env filename to copy and inject for this run (default: envFile, else .env) |
--workspace <name> |
Workspace to also inject env into; repeatable (see Monorepos) |
--no-build |
Run compose up -d without --build |
wtc stop [worktrees...]
Runs compose stop: containers are stopped, not removed, and all volumes are kept — including the anonymous ones images like postgres and redis create — so wtc start resumes the same containers with their data. wtc list shows a stopped stack as exited. One failure doesn't stop the rest; the exit code is 1 if any stack failed to stop.
wtc restart [worktrees...] [--env <name>] [--workspace <name>]... [--no-build]
compose stop, then everything start does, with up -d --build --remove-orphans: unchanged services start again, changed ones are recreated (keeping their volumes), and containers of services you removed from the compose file go away. Use it after migrations, Dockerfile changes, or config updates. Env options and selectors are validated before anything is stopped.
wtc list [--json] # alias: wtc ls
Every worktree with its index, branch, status, URL, and ports, plus a row for main's default ports. Status is up, partial (with running/total, e.g. partial 2/3), exited (containers stopped), down (no containers), or ? (no reachable engine). One-off docker compose run containers are not counted. Removed worktrees whose record remains are shown as (removed); worktrees git still lists but wtc can't start as (locked, missing on disk) or (now the main checkout). Read-only: it never writes the registry.
--json prints an array of { index, branch, path, project, status, running, total, url, ports, orphan, unavailable } to stdout (unavailable is "missing", "main", or null); warnings go to stderr. The URL is the first service whose name contains frontend, web, app, or ui, else the last isolated port.
wtc promote <worktree> [--force]
Copies a worktree's changes into the checkout you run it from (usually main) as uncommitted changes: everything it changed since it branched — committed, staged, unstaged, and untracked — with deleted files deleted and renames applied as delete + add. Compose files and local env files (.env, .env.development, … — committed templates like .env.example still promote) are skipped, because wtc rewrites them per worktree. Symlinks are copied as symlinks; directories such as submodules are skipped with a warning. Works without Docker.
It refuses, before writing anything:
- conflicts — a file it would touch has uncommitted or untracked changes in the target (unless the target already has identical content). Commit or stash first.
- reverts — a file it would touch was also changed (committed) on the target branch since the worktree branched. Promote copies whole files, so this would silently undo those commits. Merge the target branch into the worktree first (the error prints the command), or pass
--forceto overwrite them anyway. - unsafe paths — a path that escapes the repository or passes through a symlinked directory on either side.
- promoting a worktree into itself.
wtc clean [--dry-run] [--force] [--keep-worktrees] [--sweep]
Ends the session for every worktree:
-
Tears down every stack
wtcknows — all live worktrees and all orphans — withcompose down -v --remove-orphans. This deletes their volumes, including the stacks of worktrees that step 2 keeps and with--keep-worktrees. To stop everything but keep the data, usewtc stop. -
Removes worktrees that hold no work of their own (skipped entirely with
--keep-worktrees). Never removed: the main checkout and the worktree you runcleanfrom. Kept unless you pass--force:- uncommitted changes, tracked or untracked — other than the files
wtcitself wrote there (the env files it injects, and compose/Dockerfile/synccopies that still match main); - a detached HEAD with commits no branch, tag, or remote contains;
- submodules with commits not pushed to any remote;
- locked worktrees.
Each kept worktree is listed with the reason.
- uncommitted changes, tracked or untracked — other than the files
-
Sweeps leftover containers, networks, and volumes, one at a time — only of stacks this checkout owns: projects in its registry, or
<repo>-wt-<n>-…projects whose containers ran from the main checkout, one of its worktrees, or a recorded worktree path. Stacks that are merely named like this repo's (another clone with the same directory name) are listed and never removed. Ownership is judged from containers: volumes and networks of a project with no container left carry no working directory, so they are listed, never removed — see leftovers. -
Runs
git worktree prune, and forgets the records of removed worktrees and torn-down orphans. Worktrees that stay keep their index.
| Flag | |
|---|---|
--dry-run |
Print exactly what would be stopped, removed, and swept; change nothing (not even wtc's registry) |
--force |
Also remove worktrees with uncommitted changes, unreachable commits, unpushed submodule commits, and locked ones |
--keep-worktrees |
Tear down the stacks (deleting their volumes), keep every worktree |
--sweep |
Also remove <repo>-wt-<n>-… stacks whose working directory no longer exists — e.g. left behind by wtc 0.2 — judged from their containers: volumes or networks with no container left are listed, never removed. Never a stack whose directory exists |
wtc prune [--dry-run]
Tears down (compose down -v --remove-orphans, deleting volumes) and forgets orphans only: stacks of worktrees you deleted with git worktree remove or rm -rf. Live worktrees are not touched — nor is any worktree git still lists, such as a locked one on an unmounted drive; each orphan is re-checked right before teardown. Also runs git worktree prune when git still lists worktrees whose directory is gone.
wtc doctor [--json]
A ✔ / ⚠ / ✖ checklist with hints: git version; repository and main-checkout detection; compose file found and parsed (services, included files); config parsed (.wtcrc.json or package.json#wtc); compose engine and version (when it doesn't work, whether WTC_COMPOSE_COMMAND, composeCommand, or auto-detection chose it); daemon reachable; isolatable vs. hardcoded ports; isolation warnings; cross-worktree port collisions; missing worktrees; orphaned stacks; each worktree's stack state; and allocated ports already taken by something else for worktrees whose stack isn't running. Read-only. Exits 1 if any check fails.
wtc skill [--install] [--dir <path>]
Prints the bundled agent skill. --install writes it to ~/.claude/skills/worktree-compose/SKILL.md; --dir <path> installs it to <path>/worktree-compose/SKILL.md instead.
Starts the MCP server on stdio.
| Command | Containers | Volumes (your data) | Worktrees |
|---|---|---|---|
wtc stop |
stopped, kept | kept | kept |
wtc restart |
stopped, then started; changed services recreated | kept | kept |
wtc prune |
removed (deleted worktrees' stacks only) | deleted (those stacks only) | — |
wtc clean |
removed (every stack) | deleted (every stack) | removed unless they hold work; never main or the current one |
wtc clean --keep-worktrees |
removed (every stack) | deleted (every stack) | kept |
Exit codes: any error exits 1. stop, clean, and prune also exit 1 when a stack, removal, or sweep failed (worktrees kept on purpose don't count); doctor exits 1 when a check fails.
wtc works zero-config. For project-specific needs, create .wtcrc.json at the root of the main checkout:
{
"$schema": "https://unpkg.com/worktree-compose/wtc.schema.json",
"sync": [".generated/prisma-client", "local-certs/"],
"envFile": ".env.development",
"workspaces": ["server", "homepage"],
"envOverrides": {
"VITE_API_URL": "http://localhost:${BACKEND_PORT}"
},
"workspaceEnvOverrides": {
"server": {
"DATABASE_URL": "postgresql://postgres:postgres@localhost:${POSTGRES_PORT}/postgres"
}
},
"portStride": 100,
"composeCommand": "podman compose"
}Or use a "wtc" key in package.json. (.wtcrc.json takes precedence.) The $schema line gives editors completion and validation for every key; wtc ignores it.
Extra files/directories to copy from main into each worktree on start. Use for gitignored or generated files that Docker needs but aren't committed — like generated clients, local certificates, or build artifacts. Paths are repo-relative; entries that are absolute or leave the repository are skipped with a warning.
Env filename to copy from main and inject overrides into, for the root and every selected workspace. Defaults to .env. Must be a bare filename. Per-run override: --env.
Monorepo workspaces that also get their env file copied and injected, in addition to the root. Accepts package names, directory names, or repo-relative paths. Omitted → root only. Per-run override: --workspace. See Monorepos & custom env files.
Additional env vars injected into the root env file. Values interpolate ${VAR} with the allocated port variables and ${COMPOSE_PROJECT_NAME}. Existing keys are rewritten in place; new keys are appended in the override block.
Per-workspace overrides, keyed by workspace package name (a unique directory name or the repo-relative path also work). Applied only to that workspace's env file, with the same interpolation and in-place rewrite behavior as envOverrides.
Port spacing between consecutive worktree indices (default 1, must be a positive integer). Allocated port = 20000 + default_port + worktree_index × portStride. Raise it (e.g. 100) when your project exposes clustered default ports that would otherwise collide across worktrees — see Port Allocation. wtc warns and recommends a value when it detects a collision.
Which compose tool to use instead of auto-detection: exactly one of "docker compose", "podman compose", "docker-compose", or "podman-compose" (a string, or an array such as ["podman", "compose"]). Repository config is limited to these four so a cloned repository can't make wtc run an arbitrary program; anything custom goes in the WTC_COMPOSE_COMMAND environment variable, which takes precedence. See Docker, Podman, OrbStack, Colima.
wtc picks the compose command in this order:
WTC_COMPOSE_COMMANDenvironment variable — any command, split on whitespace:WTC_COMPOSE_COMMAND="podman compose", an absolute path, or global flags such asWTC_COMPOSE_COMMAND="docker --context colima compose".composeCommandin.wtcrc.json/package.json#wtc— one of the four standard commands below.- Auto-detection, in order:
docker compose(Compose v2) →podman compose→docker-compose(standalone, including legacy v1) →podman-compose. When more than one is installed, the first whose daemon answers wins, so a quit Docker Desktop doesn't shadow a running Podman machine.
| Engine | How |
|---|---|
| Docker Desktop / Docker Engine | docker compose (auto-detected) |
| OrbStack, Colima | through the docker CLI and its current context (auto-detected); for another context, WTC_COMPOSE_COMMAND="docker --context <name> compose" |
| Podman | podman compose or podman-compose (auto-detected); on macOS and Windows, start its VM with podman machine start |
Anything else (e.g. nerdctl compose) |
WTC_COMPOSE_COMMAND |
Status checks, teardown, and cleanup use the same CLI and global flags as compose, so they always talk to the same daemon. wtc doctor shows which engine was picked and its version, and whether its daemon is reachable. wtc promote doesn't need an engine at all.
Built-in MCP server, so AI agents can manage their stacks themselves. It works on the git repository of the directory it starts in — your agent's project. The setups below use the global install.
Claude Code — for every project:
claude mcp add --scope user wtc -- wtc mcp(Without --scope user, Claude Code registers it for the current project only.) Or share it with your team through a project .mcp.json:
{
"mcpServers": {
"wtc": { "command": "wtc", "args": ["mcp"] }
}
}Codex — ~/.codex/config.toml:
[mcp_servers.wtc]
command = "wtc"
args = ["mcp"]Cursor — .cursor/mcp.json, with the same JSON as .mcp.json above.
Without a global install, use npx with the package name (never npx wtc):
{ "command": "npx", "args": ["-y", "worktree-compose", "mcp"] }After upgrading wtc, restart your MCP client so it runs the new version.
| Tool | Parameters | What it does | Safety |
|---|---|---|---|
wtc_start |
indices?: (number | string)[], envFile?: string, workspaces?: string[], build?: boolean (default true) |
Same as wtc start |
not destructive |
wtc_stop |
indices?: (number | string)[] |
Same as wtc stop (compose stop) |
not destructive: containers and volumes are kept |
wtc_restart |
same as wtc_start |
Same as wtc restart |
not destructive |
wtc_list |
— | First content item is the JSON array of wtc list --json; warnings follow as separate items |
read-only: never writes the registry |
wtc_promote |
index: number | string, force?: boolean |
Same as wtc promote, into the checkout the server runs in |
destructive: overwrites and deletes files there |
wtc_clean |
force?, keepWorktrees?, dryRun?, sweep? (all boolean) |
Same as wtc clean |
destructive: deletes volumes, removes worktrees |
wtc_prune |
dryRun?: boolean |
Same as wtc prune |
destructive: deletes removed worktrees' volumes |
wtc_doctor |
— | Same checks as wtc doctor, as JSON |
read-only |
indices / index take selectors: a JSON number is always an index; a string can be an index, branch, directory name, path, or .. Omit indices for all worktrees.
- Tools that change stacks or files (start, stop, restart, promote, clean, prune) run one at a time, so a client retry can't launch a second
compose upfor the same stack. - Cancelling a request stops the compose command it started; a request cancelled while it waits its turn does not run at all. Requests with a
progressTokenget progress notifications while compose runs. - Every tool returns its progress lines. Failures come back as
isError: truewith the reason (including the tail of compose's error output) — also when a teardown or removal insidewtc_stop,wtc_clean, orwtc_prunefailed. - Tool annotations mark
wtc_listandwtc_doctorread-only andwtc_promote,wtc_clean, andwtc_prunedestructive, so clients can ask before running them. - stdout carries only JSON-RPC; all logging goes to stderr.
wtc ships an agent skill (SKILL.md) that teaches AI coding agents the worktree workflow: commands and selectors, stop vs. clean, ports, env files, config, and the MCP tools. Install the copy that matches your installed version:
wtc skill --install # ~/.claude/skills/worktree-compose/SKILL.md
wtc skill --dir ~/my-agent/skills # <dir>/worktree-compose/SKILL.md
wtc skill # print itRe-run wtc skill --install after upgrading wtc.
cd myapp
npm install -g worktree-compose
git worktree add -b agent-1-auth ../myapp-agent-1
git worktree add -b agent-2-auth ../myapp-agent-2
wtc start
# Worktree 1: BACKEND_PORT=28001 FRONTEND_PORT=25174
# Worktree 2: BACKEND_PORT=28002 FRONTEND_PORT=25175
# Compare side by side
# http://localhost:25174 (agent 1)
# http://localhost:25175 (agent 2)
wtc promote agent-1-auth
git add -A && git commit -m "feat: auth from agent 1"
wtc clean --dry-run # preview
wtc clean # worktrees with uncommitted work are kept; --force discards them1.0 changes a few behaviors; the changelog has the full list.
- Node.js 20+.
- Install globally and type
wtc— without an install,npx wtcruns an unrelated package. Update MCP configs to"command": "wtc", "args": ["mcp"]. - Indices are stable. Your existing stacks keep their names on the first run; after that, removing a worktree never shifts the others. Deleted worktrees show as
(removed)untilwtc prune. wtc stopkeeps containers (compose stopinstead ofdown);startresumes them. Onlycleanandpruneremove stacks and volumes.wtc cleankeeps worktrees that hold work (uncommitted changes, unreachable commits, unpushed submodule commits, locked) unless you pass--force, and only removes Docker resources of stacks it owns. Stale 0.2 stacks that still have containers:wtc clean --sweep. 0.2'swtc stopkept named volumes, so a worktree deleted afterwtc stopcan leave volumes with no container;wtc cleanlists those but never removes them. Find them withdocker volume ls --filter label=com.docker.compose.project=<project>and remove them withdocker volume rm.- Env keys are rewritten in place, and the root env file now gets
COMPOSE_PROJECT_NAME. wtc promotealso aborts when the target branch changed the same files since the worktree branched (--forceoverrides), and untracked files in the target now count as conflicts.
- Node.js >= 20
- Git 2.17 or newer, with worktree support (2.31+ recommended)
- Docker with Compose v2 (
docker compose), or Podman (podman compose/podman-compose) — see engines - A compose file with
${VAR:-default}host ports
Start with wtc doctor: it checks the engine, the daemon, ports, isolation settings, missing worktrees, and orphaned stacks, and says what to do.
npx wtc does something unexpected — when worktree-compose isn't installed, it downloads and runs an unrelated npm package named wtc. Install globally (npm install -g worktree-compose) and type wtc, or use npx worktree-compose <command>.
"No compose file found" — wtc reads the compose file from the root of the main checkout, not the current directory: compose.yaml, compose.yml, docker-compose.yaml, or docker-compose.yml.
"No extra worktrees found" — Create worktrees first: git worktree add -b my-branch ../my-feature
Ports not changing — Use ${VAR:-default} with an upper-case variable for host ports, not hardcoded numbers. wtc doctor lists which ports are isolatable.
"Bind for 0.0.0.0:25435 failed: port is already allocated" — Something else listens on that port. wtc doctor reports allocated ports that are already taken; stop the other process, or raise portStride. If you run docker compose yourself with a custom envFile, pass --env-file <envFile> (Compose only reads .env on its own) or export the port (e.g. POSTGRES_PORT=25435 docker compose up).
A stack shows as (removed) — its worktree was deleted but its stack or record remains. wtc prune tears it down (and frees its index); preview with wtc prune --dry-run.
Leftover containers, networks, or volumes — wtc clean --dry-run shows what wtc clean would remove. Leftovers of stacks whose worktree directory no longer exists (e.g. from wtc 0.2) are listed, and removed with wtc clean --sweep as long as the stack still has a container (that is where the working directory is recorded). Volumes or networks with no container left, and resources merely named like this repo's stacks, are listed and never removed by wtc. Inspect one project exactly before removing anything yourself: docker ps -a --filter label=com.docker.compose.project=<project> shows nothing for volume-only leftovers, so also run docker volume ls --filter label=com.docker.compose.project=<project> and docker network ls --filter label=com.docker.compose.project=<project>.
"Refusing to start worktree stacks: cannot locate the main checkout" (also restart, clean) — the repository layout doesn't say which checkout is the main one (see repository layouts). In a bare repository, check the default branch out in your main worktree again (finish the rebase or bisect), or add a worktree for it; with --separate-git-dir, run it from the main checkout (after one wtc start there, linked worktrees find it too).
wtc clean kept a worktree — it prints why (uncommitted changes, unreachable commits, unpushed submodule commits, locked, or the worktree you're in). Commit or push the work, or pass --force to discard it.
"Timed out waiting for the wtc state lock" — another wtc command (or MCP call) is updating .git/wtc/state.json; retry when it finishes. If no wtc process is running at all, delete the lock file the message names.
"…was written by a newer wtc (state version N); upgrade wtc." — an older wtc (often an MCP server started by a cached install) met a registry written by a newer one. Upgrade it (npm install -g worktree-compose@latest) and restart your MCP client. The older version leaves the file untouched.
Wrong engine, or the daemon is down — wtc doctor shows which engine was picked and whether its daemon answers. Start the daemon, or set WTC_COMPOSE_COMMAND (e.g. "podman compose").
Issues and pull requests are welcome. Found a bug or have an idea? Open an issue — and if wtc saves you time, a ⭐ on the repo helps others find it. Docs and demos live at worktree-compose.com.