PowerShell 7+ module that manages git worktrees together with VS Code/Cursor .code-workspace files.
If you run several AI coding agents in parallel — say five at once — you already know the pain. Each one wants its own branch, and juggling git worktree from the shell gets old fast. Sometimes an agent creates a worktree for you; you still need to open it, wire it into your editor, and find it again tomorrow. Tracking what lives where becomes a job in itself.
Underneath the clutter, the deeper pain is context switching: which checkout, which branch, which editor window. wtw reduces that friction by automating worktree lifecycle and .code-workspace wiring, and by giving each workspace a distinct color so you can orient at a glance instead of decoding paths.
Git worktrees are the right primitive, but using them raw is tedious. Creating one properly means: git worktree add, then craft a .code-workspace file, configure folder paths, set up terminal profiles, and remember the path. Removing one means reversing all of that. This should be one command, not six.
Then there's the visual problem. With five workspaces open, they all look identical — same title bar, same activity bar, same terminal tabs. You alt-tab and have no idea where you landed. wtw auto-assigns a unique Peacock color from a 20-color palette so your title bar, activity bar, and status bar instantly tell you which branch you're in.
That's what wtw does: one command, everything wired.
wtw create auth— git worktree, workspace file, unique color, shell aliases, and optional AI-tool project registration. Ready.wtw auth— switch to it.wtw remove auth— clean up worktree, workspace, branch, color. Gone.
No manual bookkeeping. No stale directories. No identical-looking windows.
- PowerShell 7+ — required
- Git — required
- Peacock extension — recommended for workspace colors in VS Code, Cursor, Windsurf, etc.
wtw installwill detect your editors and offer to install it. - iTerm2 (macOS) or Windows Terminal (Windows) — recommended for colored terminal tabs. wtw sets tab color automatically via escape sequences when switching worktrees. Other terminals get the window title but may not support tab colors.
Checks for git and PowerShell 7+, installs them if missing, clones wtw, and runs wtw install.
macOS / Linux:
curl -fsSL https://raw.githubusercontent.com/serrnovik/wtw/main/install.sh | bashWindows (from PowerShell):
irm https://raw.githubusercontent.com/serrnovik/wtw/main/install.ps1 | iexIf you already have PowerShell 7+:
Install-Module -Name wtw -Scope CurrentUser
Import-Module wtw
wtw installgit clone https://github.com/serrnovik/wtw.git
cd wtw
Import-Module ./wtw.psm1 -Force
wtw install- Copies the module to
~/.wtw/module/ - Sets up your shell profile (PowerShell, zsh, and/or bash)
- Detects installed editors (VS Code, Cursor, Windsurf, VSCodium, Antigravity)
- Offers to install the Peacock extension
- Checks that git is available
Re-run wtw install from source after pulling updates.
They are different commands, and which one you want depends on where your copy of wtw came from.
| You are running | Update with | What happens |
|---|---|---|
| A source checkout | git pull, then wtw install |
Your build is copied to ~/.wtw/module/ |
A hand-installed copy (wtw install from a checkout) |
wtw update |
Replaces it with the published Gallery release |
| The Gallery release | wtw update |
Refreshes it in place |
wtw install records where a copy came from in
~/.wtw/module/INSTALLATION.json, and wtw update reads it, so wtw always
offers the command that updates the copy your shell actually loads.
wtw update materialises the Gallery package into ~/.wtw/module/ rather than
leaving it on PSModulePath. Every loader wtw installs — the PowerShell profile
snippet, the zsh and bash wrappers, the Windows cmd shim — imports
~/.wtw/module/wtw.psm1 by explicit path, so an Install-Module wtw that lands
elsewhere updates a copy none of them ever load. If such a copy already exists,
wtw update says so and offers to remove it, because a bare Import-Module wtw
resolves to that one instead.
wtw --version # running module version and where it was loaded from
wtw -v # same
wtw version # same
wtw update --check # report versions and where they came from, change nothing
wtw update # prompt, then replace the install with the latest release
wtw update --yes # no prompt
wtw update --force # reinstall even when the versions already match
A local build that is newer than the published release is normal while you are working on wtw, and is reported as current. Same version, no warning either.
wtw tells you when a newer version is published. It prints a two-line hint once per shell session:
wtw 0.2.0 is available (you have 0.1.46).
Update: wtw update
The second line follows the install: a checkout is told to git pull, then wtw install, anything else is told to run wtw update.
The hint reads only ~/.wtw/update-check.json, so it never waits on the
network; a stale cache is refreshed by a detached background process and used
by the next command. Being offline, having no cache yet, or a read-only HOME
are all silent. The hint is skipped when stdout is redirected — which covers CI
and the subcommands whose output the shell wrappers parse — and when
WTW_NO_UPDATE_NOTICE=1 is set. Get-WtwUpdateStatus reports the same
comparison on demand.
PowerShell keeps the functions from Import-Module in memory. After wtw update, wtw install, or an overwrite of ~/.wtw/module in another window,
this shell can still be running the previous copy — (Get-Module wtw).Version
is often 0.0 because the profile imports wtw.psm1 by path, so that is not a
reliable signal.
On the next ordinary wtw command, wtw compares the manifest version it
snapshotted at import with the files next to that module. If the on-disk copy
is newer, it Import-Module -Forces that path and re-runs the command you
typed. A session that imported a checkout older than ~/.wtw/module is not
switched automatically (that would surprise local wtw development); it prints
wtw reload once instead.
wtw reload # re-import the copy this session should be running
wtw reload --check # loaded vs this copy vs ~/.wtw/module, change nothing
WTW_NO_SESSION_RELOAD=1 skips the automatic check. WTW_USE_REPO_MODULE=1
keeps a checkout loaded (same as after wtw go session scripts).
cd ~/projects/my-app
wtw init "app,my-app" --emoji 🎸
cd ~/projects/api-service
wtw init "api,api-service" --template ./workspace.code-workspace.templateThis generates a .code-workspace file for each repo and registers it in the global registry.
cd ~/projects/my-app
wtw create authThis creates:
- Git worktree at
~/projects/my-app_auth/(sibling to main repo) - Branch
auth - Workspace file
my-app_auth.code-workspacefrom your template - Unique Peacock color
- ChatGPT Desktop project metadata, if ChatGPT (or legacy Codex) is installed
wtw auth # cd to worktree + run session script
wtw cursor auth # open in Cursor (or: wtw cur auth)
wtw code auth # open in VS Code (or: wtw co auth)
wtw chatgpt auth # open in ChatGPT Desktop (aliases: cgpt, codex)
wtw droid auth # open in Factory desktop app (alias: factory)
wtw chatgpt auth --skip-restart # open without closing ChatGPT if the label needs repair
wtw claudecode auth # new Claude Code chat rooted at the worktree (or: wtw ccode auth)
wtw cmux auth # open the worktree as a cmux workspace (or: wtw cm auth)
wtw wmux auth # Windows: open the worktree as a wmux workspace (or: wtw wm auth)
wtw wave auth # from a Wave terminal: open the worktree in a Wave block (or: wtw wv auth)
wtw herdr auth # open the worktree as a Herdr workspace (or: wtw hd auth)
wtw workmux auth # open the worktree as a workmux tmux window (or: wtw wmx auth)wtw remove auth # removes worktree + workspace + branchwtw clean # ask: worktrees / linked / branches / all, then pick items
wtw clean --worktrees --dry-run
wtw clean --linked --dry-run # extra git worktrees (Fork / git worktree add), including tracked
wtw clean --branches --dry-run # leftover local branches already merged into main
wtw clean --all --force--worktrees now also lists extra git worktrees that git/Fork created but wtw
never registered. --linked (alias --extra) lists those plus wtw-tracked
extras so you can clean the full set Fork shows. The current checkout and the
repo primary tree are skipped. --all stays worktrees + branches; pass
--linked as well when you want tracked extras included.
--branches uses git branch --merged against the repo default branch and
skips any branch still checked out in a worktree (wtw remove or
wtw clean --linked those first). Delete is git branch -d only — unmerged
branches are never force-deleted.
| Command | Description |
|---|---|
wtw init [aliases] [--template X] [--startup-script X] [--startup-script-zsh X] [--startup-script-bash X] [--emoji X] [--sourcegit-folder] |
Register current repo with aliases, template, per-shell session scripts, optional SourceGit / list / cmux / wmux / T3 prefix, and optional SourceGit group folder |
| `wtw add [path | task] [--repo X --task X] [--branch X] [--name X] [--alias a,b] [--emoji X] [--color X] [--sourcegit-folder]` |
wtw create <task> [--branch X] [--open] [--no-branch] [--alias a,b] [--emoji X] |
Create worktree + workspace + branch |
wtw list [-f|--filter text] [-d|--detailed] [--wide] [-a|--all] [--prune] [--repo alias] |
List repos/worktrees: default compact table (-f kul = substring; --wide = full aliases and paths; --all adds untracked git worktrees; --prune drops entries git removed) |
wtw --version / -v / version |
Print the running module version and where it was loaded from |
wtw <name> |
Switch to repo/worktree — implicit go (cd + session init) |
wtw go <name> |
Same as above, explicit |
wtw open [name] [--editor X] |
Open workspace in editor (defaults to current repo/worktree) |
wtw cursor [name] |
Open in Cursor (aliases: cur) |
wtw code [name] |
Open in VS Code (aliases: co) |
wtw antigravity [name] |
Open in Antigravity (aliases: anti, ag) |
wtw windsurf [name] |
Open in Windsurf (aliases: wind, ws) |
wtw codium [name] |
Open in VSCodium (aliases: vscodium) |
wtw chatgpt [name] [--skip-restart] |
Open in ChatGPT Desktop (aliases: cgpt, codex) |
wtw droid [name] |
Open in Factory desktop app (alias: factory) |
wtw claude [name] |
Bring the Claude desktop app forward (aliases: cowork) |
wtw claudecode [name] [--prompt X] |
Start a new Claude Code chat rooted at the worktree (aliases: ccode) |
wtw cmux [name] |
Open the worktree as a cmux workspace (aliases: cm). With --on / --at, opens a local cmux whose terminal SSHs to that host |
wtw wmux [name] |
Open the worktree as a wmux workspace on Windows (aliases: wm) |
wtw wave [name] |
Open the worktree as a Wave Terminal block, from inside Wave (aliases: wv) |
wtw herdr [name] |
Open the worktree as a Herdr workspace, starting Herdr in Ghostty if needed (aliases: hd) |
wtw workmux [name] |
Open the worktree as a workmux tmux window, starting tmux in Ghostty if needed (aliases: wmx) |
wtw sourcegit [name] |
Open in SourceGit (aliases: sgit, sg) |
wtw remove <task> [--force] |
Remove worktree + workspace + branch |
wtw edit [name] [--name X] [--task X] [--alias a,b] [--key X] [--emoji X] [--sourcegit-folder] |
Edit a registry record (aliases: rename, ren). --emoji on a repo is the SourceGit / list prefix; on a worktree it overrides the derived identity glyph. --sourcegit-folder is repo-only |
wtw workspace <name> [--main] [--worktree-path X] |
Generate workspace file only (no git worktree) |
wtw copy <name> [--code-folder X] |
Standalone workspace copy from template |
wtw color [name] [hex|random] |
Set workspace color |
wtw sync --all [--dry-run] [--repo X] |
Re-apply template to all managed workspaces |
wtw clean [--worktrees] [--linked] [--branches] [--all] [--dry-run] [--force] |
Clean stale AI / unregistered / extra git worktrees and leftover merged local branches |
wtw install [--skip-profile] |
Install this checkout globally to ~/.wtw/module/ |
wtw update [--check] [--yes] [--force] |
Update the global install to the latest PowerShell Gallery release |
wtw reload [--check] |
Re-import the installed (or current) module in this session |
wtw skill [--agent claude|agents|all] |
Install AI skill into current repo for agent support |
wtw host [list|discover|add|remove|sync|trust|test] |
Manage remote machines for --on |
wtw --on <host> <editor> <name> |
Open a worktree that lives on another machine (see Remote worktrees) |
wtw --at <host> ... |
Alias of --on |
When agents work on more than one machine, the worktree you want to look at is
often not on the machine you are sitting at. --on opens it over Remote-SSH:
the editor window is local, the files and the extension host are on the remote.
wtw host add workstation --alias at --user dev `
--address workstation.local,192.168.1.10 `
--identity ~/.ssh/id_ed25519_workstation --platform windows
wtw host trust workstation # show host-key fingerprints, then accept
wtw host test at # addresses + ssh config + remote wtw
wtw list --on at # what does that machine have?
wtw --on at cursor auth # open its "auth" worktree here, in Cursor
wtw at cursor auth # same thing — bare host shorthand
wtw --on at go auth # ssh into that worktree: pwsh, right directory
wtw --on at cmux auth # same session, in a local cmux workspace
wtw --on at # open the machine as a cmux project (ssh home if no cmux)
wtw --at at cursor auth # `--at` is an alias of `--on`wtw go auth locally means "be in that worktree". With --on it means the same
thing over ssh: an interactive pwsh, already in the directory, with your remote
profile loaded. Aliases: connect, conn, ssh.
wtw --on at go auth # a worktree
wtw --on at go # just the machine, in its home directoryThe local tab is titled and tinted with that worktree's colour from the remote registry, and both are restored when you exit, including on Ctrl-C.
Give each machine an identity for those titles:
wtw host add workstation --emoji 🧊 --label WSTitles then read {emoji}{label}{separator}{worktree} — e.g. 🧊WS.🟢 PF037 gamification,
where the worktree half is its own pretty name from the remote registry, so a
remote tab looks like the local tab for the same worktree with the machine in
front. The label defaults to the shortest alias upper-cased, so this works before
you configure anything; there is no default emoji, because an auto-assigned one
would be noise rather than identity.
The separator defaults to . and is configurable — the worktree half usually
starts with its own emoji, and a dot immediately before one reads cramped:
wtw host add workstation --separator " " # 🧊WS 🟢 PF037 gamification
wtw host add workstation --separator "" # 🧊WS🟢 PF037 gamificationThree details that a plain ssh host does not give you:
-tforces a TTY; without it pwsh exits immediately.- The
Set-Locationtravels as-EncodedCommand, so paths with spaces or quotes survive the local shell, ssh's argv joining and cmd.exe on a Windows remote. - The title is set again from inside the session. A Windows remote runs pwsh under ConPTY, which clears the screen and emits its own OSC title the moment it starts — clobbering whatever the local terminal was told a moment earlier.
A worktree that is registered but no longer on disk is reported in-session rather than dumping you in the home directory behind a scrolled-off error.
wtw cmux auth locally means "open this worktree as a cmux workspace". With
--on the cmux window stays on this machine and opens two SSH tabs —
🌴 wtw and a normal remote pwsh — both running wtw --on <host> go [name].
The workspace is stamped with WTW_REMOTE_HOST / WTW_REMOTE_NAME, so the
Command Palette 🌴 wtw and pwsh actions (and the matching tab-bar buttons)
SSH into that same remote project instead of opening a local shell. Existing
remote tabs are matched by title or wtw-remote: description, never by the
local home directory they share as cwd. Omit the name to land in the remote
home directory.
Each host is also a persistent cmux project (wtw remote: workstation in the
Command Palette / sidebar), registered on wtw host add / sync / discover
and when you open a remote cmux tab. Picking that project starts the same
two-tab SSH layout for that machine. wtw --on at with no subcommand opens it.
wtw cmux (local or --on) puts the tab in a cmux sidebar group per
machine/project: 🍏SP/🎸 snowmain1 for every local snowmain1 worktree,
🧊AT/🎭 kulissa-landing for AT worktrees in that repo. The header is a
generated group workspace, not the first tab — closing a worktree does not
rename the group. Set this Mac/PC badge with wtw host self --emoji 🍏 --label SP
(or wtw self).
The built-in cmux + new terminal button still uses Ghostty's local default shell. Use 🌴 wtw or pwsh in a remote workspace when you want another SSH tab.
wtw --on at # open the machine project
wtw --on at cmux auth
wtw --at workstation cmux --via tailscale
wtw --on at cmuxwmux, T3, Claude Code, and ChatGPT stay refused under --on — those launchers
are ambiguous (open here vs register over there). Use wtw --on box run t3 auth
when the intent is the remote registration.
If your machines are on a tailnet, skip the manual address wrangling:
wtw host discover # shows a plan, asks before writing
wtw host discover --yes # non-interactiveIt reads tailscale status --json and registers every machine that can actually
host a worktree. Phones and tablets are recognised and skipped — they are real
tailnet members but cannot run pwsh. The platform is taken from Tailscale,
which is exactly what --platform needs for remote path translation.
An already-known machine is updated, not duplicated: its tailnet addresses are added in front of the ones you already had. Matching is by exact name or alias, never by prefix — merging into the wrong machine's entry is worse than a visible duplicate.
skip laptop macos online this machine
add nas linux online new host
+ nas.tailnet-example.ts.net, 100.64.0.11
skip iphone181 - online iOS cannot host a worktree
update workstation windows online add tailnet addresses
+ workstation.tailnet-example.ts.net, 100.64.0.10
Tailnet addresses go first in the candidate list on purpose: a tailnet name resolves on the LAN and away from it, so the ssh config stays valid when you change networks instead of needing a re-sync. LAN names stay behind it as a fallback for when Tailscale is down.
Tailscale supplies the machine, not the login — new hosts get your current
username unless you pass --user. Aliases are not invented for you.
Not every tailnet member is a dev box. --exclude a,b is remembered, so a NAS or
a router is declined once rather than on every run.
ssh names and Tailscale names do not clash — they compose. ~/.ssh/config
maps a pattern to a HostName; MagicDNS resolves that HostName. The collision to
watch for is within ssh config: wtw's Include is first, and OpenSSH takes the
first obtained value per keyword, so any other Host <name> block further down
keeps parsing but has its HostName/User/IdentityFile overridden by wtw's.
wtw host test <name> reports those blocks with file and line, since the
shadowing is otherwise invisible.
Supported as an ordinary address, not auto-discovered. The local ZeroTier client
exposes only your own managed address and peer node IDs — never member names
or their managed IPs — so there is nothing to enumerate without the ZeroTier
Central API and an account token. wtw host discover prints your networks and
your address on each, so you can read a peer's IP off ZeroTier Central and:
wtw host add winbox --user dev --address 10.147.20.42 --platform windowswtw host list is one line per host — active address, transport, ssh status.
wtw host show [name] is the full picture: every candidate with its transport
and whether it is up, which one is in use, and any other Host blocks that match.
There is no separate "edit": wtw host add on an existing name writes only the
fields you pass.
Each candidate is classified from the address itself — tailscale (*.ts.net
or 100.64.0.0/10), zerotier (a subnet this machine joined), mdns
(*.local), lan (RFC1918). The first two work anywhere; the last two only on
the same LAN. --via picks one, at either level:
wtw --on at list --via tailscale # one command only, nothing written
wtw at cursor auth --via lan # the editor opens over the LAN too
wtw host add workstation --via lan # stored default
wtw host add workstation --via any # back to first-reachableThe stored preference reorders the candidate list rather than filtering it —
pinning lan and then leaving the LAN falls back to the tailnet instead of
stranding the host, and host show says when that happened. The per-command form
is strict: asking for a transport the host does not have is an error.
Per-command --via works because host sync also emits every candidate address
as its own Host pattern — same User and IdentityFile, no HostName — so both
ssh and the editor's ssh-remote+<address> authority can target an address
directly and still use the right credentials.
--address is an ordered candidate list. Put a stable name first and a
last-known IP second: on DHCP — or when a machine moves between wifi and a USB
adapter — the address changes but the name does not. wtw host sync probes the
candidates and writes whichever answers on the ssh port, so a moved machine is
one command away rather than a config edit.
Both platforms advertise mDNS: macOS always, Windows 10/11 natively. Check the
name with scutil --get LocalHostName (macOS) or hostname (Windows).
Reaching a machine by a new name needs its host key accepted again — ssh
trusts keys per host pattern, and wtw connects with BatchMode=yes so it can
never hang on the prompt. wtw host trust <name> scans every candidate, prints
the fingerprints, marks any that already match a host you trust (same machine,
different name) and asks before writing to known_hosts.
wtw does not mirror the remote's registry. It asks the remote's own wtw
where the worktree currently is (wtw __resolve_json over ssh), because that is
the only thing that stays correct while agents create and remove worktrees over
there. Nothing is cached, so nothing goes stale.
Only reads, editor launches, and local-window SSH sessions. open, the VS Code
family editors, list, info, go, and cmux accept --on.
list --on delegates to the remote's own wtw list rather than reimplementing
it, so every flag behaves exactly as it does locally (--detailed, --wide,
--repo) and the colour swatches survive the trip.
Commands that change the remote's own registry — create, add, remove,
sync, color, clean, init, copy — are executed on the remote. Running
them there is exactly what keeps its registry authoritative:
wtw --on box sync --all
wtw --on box create auth
wtw --on box run init "app,my-app" --cwd /srv/repos/my-apprun is the explicit escape hatch: it forwards any wtw command verbatim.
--cwd sets the remote working directory, which repo-scoped commands need — an
ssh command starts in the remote home directory, not in a repo.
One category is refused outright: t3 / wmux / claudecode / chatgpt /
ss. These are ambiguous rather than impossible — "open T3 here pointing at a
remote path" cannot work, while "register that project over there" is perfectly
meaningful — so the intent has to be stated: wtw --on box run t3 auth.
cmux is the exception among multiplexers: the window is local and the
terminal is wtw --on <host> go.
- ssh config is mandatory. The Remote-SSH extension resolves
ssh-remote+<host>through the ssh client, not through wtw.wtw host addandwtw host syncwrite~/.ssh/config.d/wtwand prepend anIncludeto~/.ssh/config. The Include goes first because OpenSSH takes the first obtained value per keyword — appended after aHostblock it would apply only to that block. - Each fork needs its own Remote-SSH extension: VS Code
ms-vscode-remote.remote-ssh, Cursoranysphere.remote-ssh, VSCodiumjeanp413.open-remote-ssh(Microsoft's is proprietary and absent from Open VSX). wtw probes what is installed and offers to add one. - Windows remotes need
remote.SSH.remotePlatformpinned or the connection fails with an opaque error. wtw writes it into the editor'ssettings.jsonfor--platform windowshosts, backing the file up first (comments do not survive a JSON rewrite). - The remote needs pwsh, found without a login shell.
ssh host <command>runs a non-login, non-interactive shell, so a PATH exported from~/.zprofileor~/.bashrcdoes not apply — on an Apple-Silicon Mac that hides/opt/homebrew/bin/pwshcompletely. wtw probes the usual install locations;wtw host add <name> --pwsh <path>covers anything unusual. - Both ends want 0.2.0+. Discovery asks the remote for
__resolve_json, which older versions do not have. An older remote still works — wtw falls back to the legacy__resolveand prints "Remote wtw predates --on" — but you get a folder open instead of the.code-workspace, so no Peacock colour or multi-root layout. - Nothing depends on the remote's
wtwcommand. The payload is apwsh -EncodedCommandthat imports the module and callsInvoke-Wtwdirectly. ssh hands its command string to the remote's default shell — cmd.exe on Windows, wherewtwis the cmd shim that treats unknown subcommands asgotargets. Base64 also sidesteps every layer of quote reinterpretation between the local shell, ssh, cmd.exe and pwsh.
| Flag | Meaning |
|---|---|
--print-only |
Print the resolved editor command and exit — no ssh launch |
--folder |
Open the directory even when a .code-workspace exists remotely |
--skip-checks |
Skip the ssh-config / extension / settings preflight |
wtw host test <name> probes both halves (ssh resolution and remote wtw
reachability) when something is not working.
To import a worktree created by another tool (codex, cursor, manually) into the registry:
# From inside the worktree (auto-detects parent repo):
cd /path/to/worktree
wtw add
# → Detected parent repo: my-app
# → prompts for task name
# Or from anywhere, with explicit params:
wtw add /path/to/worktree --repo my-app --task my-featureAfter importing, the worktree appears in wtw list and you can use wtw go my-feature.
To keep a short name without renaming the folder or task (typical after adopting a T3/agent worktree):
wtw add ~/.t3/worktrees/snowmain1/t3code-ad4f13f1 --alias "onboarding video"
wtw edit t3code-ad4f13f1 --alias "onboarding video"
wtw go onboarding # or: wtw go onboarding videoSpaces and hyphens are equivalent. --alias none clears the extra names. Derived alias-task names (sn1-t3code-ad4f13f1) stay.
Agents and other tools (T3 Code, Codex, Cursor, git worktree add) keep creating
worktrees of repos wtw already tracks. You do not have to wtw add each one:
wtw list --all(-a) shows them with Kinduntracked(folder name as task, git branch);-ffilters them like any worktree. Plainwtw listskips them, because scanning every repo is the slow part.wtw go <name>,wtw open <name>and the editor shortcuts look at them after every registered name has missed (before fuzzy matching). The name matches the folder, the task, or the branch (exact, then prefix, then substring). One hit is adopted withwtw add(color, workspace file, editor registrations) and then opened. Several hits are reported as ambiguous.- A worktree git has removed (its folder is gone and git no longer lists it, or
lists it as prunable) is reported after the
wtw listtable. Interactive runs ask before dropping it from the registry, colors, and its workspace file;wtw list --prunedrops them without asking. A folder git still lists as live (an unmounted drive, say) is kept.
Only commands that act on a target adopt. Listing, terminal titles and cmux metadata never register anything.
All commands that accept a target name (go, open, remove, editor shortcuts, and the implicit go) share the same resolution logic via Resolve-WtwTarget:
- Exact repo alias —
appgoes to main repo - alias-task format —
app-authresolves to repoapp+ worktreeauth - Bare task name —
authsearches all repos (works if unambiguous) - Worktree alias —
onboarding video/onboarding-video(spaces and hyphens match) - Task prefix / substring —
au→auth,content→my-content-engine - Worktree alias prefix / substring
- Pretty name, then branch — lower priority so a task or alias always wins.
wtw go onboardingcan land ont3code/pronouncefit-…-onboarding-videowith no rename - Untracked git worktree — a worktree of a registered repo that wtw does not track yet (folder, task, or branch).
go/open/ editor shortcuts adopt it withwtw add - Fuzzy match (Levenshtein) on task / alias names
Unquoted leftovers are joined for go / open / info / editor shortcuts: wtw go onboarding video is the same as wtw go "onboarding video".
Multiple aliases per repo: wtw init "app,my-app" registers both.
After wtw create auth, all these work:
wtw auth # implicit go (task name)
wtw app-auth # alias-task format
wtw go auth # explicit go
wtw remove app-auth # remove using alias-task format
wtw cursor app-auth # open in Cursor
app-auth # shell alias (after terminal restart)Templates define the shared workspace structure and use {{WTW_*}} placeholders:
{
"folders": [
{ "name": "{{WTW_WORKSPACE_NAME}}", "path": "{{WTW_CODE_FOLDER}}" },
{ "path": "../shared-tools" }
],
"settings": {
"terminal.integrated.cwd": "${workspaceFolder:{{WTW_WORKSPACE_NAME}}}",
...
}
}| Placeholder | Replaced with |
|---|---|
{{WTW_WORKSPACE_NAME}} |
Workspace name (e.g., my-app_auth) |
{{WTW_CODE_FOLDER}} |
Absolute path to the worktree |
{{WTW_ENV_XYZ}} |
Value of the XYZ environment variable on the host machine. If XYZ is not set, the entire folder block containing this placeholder is safely skipped and omitted from the generated workspace file. |
Colors (workbench.colorCustomizations, wtw.color) are not in the template — wtw injects them automatically from the color palette.
wtw writes wtw.color rather than peacock.color on purpose. The Peacock extension re-applies its own palette whenever a peacock.* setting changes — which is what rewriting a workspace file under a live editor does — and in Cursor it hard-codes the title bar and command center foreground to #595959 whatever the background is. wtw already writes the full palette with WCAG-checked foregrounds, so it simply does not name the setting that wakes Peacock up. Workspaces that still carry peacock.color are read as a fallback.
wtw init chooses the workspace template in this order:
--template <alias|path>when provided.- An existing
<repo>.code-workspacein the configured workspaces directory. - A repo-shipped template under
configs/workspace-templates/when one can be resolved by repo name, repo name without trailing digits, or as the only template in that directory. - The bundled minimal template from
devops/worktree-workspace/templates/minimal.code-workspace.template.
The minimal fallback contains only the active code folder and empty settings;
wtw still injects Peacock colors and wtw.* metadata, so fresh installs get
usable colored workspaces even when a repo has no custom template.
Multiple repos can share a template for consistent terminal profiles, extra folders, and editor settings:
# Two repos share the same workspace structure:
wtw init "app,my-app" --template ./templates/shared.code-workspace.template
wtw init "api,api-service" --template ./templates/shared.code-workspace.template
# A third repo has a different structure:
wtw init "dash,dashboard" --template ./templates/dashboard.code-workspace.templateWhen you update a template (add a folder, change a setting), re-apply it:
wtw sync --all --dry-run # preview what would change
wtw sync --all # apply to all repos + worktrees
wtw sync --all --repo app # apply to one repo onlyIf --template points to a real .code-workspace file (no {{WTW_*}} placeholders), wtw falls back to regex replacement of folder paths and ${workspaceFolder:X} references.
wtw list -f kul (alias --filter) keeps repos whose name or alias contains
kul — and every worktree under those repos — plus any worktree whose task,
alias, or pretty name matches. wtw list kulissa-landing still requires an
exact repo name or alias; -f is the substring form.
wtw list shows registered repos and worktrees in a compact table so terminal width stays usable:
- Task: worktree slug (
-on the main repo row ). - Aliases: in wide mode (and detailed view), each alias is on its own line inside the cell. In compact mode, worktrees show one
alias-taskline; the main repo shows up to two alias lines plus a(+N)line when more aliases exist. - Branch: truncated with an ellipsis when very long.
- Path:
$HOMEshortened to~, with a middle ellipsis when still long.
Use wtw list --wide for the previous density on paths and branches, with every alias permutation listed (still one alias per line in the Aliases column). The Color column uses ANSI true-color swatches; for color hex cells, the swatch appears only on the first sub-line of a row.
Example (values abbreviated; alias cells may span several screen lines):
Kind Repo Task Aliases Branch Color Path Workspace
---- ------ ----- ---------- ----------- ------- ---------------- ------------------
repo my-app - app main (swatch) ~/projects/... my-app.code-workspace
my-app
(+1)
wt my-app auth app-auth auth-branch (swatch) ~/projects/... my-app_auth.code-workspace
- Kind:
repo= main repo,wt= worktree,untracked= git worktree wtw does not track yet (shown with--all) - Aliases (compact): primary match target; use
--widefor the full cross-product list - Color: colored swatch with contrasting text
wtw list --detailed (or wtw list -d) and wtw info show a card-style layout with:
- Repo names rendered as full-width colored badges (emoji is a separate field, not merged into the name)
- Worktree entries with color dot indicators
- Clickable
file://hyperlinks on paths (in terminals that support OSC 8) - A Settings section at the bottom with clickable links to all config files
The compact table still composes glyphs (🎸 snowmain1, 🐕 ntb_live_dogfood_fixes).
╔══════════════════════════════════════════╗
║ wtw — Worktree & Workspace Registry ║
╚══════════════════════════════════════════╝
snowmain1 #aaaaaa main
Emoji : 🎸
Aliases : sn1
snowmain1
Path : /home/user/projects/snowmain1
Workspace : snowmain1.code-workspace
Agent : solo
██ #dd2cdd fix/ntb-live-dogfood
Name : 🟢 ntb_live_dogfood_fixes
Task : ntb_live_dogfood_fixes
Emoji : 🐕
Aliases : sn1-ntb_live_dogfood_fixes
Path : /home/user/projects/snowmain1_ntb_live_dogfood_fixes
Workspace : 🎸🐕 ntb_live_dogfood_fixes.code-workspace
Created : 2026-08-31
─── Settings ───
Registry : ~/.wtw/registry.json
Colors : ~/.wtw/colors.json
Config : ~/.wtw/config.json
All config lives in ~/.wtw/:
| File | Purpose |
|---|---|
config.json |
Editor preference, workspaces dir, stale paths to scan, remote hosts |
registry.json |
Registered repos, aliases, template paths, worktrees |
colors.json |
20-color palette + per-worktree color assignments |
module/ |
Globally installed module copy |
editor accepts a single name or an ordered chain:
The array is a preference order, not "open in all of them". It exists so one config file survives machines with different editors installed — which is the normal case once you open worktrees from more than one box.
When an optional agentctl-compatible helper is installed on PATH,
wtw create attaches a local AI-agent overlay inside the new worktree:
.agent.local/personal.md
.agent.local/PLAN.md
CLAUDE.local.md
This integration is best-effort and skipped when agentctl is not installed.
WTW does not vendor agentctl or profile content. For open-source use,
agentctl is just a local executable hook with this tiny contract:
agentctl repo attach --profile <profile>The helper decides what profiles mean and what ignored local files to create.
Profile resolution is intentionally conservative:
registry.jsonrepo entryagentctlProfileconfig.jsonagentctl.repoProfiles.<repoName>config.jsonagentctl.defaultProfile- fallback:
team
Example ~/.wtw/config.json fragment:
{
"agentctl": {
"enabled": true,
"defaultProfile": "team",
"repoProfiles": {
"snowmain1": "solo",
"sample1": "team"
}
}
}Set these values through WTW:
wtw agent profile set snowmain1 solo
wtw agent profile set e1 team
wtw agent profile default team
wtw agent profile listRepo aliases are accepted and stored under the canonical WTW repo name.
Set "enabled": false to disable the integration globally.
Worktrees are created as siblings to the main repo, named {registryKey}_{task}:
~/projects/
├── my-app/ # main repo
├── my-app_auth/ # worktree: wtw create auth
├── my-app_billing/ # worktree: wtw create billing
├── api-service/ # another repo
├── api-service_hotfix/ # worktree: wtw create hotfix (from api)
Generated in the configured workspacesDir (default: ~/code-workspaces/):
~/code-workspaces/
├── my-app.code-workspace # main repo (generated by wtw init)
├── my-app_auth.code-workspace # generated by wtw create auth
├── my-app_billing.code-workspace # generated by wtw create billing
Each generated workspace:
- Points to the worktree as the code folder
- Keeps extra folders from template
- Gets a unique Peacock color from the 20-color palette
- Has terminal profiles pointing to the correct
${workspaceFolder:X} - Stores
wtw.*metadata in settings for sync support
When optional tools are installed, wtw create registers the new worktree with
them and wtw remove cleans that registration up:
- ChatGPT Desktop (formerly Codex) — trusts the worktree path in
~/.codex/config.toml, keeps a minimal.codex/config.tomlin the worktree when one is missing, and pre-seeds the saved sidebar label in~/.codex/.codex-global-state.jsonfrom the same--name/pretty name and color-circle prefix used by Superset/SourceGit when ChatGPT is not running. When ChatGPT is already running,wtw chatgpt <name>prompts to close/relaunch it around the label write so the app does not overwrite the external state edit. It skips that prompt when the saved label already matches; pass--skip-restartto open without repairing a missing/stale label. - Claude Code (Claude for Desktop) —
wtw claudecode <name>(aliaswtw ccode <name>) starts a new Claude Code chat rooted at the worktree via the app'sclaude://code/new?folder=<path>deep link, launching Claude first if it is not running.wtw createpre-accepts the "Trust this workspace?" prompt for the new worktree by settinghasTrustDialogAcceptedunderprojects.<path>in~/.claude.json(honours$env:CLAUDE_CONFIG_DIR), andwtw removedrops that project entry again. Both are no-ops when the file does not exist. Note: pre-trusting means Claude Code may read, write, and execute in the worktree without asking first — the same posture wtw already takes for Codex'strust_level = "trusted". There is no wtw-settable session label: the desktop app titles sessions itself (auto-generated from the first message, renameable in the UI) and identifies them by folder.--prompt <text>pre-fills the new chat's composer without submitting it, which is the closest equivalent to the sidebar labels wtw writes for ChatGPT/Cursor. Plainwtw claude <name>just brings the app forward. - Cursor — registers the generated
.code-workspacein Cursor's recent workspace list (state.vscdb) when Cursor andsqlite3are present.wtw cursor <name>refreshes that entry before opening it. The~/.local/bin/cursorstub fromcursor agentis skipped so it cannot recurse back into wtw. Cursor does not expose a separate stable label field in this state; it displays the workspace from the generated workspace filename/folder name. Colors come from the workspace'sworkbench.colorCustomizationsandwtw.colorsettings. - Superset — creates/removes a local workspace named from the same pretty name when the Superset CLI is installed and the repo project can be matched.
- SourceGit — adds/removes the worktree from managed repositories when
SourceGit is installed. If a group folder already exists for the repo
(
wtw-folder:<repo>or a folder that already holds the main checkout), new worktrees land under it.--sourcegit-folderoninit/add/editcreates that folder and moves the main checkout in;--no-sourcegit-folderonly skips creating one — an existing folder still collects new worktrees. Repo-level--emojiprefixes the main checkout (and any SourceGit group named after the registry key), e.g.🎸 snowmain1. The same prefix is reused for cmux / wmux / T3 / ChatGPT main-checkout titles andwtw list. Worktrees get a deterministic identity emoji from the task name (override with--emoji); titles compose as🎸🦔 authwith no space between the two glyphs, using the form name (not an alias). Derived slugs such asNTB-real-dogfooddisplay asNTB real dogfood; a custom--nameis left as typed. The main checkout stays repo-only. Before every write, wtw copiespreference.jsoninto~/.wtw/backups/sourcegit/(last 3 copies plus one snapshot each at 3 / 7 / 30 days). The same rotating backup is used for cmux, Codex, Claude, Cursor, and T3 configs wtw edits. - cmux —
wtw cmux <name>(aliaswtw cm <name>) selects an existing live cmux workspace for the worktree's path, or creates one viacmux new-workspace --name <pretty> --cwd <path>. Main-repo titles include the repo--emojiwhen set; worktrees compose repo + worktree glyphs.wtw --on <host> cmux [name]opens a local cmux workspace whose terminal iswtw --on <host> go [name]. Both local and remote opens place the tab in a sidebar group named<machine-badge>/<repo>(for example🍏SP/🎸 snowmain1).wtw host self --emoji --labelsets this machine's badge.wtw createregisters a Command-Palette workspace entry in~/.config/cmux/cmux.json, andwtw removecleans that entry up. Inside cmux terminals, thewtw.bash/wtw.zshshell init stamps the worktree's pretty name, color, and awtw:<repo>/<task>status pill onto the surrounding cmux workspace. All of this is a no-op when the cmux CLI is not installed. - wmux —
wtw wmux <name>(aliaswtw wm <name>) on Windows selects an existing wmux workspace for the worktree's path (cwd first, then title — so a title/emoji rename does not duplicate the tab), renames it to the composed pretty title, and selects it so the tab is visible. Otherwise it creates one viawmux new-workspace --title <pretty> --cwd <path> --shell pwsh(starting wmux if it isn't running — this is the only command that launches the app). Main-repo titles use the registry key plus the optional repo--emoji(🎸 snowmain); worktrees compose{repoEmoji}{worktreeEmoji} {form name}(🎸🐕 NTB real dogfood). Derived slugs get-/_turned into spaces unless you passed a custom--name. Packaged Electron rejects jax/pnpmNODE_OPTIONS; wtw clears that for CLI and GUI launch and strips the leftover log line so JSON parsing and$LASTEXITCODEstay clean. Native Windows pwsh also strips Windows TerminalWSLENVfor the CLI — wmux.js otherwise assumes WSL and failspingwhile the GUI is already running.wtw create/wtw addcreate the live workspace only when wmux is already open; otherwise they skip and printwtw wmux <name>.wtw removecloses it. wmux workspaces are live (daemon-backed) rather than a static config registry, so there is no on-disk equivalent ofcmux.json. wmux ships without an installer; wtw finds its Node CLI (<install>/resources/cli/wmux.js) via$env:WMUX_EXE, a running wmux process,wmux.exeon PATH, then common install dirs — set$env:WMUX_EXEto override. The CLI is run withnodewhen present, otherwise withwmux.exeandELECTRON_RUN_AS_NODE(same as wmux's own shims). All of this is a no-op when wmux is not found. - Wave Terminal —
wtw wave <name>(aliaswtw wv <name>) drives Wave through itswshCLI on Linux, macOS and Windows.wshonly reaches the app from a shell Wave spawned (it needs the injectedWAVETERM_JWT/WAVETERM_TABID), so run it from a Wave terminal.wshcannot create Wave workspaces or tabs, so the target becomes a terminal block in the current tab: a block already rooted at the path (cmd:cwd, thenframe:title) is focused, otherwisewsh term <path>opens one. The block gets the composed pretty title (frame:title) and the worktree color as its frame border. wtw findswshvia$env:WTW_WSH, PATH,$env:WAVETERM_WSHBINDIR, then Wave's data dirs (including the snap install).wtw create/wtw add/wtw removedo not touch Wave, since that would split the tab you ran them from. - Herdr —
wtw herdr <name>(aliaswtw hd <name>) drives Herdr, the agent multiplexer whose sidebar shows which agent is working, blocked or done. Its CLI reaches the running server from any shell. A workspace with a pane already rooted at the path (pane listcwd, then the label) is relabelled with the composed pretty title and focused; otherwiseherdr workspace create --cwd <path> --label <title>opens one. With no server running, wtw startsherdrin a new Ghostty window at the path. Herdr has no workspace color, so the color emoji in the title carries it. wtw findsherdrvia$env:WTW_HERDR, PATH,~/.local/bin, then~/.cargo/bin. - workmux —
wtw workmux <name>(aliaswtw wmx <name>) drives workmux, which pairs git worktrees with tmux windows and adds an agent status sidebar (workmux sidebar) and dashboard. workmux readsgit worktree list, so wtw runsworkmux open <dir-name>from the target: it switches to the existingwm-<dir>window or creates one. workmux needs a tmux server (not a client); with none running, wtw startstmux new-session -s wtwin a new Ghostty window first. workmux slugs window names (emoji become words), so the windows keep workmux'swm-<dir>names. wtw findsworkmuxvia$env:WTW_WORKMUX, PATH,~/.cargo/bin, then~/.local/bin. - agentctl — when installed, attaches ignored local AI-agent overlay files
to the new worktree using the configured profile. Defaults to
teamfor safety unless a repo/global override selects another profile.
Foreground colors for the colored chrome are chosen by WCAG contrast ratio, not by a brightness threshold. Both a softened near-black and a softened near-white are scored against the workspace color and the better one wins; if neither reaches 4.5:1 — which happens on mid-tone reds, teals and blues — it escalates to pure black or white for maximum contrast.
The active tab also gets tab.activeBorder in that foreground color, and keeps
its background when the editor loses focus. Fill alone was a weak cue for "which
file am I in", since the tab shares the title bar's hue.
Re-run wtw sync --all after upgrading to re-apply the improved colors to
existing workspaces.
20-color palette auto-assigned per worktree. Colors are recycled when worktrees are removed. Colors are applied to:
- Editor — VS Code/Cursor/Windsurf/VSCodium Peacock color customizations (title bar, activity bar, status bar) via the Peacock extension
- Terminal tabs — iTerm2 and Windows Terminal tab colors, set automatically via escape sequences when you
wtw gointo a worktree
When you wtw go <name>, wtw changes to the worktree directory, sets the terminal tab color and title, and then runs the startup script if one is configured.
wtw detects the interpreter from the file extension:
| Extension | In pwsh session | In zsh/bash session |
|---|---|---|
.ps1 |
Runs with pwsh | Runs with pwsh (subprocess) |
.zsh |
N/A | Sourced in zsh |
.sh, .bash |
N/A | Sourced in current shell |
You can set different scripts for different shells. The zsh/bash wrappers check for a per-shell override first, then fall back to the default:
# Default script (used by pwsh, and by zsh/bash if no override)
wtw init "app" --startup-script start-repository-session.ps1
# Zsh-specific session script (sourced when switching from zsh)
wtw init "app" --startup-script-zsh start-session.zsh
# Bash-specific session script
wtw init "app" --startup-script-bash start-session.sh
# All at once
wtw init "app" --startup-script start-repo.ps1 --startup-script-zsh start-session.zshThis is stored in the registry as:
{
"sessionScript": "start-repository-session.ps1",
"sessionScripts": {
"zsh": "start-session.zsh",
"bash": "start-session.sh"
}
}If no --startup-script is provided, wtw looks for these files in the repo root: start-repository-session.ps1, start-tools-session.ps1.
Worktrees inherit the startup script from their parent repo.
wtw install adds a loader to your PowerShell profile. On shell startup, Register-WtwProfile creates shortcut aliases from the registry:
app # same as: wtw go app
app-auth # same as: wtw go app-auth
my-app # same as: wtw go my-app
my-app-auth # same as: wtw go my-app-authNew worktrees get aliases after terminal restart. Within the current session, use wtw <name> directly.
wtw init rejects aliases already used by another repo:
wtw init "app,duplicate"
# Error: Alias 'app' is already used by repo 'my-app'. Choose different aliases.
Worktrees share hooks with the main repo (.git is a file in worktrees, not a directory). Session scripts detect this and skip hook installation for worktrees.
wtw is a PowerShell module, but you don't need to use pwsh as your daily shell. Thin wrappers for zsh and bash delegate to pwsh for all logic while handling cd and terminal colors natively.
wtw install detects your shell and offers to add the loader:
| Shell | Config file | Loader |
|---|---|---|
| PowerShell | $PROFILE |
Module import + Register-WtwProfile (automatic) |
| zsh | ~/.zshrc |
source ~/.wtw/shell/wtw.zsh |
| bash | ~/.bashrc |
source ~/.wtw/shell/wtw.bash |
| Command | zsh/bash | pwsh |
|---|---|---|
wtw go <name> / wtw <name> |
cd + terminal color (native) |
Name resolution (subprocess) |
Shell aliases (app-auth) |
cd + terminal color (native) |
Pre-generated at shell startup |
wtw list, wtw create, etc. |
Passthrough | Full logic |
The pwsh subprocess adds ~400ms latency to wtw go. Pre-generated shell aliases are instant.
CLI chrome (wtw clean, tables, status lines) follows the same theme knobs as chezmoi shell-theme / agent-shell. After theme.ps1 or theme.zsh has run, WTW uses those truecolor inks instead of the 16-color Write-Host -ForegroundColor palette (which washes out on a light cream background).
| Variable | Role |
|---|---|
WTW_THEME |
Force light or dark for WTW only |
AGENT_SHELL_THEME |
Set by shell-theme |
STARSHIP_THEME |
Starship light/dark |
TERM_BACKGROUND |
Common terminal hint |
If none of those are set, WTW falls back to a cheap console hint (Windows light sets RawUI ink to Black; COLORFGBG; light RawUI backgrounds). Set NO_COLOR=1 to print uncolored text.
| Terminal | Tab color | Window title | Platform |
|---|---|---|---|
| iTerm2 | Yes | Yes | macOS |
| Windows Terminal | Yes | Yes | Windows |
| Kitty | Yes | Yes | Linux/macOS |
| Konsole | Yes | Yes | Linux (KDE) |
| tmux | Pane border color | Pane title | All |
| WezTerm | Via user var | Yes | All |
| Terminal.app | No | Yes | macOS |
| GNOME Terminal | No | Yes | Linux |
| Alacritty | No | Yes | All |
| cmd.exe / PowerShell console | No | Yes | Windows |
Terminals without tab color support still get the window title set, which helps with orientation when alt-tabbing.
wtw's surface is lean: it manages worktrees, workspaces, colors, and shell aliases. It does not know about ports, namespaces, databases, or deployment — that's your build tool's job.
What wtw does is tag the session with environment variables that your build tool can read. If the variable is set, your deploy scripts can isolate. If not, they run shared. Zero config for the common case, opt-in isolation when you need it.
When you wtw go auth (or use a shell alias), wtw exports:
| Variable | Example (worktree auth, index 1) |
Example (main repo) |
|---|---|---|
WTW_WORKTREE_ID |
auth |
(empty) |
WTW_WORKTREE_INDEX |
1 |
(empty) |
DEV_WORKTREE_ID |
auth |
(empty) |
DEV_WORKTREE_INDEX |
1 |
0 |
DEV_WORKTREE_DASHED_POSTFIX |
-auth |
(empty) |
DEV_WORKTREE_PORT_OFFSET |
100 |
0 |
The WTW_* vars are wtw-specific. The DEV_WORKTREE_* vars are generic and tool-agnostic — any build system, task runner, or deploy script can use them without depending on wtw.
The key insight: DEV_WORKTREE_DASHED_POSTFIX is either -auth or empty string. So config that appends it works unchanged for both isolated and shared mode:
Namespace isolation (config file with env var expansion):
# Your build config — namespace includes worktree suffix when set, empty otherwise
kubernetes:
namespace: "myapp${DEV_WORKTREE_DASHED_POSTFIX}"
# → "myapp-auth" (worktree) or "myapp" (main)Port isolation (in a deploy script):
BASE_PORT=31632
PORT=$((BASE_PORT + ${DEV_WORKTREE_PORT_OFFSET:-0}))
# → 31732 (worktree index 1) or 31632 (main)# Same in PowerShell:
$port = 31632 + [int]($env:DEV_WORKTREE_PORT_OFFSET ?? '0')Docker Compose (in .env or directly):
services:
postgres:
ports:
- "${POSTGRES_PORT:-5432}:5432"# Set before docker compose up:
export POSTGRES_PORT=$((5432 + ${DEV_WORKTREE_PORT_OFFSET:-0}))The worktree index is determined by registry order (1-based, sequential per repo). Main repo is always index 0. The index is stable — it doesn't change when other worktrees are added or removed, only when the worktree itself is created.
Works on macOS, Windows, and Linux with PowerShell 7+. Uses Join-Path everywhere, du -sk for fast size scanning on Unix (falls back to Get-ChildItem on Windows). zsh and bash wrappers available for non-PowerShell daily drivers.