Worktree — isolated dev checkout lifecycle
Create / list / remove isolated git worktrees under .claude/worktrees/.
Creation produces a worktree that's actually ready to run: fetched from
origin, branched from the right base, env files symlinked, dependencies
installed. This is a shared bootstrap you can call directly (spikes,
ad-hoc branches, parallel experiments) and that implement-style flows and
prune/rig-tidy steps can delegate to.
The logic lives in two scripts so the invariants live in one place:
<SCRIPTS>/setup-worktree.sh (create) and
<SCRIPTS>/remove-worktree.sh (teardown), where <SCRIPTS> is resolved
per install (see Configuration). Creation encodes
three invariants that bite if skipped — fetch-before-branch (stale
local refs silently start you on a pre-merge tip), env symlinks (missing
secrets read as flaky/timeout test failures, not "config not found"),
and a per-worktree dependency install (installed deps don't carry across
worktrees → Cannot find package '...'). Don't reimplement these
inline; call the scripts.
Configuration
Script location (<SCRIPTS>). Where the helper scripts live depends on
which target installed them, so resolve — don't assume — from the repo root
(git rev-parse --show-toplevel), first hit wins:
.claude/scripts/— theclaude-codetarget (on a dual-target install this is a symlink at.rig/scripts/, so either entry finds the same file)..rig/scripts/— theagents-mdtarget (Codex, Cursor, Gemini CLI, …), where there is no.claude/at all.
If neither holds setup-worktree.sh, say so and stop rather than
reimplementing the script inline — its invariants are the whole point.
Reads .rig/config.json (missing keys → defaults):
| Key | Default | Used for |
|---|---|---|
vcs.baseRef |
origin/main |
Default --base the worktree branches from. |
vcs.branchConvention |
{user}/{ticket}-{slug} |
Template when inferring a branch name from a ticket ID. |
vcs.defaultBranch |
main |
The trunk (informational; the main worktree). |
runtime.installCommand |
derived from packageManager |
Passed to the setup script as --install-cmd. |
runtime.packageManager |
npm |
Derives the install command when installCommand is unset (bun→bun install, pnpm→pnpm install, yarn→yarn, npm→npm install, none→skip install). |
tracker.ticketPrefix |
— | Recognizing a ticket ID (e.g. ABC-521) to slot into the branch template. |
If .rig/config.json is absent, run unconfigured: base origin/main,
install npm install, branch template {user}/{ticket}-{slug}, and say so.
Subcommands
$ARGUMENTS routes to one of:
- (default — a branch name) create/reuse a worktree. See Create below.
list— show all worktrees with their branch + PR state. Read-only.rm <branch|path>— safely tear a worktree down (skip if dirty).
Create — arguments
$ARGUMENTS is the branch name plus optional flags. The branch is the
only required argument.
<branch>— branch to create/check out, e.g.alice/feat-521-renameorfeat-679-observability. The naming convention for ticket work isvcs.branchConvention(default{user}/{ticket}-{slug}).--base <ref>— base to branch from. Defaultvcs.baseRef. For a stacked/integration child, passorigin/<integration-branch>.--path <path>— worktree location. Default.claude/worktrees/<last segment of branch>(soalice/feat-521-foo→.claude/worktrees/feat-521-foo). This is the directory the Claude Code harness manages, so a worktree created here can be adopted natively viaEnterWorktree/ExitWorktree. The default is unchanged on non-Claude agents — it's then just a path with no special meaning, so pass--pathif you'd rather not have a.claude/dir in the repo.--reuse— if the worktree/branch already exists, fetch and hard-reset it to<base>instead of failing. Use this for a "reuse a child worktree" path.--no-install— skip the dependency install (rarely wanted; only for a read-only checkout).--name <session-name>— background-job/session display name. Delegates toset-session-name.sh(ships with the kit). Claude-Code-only — it renames the FleetView background-job entry so the job list reads as the work (the ticket/epic) instead of the raw prompt; a clean no-op on any other agent.--skip-if-prefix <p>— passed through toset-session-name.sh; leave an existing name in place if it starts with<p>(so anEPIC:label set byrig-epicwins over a per-childFEAT:/CHORE:label).
Create — procedure
Read
.rig/config.jsonforvcs.baseRef,vcs.branchConvention, and the install command. Derive the install command fromruntime.installCommand, else fromruntime.packageManager.Infer the branch if not given. If
$ARGUMENTSis empty:- If the current branch matches the ticket-work convention, offer to reuse / recreate it. Otherwise ask the user for a branch name or ticket ID. Don't guess a branch out of thin air.
- If given a ticket ID (e.g.
<ticketPrefix>521) with no slug, build the branch fromvcs.branchConvention, substituting{user}(git user),{ticket}(the lowercased ID), and{slug}(a short kebab from the ticket title — fetch it if you have a tracker, else ask).
Run the script from the repo root, passing the resolved base and install command. Resolve
<SCRIPTS>first (see Configuration):ROOT="$(git rev-parse --show-toplevel)" for d in "$ROOT/.claude/scripts" "$ROOT/.rig/scripts"; do [ -x "$d/setup-worktree.sh" ] && SCRIPTS="$d" && break done [ -n "$SCRIPTS" ] || { echo "rig scripts not found under .claude/ or .rig/" >&2; exit 1; } "$SCRIPTS/setup-worktree.sh" \ <branch> [--base <baseRef>] [--install-cmd "<install cmd>"] [--reuse]The script prints progress to stderr and the absolute worktree path as the last line of stdout, so capture it:
WT=$("$SCRIPTS/setup-worktree.sh" \ alice/feat-521-rename --base origin/main --install-cmd "bun install" | tail -1)(The script also honors
WORKTREE_BASE_REFandWORKTREE_INSTALL_CMDenv vars, so you can export those from config instead of passing flags.)Report back the worktree path, the branch, and the base it was cut from. If the caller is a human running this directly, remind them they can
cd "$WT"to work there. If the script failed (e.g. path exists without--reuse), surface the error verbatim and suggest--reuse.
list
Show every worktree with the branch it has checked out and, where one exists, the state of its PR. Read-only — never removes anything.
git worktree list --porcelain
For each non-main worktree, resolve its branch and look up the PR state so the user can see what's safe to prune:
gh pr list --head <branch> --state all --json number,state,title -q '.[0] // empty'
Report a table: worktree path · branch · PR # · PR state (OPEN /
MERGED / CLOSED / none) · dirty? (git -C <path> status --porcelain).
End with a one-line hint, e.g. "2 worktrees on MERGED branches —
/rig-worktree rm <branch> to reclaim them."
rm <branch|path>
Safely tear a worktree down via the shared script. Dirty worktrees are
skipped, not removed (a skip is a normal outcome, not an error) —
pass --force-dirty to override. Resolve <SCRIPTS> as in Create step 3:
"$SCRIPTS/remove-worktree.sh" \
<branch|path> [--force-dirty] [--keep-branch]
The first stdout token is the outcome: removed <path>,
skipped <path> (dirty), or absent <arg> (no such worktree). The
script deletes the branch too (force -D, since a squash/rebase-merged
PR leaves the local branch unmerged); pass --keep-branch to retain it.
Report what was removed, skipped (with the dirty reason), or absent.
Notes
- The scripts resolve the main worktree (first entry of
git worktree list) as the source for env files / ignores and as the place to rungit worktreecommands, so they're safe to invoke from inside another worktree.remove-worktree.shrefuses to remove the main worktree. - Symlinks (not copies) — editing an env file in the main repo
propagates to every worktree.
ln -sfnis idempotent, so re-running create with--reusere-links cleanly.