Switch between multiple branch environments in an instant.
A CLI tool built on Git worktrees that gives every branch its own isolated working directory — with automatic .env copying, port remapping, Docker Compose isolation, Docker volume cloning so each branch starts with the same data your main worktree has, and symlinks for heavy directories like node_modules.
- Why wtb?
- Philosophy & scope
- How it works
- Quick start
- Commands
- Configuration
- Environment variable adjustment
- Docker Compose integration
- Volume cloning
- Lifecycle scripts
- Architecture
- Development
- Design notes
- Requirements
- Claude Code integration
- Troubleshooting
- FAQ
- Roadmap
- Changelog
- License
Git worktrees are powerful but awkward on their own: every new working directory needs its gitignored files copied, dependencies installed, ports remapped, and long-lived services restarted. wtb automates that glue so each branch feels like a self-contained mini-environment.
Typical use cases:
- You're in the middle of a feature branch and an urgent hotfix lands — spin up a second working directory in seconds.
- You want several feature branches building, testing, or serving in parallel without port collisions.
- You need a clean checkout to review a PR without stashing, resetting, or killing your running dev server.
- You'd like
.env, local configs, or credentials automatically copied (and adjusted) to each new worktree. - You run Docker Compose and need each branch's services on their own ports.
wtb is built for a particular way of working: running many changes — including ones that touch the database and backend — in true parallel, one isolated worktree per change.
- Parallelism is the speedup. In vibe-coding workflows, doing DB- and backend-touching changes in full parallel (a worktree per change) is where the time savings come from.
- Every worktree is fully autonomous on code. Each worktree can change and run code on its own, completely independent of the others.
- Every worktree is fully autonomous on data. Each worktree starts from a complete copy of the DB state, so it can write migrations and mutate data freely without affecting any other worktree.
- Conflicts are expected — and that's fine. Working this way, conflicts are the norm; the best code emerges from the collision of competing requirements. wtb deliberately does not try to resolve them for you.
- Docker Compose only, for now. wtb currently supports Docker Compose plus its YAML and env files only. Other stacks are out of scope at this stage.
- No coding-agent orchestration (yet). wtb does not orchestrate coding agents. A coding agent launched inside a worktree should treat its job as done once it finishes the task; if more work is needed, a human is expected to go in and pick it up. In practice the recommended pattern is to let the agent run all the way to opening a pull request.
- The author is partial to the V6 hybrid power units used in F1.
project/ ← main worktree (your original repo)
├── wtb.yaml
├── .env APP_PORT=3000
├── docker-compose.yml 3000:80
├── node_modules/
└── src/
worktree-feature-auth/ ← created by `wtb create feature/auth`
├── .env APP_PORT=3001 (auto-bumped, collision-free)
├── docker-compose.yml 3001:80 (auto-bumped)
├── node_modules -> ../project/node_modules (symlinked, not copied)
└── src/ (git worktree — shares the same .git)
When you run wtb create <branch>, the tool walks these phases in order:
- Worktree —
git worktree addat../worktree-<sanitized-branch>/(or a custom-p <path>), branching frombase_branchunless the branch already exists. - Copy files —
copy_files(gitignored configs, secrets, etc.) are copied over. Paths also listed inlink_filesare skipped here. - Symlink —
link_filesentries are symlinked back to the source (existing files/dirs/symlinks are replaced safely). - Environment files —
env.fileentries are copied; ifenv.adjustis non-empty, port-style values are bumped to the next free port that doesn't collide with other worktrees'.envfiles. - Docker Compose — if
docker_compose_fileis configured, wtb reads it, remaps host ports around running containers, and writes the adjusted copy into the worktree. - Volume clone — every named (non-
external) Docker volume declared in the Compose file is cloned to the new worktree's project, so e.g. PostgreSQL data carries over without re-seeding. If the source stack is running (the usual case for a live dev DB), wtb automatically stops it, clones, and restarts it so the copy is corruption-safe — no manual step. Pass--no-stopto skip in-use volumes instead, or--force-volume-copyto clone live. See Volume cloning. - Start command —
start_command, if configured, runs inside the new worktree with/bin/sh.
wtb remove <branch> runs in reverse: docker compose down (or down -v with --remove-volumes, unless end_command is set), then end_command, then git worktree remove.
npm install -g @schemelisp/wtb
# or one-shot
npx @schemelisp/wtb create feature/awesome# wtb.yaml
base_branch: main
copy_files:
- .env
- .env.local
link_files:
- node_modules
env:
file:
- .env
adjust:
APP_PORT: 1 # auto-bump to the next free port
DB_PORT: 1wtb create feature/awesome
cd ../worktree-feature-awesome
# ...hack...
wtb remove feature/awesomePreview without touching anything:
wtb create feature/awesome --dry-runCreates a new worktree for <branch>, branching from base_branch unless the branch already exists.
Pipeline (short version): worktree → copy → symlink → env → compose → start.
Default path: ../worktree-<branch-with-"/"-replaced-by-"-">. Use -p to override.
| Option | Description |
|---|---|
-p, --path <path> |
Custom worktree location |
--no-create-branch |
Use an existing branch (fails if it doesn't exist) |
--no-docker |
Skip Docker Compose copy/port-remap — also skips volume cloning (the volume phase requires Docker), so the worktree starts with empty volumes |
--no-env |
Skip env.file copy + env.adjust |
--no-copy |
Skip copy_files |
--no-link |
Skip link_files symlinks |
--no-start |
Skip start_command |
--no-volume-copy |
Skip cloning Docker volumes from the source project |
--force-volume-copy |
Clone volumes even when the source container is running or the target volume already has data |
--no-stop |
Don't auto-stop the source Compose stack before cloning; skip in-use volumes instead (the old behavior) |
--seed |
Seed the data instead of cloning: skip the volume-clone phase and run volumes.seed_command in the new worktree. Never touches the source volume, so the source stack is left running. Requires volumes.seed_command in the config; mutually exclusive with --force-volume-copy. See Volume cloning |
--strict |
Exit non-zero (1) if any volume clone or the seed command fails (default: exit 0 — the worktree still exists). For CI / coding-agent pipelines that must detect incomplete data isolation. See Volume cloning |
--dry-run |
Print the plan, make no changes |
Examples:
wtb create feature/quick-fix --no-docker # skip Docker even if configured
wtb create feature/wip --no-start # skip install/setup
wtb create release/v2 --no-create-branch # attach to an existing branch
wtb create feature/minimal \
--no-docker --no-env --no-copy --no-link --no-start # bare git worktree only
wtb create feature/test --dry-run # preview
wtb create feature/auth -p /tmp/auth-wt # custom pathRemoves the worktree that owns <branch>. Guards against removing the main repository.
| Option | Description |
|---|---|
-f, --force |
Pass --force to git worktree remove (uncommitted changes) |
--no-docker |
Skip docker compose down in the worktree |
--no-end |
Skip end_command |
--remove-volumes |
Also delete this worktree's Docker volumes (docker compose down -v). Has no effect (wtb warns) when teardown is skipped — i.e. with --no-docker or when end_command is set (your end_command must drop the volumes itself) |
Ordering: Docker teardown → end_command → git worktree remove. If end_command is set, wtb assumes you own teardown and skips the automatic docker compose down.
wtb remove feature/old --no-docker # Docker daemon already stopped
wtb remove feature/abandoned -f --no-end # force-remove, skip cleanupRe-runs only the volume-clone phase for an existing worktree — useful when a clone previously failed or was skipped (empty/stale volumes) and you want to recover the data without removing and recreating the worktree (so uncommitted work is safe). Defaults to the current worktree; pass a branch to target another.
| Option | Description |
|---|---|
--force-volume-copy |
Clone even when the source container is running or the target already has data (overwrite is atomic) |
--no-stop |
Don't auto-stop the source Compose stack; skip in-use volumes instead |
--strict |
Exit non-zero (1) if any volume fails to clone (default: exit 0). For CI / coding-agent pipelines that must detect incomplete data isolation |
--dry-run |
Print which volumes would be cloned; make no changes |
wtb reclone # current worktree
wtb reclone feature/auth # a specific worktree
wtb reclone feature/auth --force-volume-copy # overwrite stale target dataPrints the same N cloned, N skipped, N failed summary as create; a failure exits 0 with the loud ⚠️ … data is NOT fully isolated line (resolve and re-run). Refuses to target the main repository worktree (source and target would be the same project). If docker_compose_file isn't configured, it's a no-op with a message. To re-seed instead of re-clone, run your volumes.seed_command inside the worktree.
Removes wtb-managed Docker volumes that are orphaned — i.e., volumes wtb cloned for worktrees that no longer exist (because wtb remove leaves volumes by default) — plus leftover temp volumes from interrupted --force-volume-copy overwrites. Over many create/remove cycles these accumulate; prune is the cleanup. Only volumes labelled wtb.managed=true are ever touched, and a volume is removed only if it belongs to no existing worktree of this repo.
| Option | Description |
|---|---|
-y, --yes |
Actually remove the volumes. Without this, prune is a dry run (lists candidates only) |
--json |
Machine-readable output ({ dryRun, candidates, removed }) |
wtb prune # preview what would be removed (safe; deletes nothing)
wtb prune --yes # remove the orphaned + leftover temp volumes
wtb prune --json # machine-readable preview for scripts/agentsSafety: it's dry-run by default (deletion needs --yes); a volume currently in use by a container is skipped; and a worktree's volume is matched by its exact Compose project prefix (<project>_…), so it never removes a live worktree's data. Determines "live" worktrees from git worktree list for this repo.
Lightweight, scriptable listing of worktrees — like Unix ls. Use this instead of status when you just want to see what worktrees exist, without the Docker noise.
| Option | Description |
|---|---|
-l, --long |
Long format: short hash, relative age, dirty flag, subject |
--json |
Machine-readable JSON (combines with -l for enriched fields) |
-p, --paths |
Absolute paths only, one per line — pipe-friendly |
Default (compact, 1 git call):
→ main /Users/me/proj [main]
feature/api /Users/me/proj-worktrees/feature-api
feature/ui /Users/me/proj-worktrees/feature-ui [locked]
hotfix/crash /Users/me/proj-worktrees/hotfix-crash [prunable]
(detached) /Users/me/proj-worktrees/detached-xyz
Long (-l, extra git log/git status per worktree in parallel):
BRANCH COMMIT AGE D PATH TAGS / SUBJECT
→ main a1b2c3d 2h ago * /Users/me/proj [main] Add foo
feature/api 9f8e7d6 3d ago /Users/me/proj-worktrees/feature-api WIP refactor
Legend:
→in column 0 marks the worktree that contains your current working directory (works even in detached HEAD).- Tags:
[main](main repository worktree),[locked](git worktree lock),[prunable](worktree directory gone),[bare](bare repository). Dcolumn:*means the worktree has uncommitted changes.
Paths-only for shell pipelines:
cd "$(wtb ls -p | fzf)" # fuzzy-jump between worktrees
wtb ls -p | xargs -I{} du -sh {} # disk usage per worktreeJSON:
wtb ls --json | jq '.[] | select(.isMain == false) | .path'
wtb ls -l --json | jq '.[] | select(.dirty == true)'JSON fields (always): path, branch, head, isMain, isCurrent, locked, prunable, bare, detached.
With -l: adds shortHash, subject, ageRelative, ageTimestamp, dirty — plus enrichmentError if per-worktree enrichment failed (e.g., prunable).
Prints the adjusted env.adjust values, Docker Compose host/container ports, and a pre-rendered http://localhost:<port> endpoint list for the current worktree (or all worktrees).
| Option | Description |
|---|---|
--all |
Output an array of every worktree's ports (default: current worktree as an object) |
--pretty |
Human-readable table instead of JSON |
Designed to be called from Claude Code (via the shipped skill) or from shell scripts. See Claude Code integration for the full output schema.
Richer inspection: worktrees + Docker Compose services + running containers + volumes. Slower than ls because it shells out to Docker.
| Option | Description |
|---|---|
-a, --all |
Show all worktrees (default: current branch only) |
--docker-only |
Suppress worktree section, show only Docker info |
--json |
Machine-readable JSON (worktrees + Docker state) on stdout — for scripts/agents |
📁 Git Worktrees Status
→ main (main)
📂 /Users/me/project
🐳 Docker: docker-compose.yml
📦 Services: 3
🔧 Environment: .env, .env.local
--json returns one structured object ({ worktrees: [...], docker: {...} }) and stays valid JSON even when Docker is down (docker.available: false). This completes the machine-readable trio with wtb ls --json and wtb ports.
wtb status --json | jq '.docker.containers[] | select(.isWtb) | .name' # this project's containers
wtb status -a --json | jq '.worktrees[] | {branch, services: .compose.services}'Installs the bundled Claude Code skill into this repo (or globally). See Claude Code integration for what the skill does.
| Option | Description |
|---|---|
-f, --force |
Overwrite an existing SKILL.md |
--user |
Install at ~/.claude/skills/wtb/ instead of the repo |
--dry-run |
Print the target path; don't write |
wtb searches for a config file in this order and stops at the first match:
wtb.yamlwtb.yml.wtb.yaml.wtb.yml.wtb/config.yaml.wtb/config.yml
If nothing is found, wtb still runs with defaults (prints a warning to stderr). The config is merged with defaults — any field you omit gets the default.
| Field | Type | Default | Description |
|---|---|---|---|
base_branch |
string | "main" |
Base branch used when creating a brand-new branch |
docker_compose_file |
string | "" |
Path (relative to config) to the Compose file. Empty/omitted → Docker skipped entirely |
copy_files |
string[] | [] |
Files/dirs to copy to new worktrees (even if gitignored). Directories are copied recursively |
link_files |
string[] | [] |
Files/dirs to symlink into the new worktree. Takes priority over copy_files on duplicates |
start_command |
string | — | Runs in the new worktree via /bin/sh after creation. Relative scripts are resolved against the worktree root |
end_command |
string | — | Runs in the worktree before removal. Setting this suppresses the automatic docker compose down |
env.file |
string[] | ["./.env"] |
Env files to copy into the worktree |
env.adjust |
map | {} |
Per-key adjustment (see Environment variable adjustment) |
volumes.exclude |
string[] | [] |
Compose volume keys to exclude from auto-cloning. Default: every named non-external volume in the Compose file is cloned. See Volume cloning |
volumes.seed_command |
string | — | Command run in the new worktree (via /bin/sh) when wtb create --seed is used, instead of cloning volume data. Lets a worktree start from a freshly seeded DB rather than a copy of main's. See Volume cloning |
On load, wtb validates the config:
- Errors (fail with exit code
4): wrong types, missing/invalidbase_branch, non-arraycopy_files/link_files, invalidenv.adjustvalue type. - Warnings (stderr, keep running): referenced
docker_compose_file/env.filenot found on disk; anenv.adjustkey that isn't a valid POSIX env var name (it would never match a.enventry — wtb suggests a sanitized form).
# wtb.yaml — full example
base_branch: main
docker_compose_file: ./docker-compose.yml
# Copied into each new worktree even when gitignored
copy_files:
- .env
- .env.local
- .secrets
- config/
# Symlinked back to the source repo — avoid copying giant dirs
link_files:
- node_modules
- .cache
- .next/cache
# Lifecycle scripts — run inside the worktree via /bin/sh
start_command: npm install && npm run db:migrate
end_command: docker compose down -v
env:
file:
- .env
- .env.local
adjust:
APP_PORT: 1 # any number → "auto-bump to the next free port"
DB_PORT: 1
API_KEY: "dev-key" # string → literal replacement
DEBUG_PORT: null # null → remove the variable entirelyenv.adjust lets you rewrite values in every env file as it is copied. Three value types are supported:
| Value type | Behavior on existing key | Behavior when key is absent |
|---|---|---|
| number | Scans other worktrees + this file for the same key's port, then picks the first free port starting at original + 1. The number literal itself is used as a type marker — any positive integer works. |
Nothing is added — wtb prints a warning. A port adjustment needs an existing value to bump, so writing the marker integer (e.g. PORT=1) would be meaningless. Define the key in the file, or use a string value to add a literal. |
| string | Value is replaced verbatim. | Key is appended with the string value. |
| null | Key is removed from the output. | No-op. |
Port collision sources considered:
- Other worktrees'
.envfiles (only for keys listed as numbers inenv.adjust). - Other numeric entries in the current adjustment pass (so a single file doesn't collide with itself).
Key naming: only POSIX-compliant names (^[A-Za-z_][A-Za-z0-9_]*$) are recognized as adjustable variables. Lines whose key doesn't match are passed through untouched (they're treated as ordinary file content, not as keys to adjust).
When docker_compose_file is set and the file exists:
- wtb reads it from the source repo.
- Calls
docker psto collect ports already claimed by running containers. - For every
services.*.portsmapping, the host port is rewritten to the first free port at/above the original, honoring the running-container set plus any ports already remapped in this pass. - Writes the adjusted Compose file into the worktree at the same relative path.
Notes:
- Port format recognized:
HOST:CONTAINER,0.0.0.0:HOST:CONTAINER, optional/tcp//udp. - The original host port is tried first — if the base port is free, it's kept. (Env-file adjustment is stricter and always starts at
original + 1.) - If Docker isn't installed or the daemon isn't running, wtb copies the Compose file without remapping and prints a warning — your worktree still works, you just own port collisions.
wtb removecallsdocker compose downin the worktree before removing it, unlessend_commandis set (then you own teardown) or--no-dockeris passed.- Disable Compose integration entirely by omitting the field or setting it to
"".
After remapping the Compose file, wtb automatically clones every named Docker volume declared in the Compose volumes: section from the source project to the new worktree's project. This is what makes a new worktree start with the same database/cache contents your main worktree already has — no manual pg_dump | pg_restore cycle, no re-seeding.
How it works:
- wtb enumerates
volumes:keys from the Compose file. - Volumes marked
external: trueare skipped (they're shared by design). - Source volume name is resolved as
<source_project>_<key>(or the explicitvolumes.<key>.nameif set). Same for the target with the new worktree's project name. - Stop-then-copy. If any cloneable source volume is in use by a running container, wtb stops the source Compose stack (
docker compose stop— containers/networks are preserved), clones, then restarts it (docker compose start). The restart runs in afinallyblock and is also wired toSIGINT, so an interrupted clone (Ctrl-C) never leaves your source services down. This makes a live dev DB clone safely with no manual step. Opt out with--no-stop(then in-use volumes are skipped with a warning instead), or use--force-volume-copyto clone live without stopping (data-corruption risk). Note:docker compose startbrings up every stopped service in the project — if you had intentionally left some down, re-stop them after. - For each volume:
- If the source stack was stopped (or
--force-volume-copywas passed, or nothing was running), wtb clones it. - If
--no-stopis set and a running container is using the source volume, wtb skips it with a warning (a live filesystem copy of an active database can corrupt — Postgres/MySQL/Redis). Stop the source side withdocker compose stopfirst, drop--no-stop, or pass--force-volume-copy. - If the target volume already has data, wtb skips it (assumes you've already populated it). Pass
--force-volume-copyto overwrite. The overwrite is atomic: wtb stages the new data into a temporary volume and verifies it before replacing the target, so a failed copy never leaves the target emptied. - Otherwise, wtb does a recursive copy via a transient
instrumentisto/rsync-sshsidecar container (with an Alpinecp -afallback if rsync isn't available).
- If the source stack was stopped (or
Every volume wtb creates is labelled wtb.managed=true, so it is self-identifying regardless of how the project/path is named. wtb status uses this label to report wtb-managed volumes accurately (even for custom -p paths), and you can list them yourself with docker volume ls --filter label=wtb.managed=true.
Selectively exclude volumes you don't want to clone (e.g. regenerable caches):
# wtb.yaml
volumes:
exclude:
- cache_data
- tmp_dataDisable the whole phase per-invocation with wtb create <branch> --no-volume-copy. Keep the source stack running and skip in-use volumes with --no-stop. Force-clone running source volumes live (data-loss risk, dev only) with --force-volume-copy.
The per-volume summary reports N cloned, N skipped, N failed. If any volume fails to clone, the worktree is still created but the final banner changes from 🎉 Worktree created successfully! to ⚠️ Worktree created, but N volume(s) FAILED to clone — this worktree's data is NOT fully isolated, so the incomplete state is obvious. By default the command still exits 0 (the worktree exists) — pass --strict to make a clone (or --seed) failure exit 1 instead, so CI and coding-agent pipelines can detect incomplete data isolation. A skip is intentional (external/excluded volume, missing source, in-use under --no-stop, or a target that already has data); a failure means the copy itself errored.
wtb remove <branch> does not delete cloned volumes by default (consistent with docker compose down). Pass wtb remove <branch> --remove-volumes to also drop them (docker compose down -v). Because that runs through the automatic teardown, --remove-volumes is a no-op (with a warning) when teardown is skipped — under --no-docker, or when end_command is set (then your end_command owns volume removal). Volumes left behind this way accumulate as orphans over time — sweep them with wtb prune (every wtb-created volume carries the wtb.managed=true label).
Sometimes you don't want a copy of main's data — you want a freshly seeded database in the new worktree (a clean migration target, a deterministic test fixture, etc.). Configure a seed command and pass --seed:
# wtb.yaml
volumes:
seed_command: docker compose up -d db && npm run db:migrate && npm run db:seedwtb create feature/clean-db --seedWith --seed, wtb skips the volume-clone phase entirely and runs volumes.seed_command in the new worktree (via /bin/sh, cwd = worktree root, same path-or-shell resolution as start_command). Because nothing is ever read off a live source volume, this path never stops the source stack — your main services keep running untouched. This is the "data-autonomous by construction" path: the worktree's data is built fresh, not copied.
Notes:
--seedrequiresvolumes.seed_commandto be set; otherwisecreatefails with exit4before creating the worktree.--seedand--force-volume-copyare mutually exclusive (one seeds, the other clones) — passing both fails with exit1.- If the seed command fails, the worktree is still created but the banner becomes
⚠️ Worktree created, but the seed command FAILED — this worktree's data is NOT ready(exit stays0, same contract as a failed clone; pass--strictto exit1instead). Re-run the seed inside the worktree after fixing it.
start_command and end_command run inside the worktree with cwd set to the worktree root and a /bin/sh shell. For start_command, wtb first tries resolving the string as a path relative to the worktree (so ./scripts/setup.sh works); if the file doesn't exist it's passed to the shell as-is (so npm install && npm run dev also works).
Script failures are non-fatal — wtb prints a warning and the worktree is left in place so you can finish the setup manually.
src/
├── cli/
│ ├── commands/ create, remove, reclone, prune, ls, ports, status, init-claude
│ ├── utils/ worktree/ports renderers, command error wrapper, claude skill installer
│ └── index.ts commander wiring + global error handlers
├── core/
│ ├── config/ YAML loader + validator + defaults merge
│ ├── git/ repository / worktree / commit-info helpers
│ ├── docker/ `docker ps`, compose parse/write, port adjust
│ └── environment/ .env parser (order-preserving) + adjust + serialize
├── utils/ safe exec helpers (execFileSync wrappers), errors
├── types/ all public types (WtbConfig, WorktreeInfo, …)
├── constants/ defaults, command templates, regex, exit codes
└── index.ts library entry point
For full module-by-module API surface and design rationale, see ARCHITECTURE.md.
Key design choices:
- No shell-injection surface for git/docker. Anything derived from user input (branch names, paths) is passed to
execFileSyncas array arguments, never interpolated into a shell string. A few fixeddocker composeinvocations useexecSyncwith hardcoded constants only (no user input). The one place a shell is used intentionally is user-supplied lifecycle scripts, which run via/bin/sh. - Defaults-merge with
??. Missing fields fall back to defaults, but empty arrays/strings you explicitly set are preserved. - Order-preserving
.envparsing. Comments, blank lines, and inline# commentssurvive the copy + adjust round-trip. - Pure renderers for
ls.renderDefault/renderLong/renderPaths/renderJsonare unit-tested in isolation; the command module just wires them up. - Enrichment is best-effort.
ls -lfalls back gracefully on prunable/broken worktrees and still prints the rest — the failure is surfaced in JSON asenrichmentError.
Exit codes (src/constants/index.ts):
| Code | Meaning |
|---|---|
0 |
Success |
1 |
General error |
2 |
Invalid CLI usage |
3 |
Not in a git repository |
4 |
Configuration error (config not found-but-invalid, parse failure, or validation failure) |
5 |
Docker error — reserved. Docker is optional and degrades gracefully (warn + continue), so wtb does not currently exit with this code; Docker problems either warn (and the command still succeeds) or surface as 1. |
git clone https://github.com/origamium/wtb.git
cd wtb
npm install
npm run dev # run the CLI from source (tsx)
npm run build # tsc → dist/
npm start # run the built CLI
npm run test # vitest watch
npm run test:run # vitest once
npm run test:unit # unit tests (src/)
npm run test:e2e # e2e (creates real git repos under test-repos/)
npm run test:integration # real-Docker volume-clone checks (skips if Docker is absent)
npm run test:ui # vitest UI
npm run typecheck # tsc --noEmit
npm run lint # biome lint
npm run format # biome format --write
npm run check # biome check --write (lint + format)E2E tests (e2e/) create temporary git repos and exercise the compiled CLI end-to-end. See sample/ for a runnable playground — a tiny Next.js + Postgres stack with a real wtb.yaml, .env, and docker-compose.yml.
For a broader spread of configs — full-stack Compose, minimal Compose, seed/exclude/external volumes, a no-Docker Node project, and a bare-minimum setup — see examples/. Each is a self-contained project, and examples/try.sh <example> [branch] [--real] drives the real CLI against any of them in a throwaway git repo (dry-run by default):
examples/try.sh # list the examples
examples/try.sh minimal # preview the plan
examples/try.sh compose-minimal feature/db --real # real run (clones the DB volume)- Symlinks beat copies for large trees.
node_modules,.cache,.next/cacheshould almost always go inlink_files. One source of truth, zero disk duplication, instant worktree creation. The tradeoff: native modules rebuilt for a different platform in one worktree affect all of them — usecopy_filesfor those. - Branch name sanitization.
/in branch names becomes-in the default path:feature/auth→worktree-feature-auth. Use-p <path>if you need full control. - Docker is optional at every step. Omit
docker_compose_file, or install without Docker, or pass--no-docker— wtb degrades gracefully and only produces Docker-related output when Docker is reachable. wtb lsvswtb status.lsis for fast, scriptable enumeration (1 git call in the default form).statusis for human inspection with Docker context. Usels -l --jsonin scripts.- Dry-run is honest.
--dry-runwalks every phase and prints what it would do, including which files are missing and would be skipped.
- Node.js ≥ 18
- Git (any modern version with
worktreesupport) - Docker + Docker Compose (optional — only if
docker_compose_fileis configured)
wtb ships a Claude Code skill that teaches the agent how to inspect this repo's worktrees and call the CLI itself. Once installed, Claude can answer "what port is this worktree on?" or "spin up a worktree for feature/auth" without any hand-holding.
wtb init-claude # writes .claude/skills/wtb/SKILL.md
git add .claude/skills/wtb
git commit -m "chore: install wtb Claude Code skill"Because .claude/skills/ is a regular tracked directory, every worktree you create with git worktree add / wtb create automatically inherits the skill — there is nothing to sync per-worktree.
Prefer a global install?
wtb init-claude --user # writes ~/.claude/skills/wtb/SKILL.mdFlags: -f, --force (overwrite existing), --user (global), --dry-run (preview target path only).
The skill tells Claude to call wtb ports (JSON is the default output — there is no --json flag). The command is useful on its own too:
wtb ports # current worktree as a JSON object
wtb ports --all # every worktree as a JSON array
wtb ports --pretty # human-readableOutput shape:
{
"path": "/Users/me/worktree-feature-auth",
"branch": "feature/auth",
"env": { "APP_PORT": "3001", "DB_PORT": "5433" },
"compose": {
"file": "docker-compose.yml",
"services": {
"web": { "host_ports": [3001], "container_ports": [80] },
"db": { "host_ports": [5433], "container_ports": [5432] }
}
},
"endpoints": ["http://localhost:3001", "http://localhost:5433"]
}Notes:
envonly contains keys listed underenv.adjustinwtb.yaml— other.enventries (secrets, API keys) are not leaked.compose.servicesis populated from the worktree's copy of the Compose file, so it reflects the already-adjusted ports.endpointsis a convenience list ofhttp://localhost:<port>entries built from compose host ports.- stdout stays valid JSON even if Docker isn't installed (
compose.servicesbecomes{}). Warnings go to stderr.
With the skill installed, typical prompts just work:
| You say | Claude does |
|---|---|
| "What port is the API on here?" | wtb ports → picks the right host port (JSON by default) |
| "List the worktrees." | wtb ls -l |
| "Make a worktree for feature/login." | wtb create feature/login (prompts you first if destructive) |
| "Clean up feature/old." | wtb ls -l to show the target → confirms → wtb remove feature/old |
| "This worktree's DB is empty / the clone failed." | wtb reclone → re-runs just the volume-clone phase, no worktree recreation |
| "What's actually running for this worktree?" | wtb status --json → live containers/volumes as structured data |
The skill's description triggers automatically when wtb.yaml is in the repo, so you usually don't need to invoke it by hand.
Run wtb from anywhere inside your repo. It discovers the git root via git rev-parse --show-toplevel.
wtb adjusts against known sources:
- For
.envfiles: other worktrees'.envfiles containing the same key. - For Docker Compose: currently running containers and the ports it remapped earlier in the same pass.
It does not probe arbitrary OS-level listening sockets. If something outside Docker is holding a port (a native dev server you started by hand, another project on the same machine, etc.), you'll need to stop it or edit env.adjust manually. Check wtb status -a to see what wtb thinks is going on.
The branch already has a worktree. wtb ls shows where it is. wtb remove X cleans it up first.
The branch doesn't exist and you passed --no-create-branch. Drop that flag to create it, or check your branch name.
The config is structurally invalid — the error lists each bad field. Warnings about missing docker_compose_file / env.file paths are non-fatal and go to stderr.
wtb leaves the worktree in place and prints a warning. Finish setup manually in the worktree, then proceed.
docker compose down on remove fails silently with a warning; the worktree still gets removed. On create, wtb skips port adjustment (Compose file is copied verbatim).
Is this different from git worktree add?
wtb uses git worktree add under the hood, then layers on the environment-sync logic that git itself doesn't handle: gitignored config files, symlinks, env-var remapping, Compose port adjustment, and lifecycle scripts.
Do I have to use Docker?
No. Leave docker_compose_file empty (or omit it) and the Docker phases are skipped entirely. Everything else — copy, symlink, env adjust, lifecycle scripts — still works.
What happens to my .git directory?
Untouched. Every worktree shares the same .git via Git's native worktree mechanism; disk usage stays flat.
Can I use this in CI?
Yes — but lifecycle scripts, Docker integration, and port remapping are mostly useful on a dev box. In CI, wtb create <branch> --no-docker --no-start --no-link gives you a clean isolated checkout fast.
Why the "wtb" name? Short for "worktree turbo" — git worktrees, but with the environment-wrangling turbocharged.
Planned, not yet implemented — listed so the intended direction is on record.
- Nothing currently on the list — the previously-planned items have shipped (see below). Open an issue with what you'd like to see next.
Recently shipped (was on this list):
- Seed instead of copy (opt-in). ✅
wtb create --seedrunsvolumes.seed_commandin the new worktree instead of cloning volume data — for a freshly seeded DB rather than a clone of main's. Because nothing is copied off a live volume, this path does not stop the source stack. See Volume cloning → Seed instead of clone. - Stop-then-copy for DB integrity. ✅
createnow auto-stops the source Compose stack before cloning live volumes and restarts it afterward (crash-safe), so a running dev DB clones with no manual step. Opt out with--no-stop.
See CHANGELOG.md for release notes.
MIT © ONOUE Origami