Skip to content

Parallel work — scripts/wt

scripts/wt creates a git worktree per issue at ../<repo>-wt/<branch-leaf>/, with heavy dev-dep directories symlinked back to the main checkout so installs are not duplicated. This is the default workflow whenever more than one issue — or more than one agent session — is in flight, because it prevents the branch-flip problem where one session’s git checkout swaps the working tree out from under another.

Terminal window
scripts/wt new 1234 # worktree + branch off origin/<default>
scripts/wt list # branch, age, pushed-state, path
scripts/wt remove 1234 # refuses if there is uncommitted work
scripts/wt prune # remove merged-and-deleted worktrees — run after every merge
scripts/wt stash # worktree-PRIVATE stash (see guard 4 below)
scripts/wt doctor # verify symlinks and shared stack reuse

It warns at 8 active worktrees and refuses at 10 — a WIP guard, not a resource limit. Worktrees are cheap here (dependencies symlinked, dev stack shared); what the cap protects is your ability to finish things. Raise it per-invocation for a deliberate burst (WT_CAP=16 scripts/wt new 1234) — see Working the harness — a day in the life.

Concurrent wt use is safe by construction, not by coordination. Three structural guards make it impossible for one session’s prune to reap another’s worktree:

  1. --no-track on branch creation. Without it a fresh branch inherits origin/<default> as its upstream, and all three of prune’s conditions then line up for a never-pushed worktree — an upstream exists, origin/<branch> does not, and the branch is trivially an ancestor of the default. The worktree gets reaped while someone is working in it. --no-track leaves no upstream, so the never-pushed check correctly skips it.
  2. A .wt-owner marker and a freshness grace window. prune never removes a worktree younger than WT_GRACE_MIN (default 30 minutes) unless --force is passed, protecting one that another session created moments ago and has not pushed yet.
  3. A per-worktree WT_TEST_DB. Each generated .envrc exports a unique test database name, so parallel test runs in sibling worktrees get isolated databases and no external lock file is needed. Have the project’s test settings read it, and source .envrc before running the suite.
  4. scripts/wt stash instead of git stash. refs/stash lives in the git common dir, so it is not worktree-scoped: every worktree of a repo shares one stack. Worktree A pushes, B pushes, A pops — and A gets B’s work. Because a clean apply also drops the entry, B’s uncommitted work is now gone from the stash and mixed into an unrelated tree, with nothing in either session’s output naming it. (A conflicting apply keeps the entry — that is the lucky branch of the same bug.) There is no pre-stash hook, so nothing can catch it where it happens; it has to be replaced. wt stash [push|pop|apply|list|show|drop] keeps entries under refs/wt-stash/<worktree> and never reads or writes refs/stash. wt doctor warns when refs/stash is non-empty while worktrees are active. Untracked files are not captured (git stash create cannot) and the command says so rather than letting you assume otherwise — for a one-file detour, cp aside and back is still simplest.
  5. Per-worktree E2E ports. Each .envrc also exports WT_E2E_PORT and WT_E2E_DEV_PORT, derived from the worktree’s slug and probed forward past any port a sibling worktree has claimed or a live process holds. Playwright’s reuseExistingServer identifies a running server by port alone, so with one hardcoded port every worktree shares one preview server and a local run can pass against another branch’s bundle. Read the variables in your E2E config (see .claude/rules/tests.md); unset — the main checkout and CI — your defaults apply.

prune recognizes every way a branch lands. A branch counts as merged when its tip is an ancestor of the default branch (a merge commit), when git cherry finds every one of its patches already there (a tip amended or rebased after its last push), or when the forge has a merged MR/PR whose head is exactly the local tip (a squash merge). If none can be shown — including when the forge cannot be reached — the worktree is kept. A tracked .envrc or .wt-owner does not count as local work. Nothing reaps a worktree after a forge merge, so run scripts/wt prune from the main checkout after merging; /mr reminds you, and /mass-merge removes each worktree as its MR lands.

wt list shows age and pushed-state per worktree so it is obvious at a glance whose worktree is whose.