a pool of reusable git worktrees shared across sessions, so expensive build trees (especially WebKit's WebKitBuild/) are reused instead of rebuilt from scratch every time
each session owns a worktree through a lockfile that records its session identity
a released worktree is reset to a clean base but keeps its build output, so the next session that claims it only needs an incremental build
all commands live in ~/Developer/worktrees/.pool/bin
the wt entry point dispatches to the rest, so wt claim runs wt-claim and so on
put the bin directory on your PATH for the nicest experience:
export PATH="$HOME/Developer/worktrees/.pool/bin:$PATH"| command | purpose |
|---|---|
wt claim [REF] [--repo PATH] [--base REF] [--branch REF] [--note TEXT] [--prefer=MODE] [--seed-cache[=MODE]] [--create] [--timeout=SECONDS] [--progress=SECONDS] [--notify=MODE] [--notify-command=CMD] [--refresh] |
claim a worktree and print its path; reusable worktrees are scored by affinity/cache/recency, dead local terminal owners are immediately reclaimable, and unavoidable creation can opt into cache seeding |
wt release [--repo PATH] [--base REF] [--all] [--force] |
return this session's worktree to the pool, reset to a clean base but keeping the build cache (a worktree with uncommitted changes is kept unless --force); run with no arguments from inside a pooled worktree, it releases that worktree instead (like .) |
wt release NAME|REF... [--force] [--delete-branch] (or --worktree NAME / --branch REF) |
release one or more specific pooled worktrees, each named by its worktree name/path, its checked-out branch, or . for the one you are in (auto-detected), whichever session owns it, so you can reclaim a departed session's worktree by hand; --delete-branch also drops the branch ref |
wt build [NAME|REF] [--notify=MODE] [--notify-command=CMD] -- <cmd...> |
run a build under a machine wide lock so only one heavy build runs at a time; target another worktree by name/branch and optionally notify when the build finishes |
wt run [NAME|REF] -- <cmd...> |
run a command without the build lock; target selection matches wt build, and no target runs in the current directory |
wt exec [--repo PATH|--all] [--free] [--mine] [--dry-run] -- <cmd...> |
run a command sequentially across selected worktrees with aggregate status; reusable worktrees are temporarily claimed to avoid races |
wt open [NAME|REF] [--command=CMD] [--dry-run] |
open a selected worktree with the configured editor/browser command or platform default |
wt edit [NAME|REF] [--command=CMD] [--dry-run] |
open a selected worktree with an explicit/configured command or $EDITOR; target resolution matches wt open and wt path |
wt adopt PATH [--note TEXT] [--force] |
move an existing linked worktree into a managed pool name while preserving its branch, changes, and ignored caches |
wt status [--disk] [--mine] [--free] [--stale] [--dirty] [--sort=MODE] [--watch[=SECONDS]] [--color=MODE] [--hyperlinks=MODE] [--porcelain] [--repo PATH] |
show pool state using the live terminal width, with optional color and clickable worktree names; non-tty human output is unbounded and --porcelain remains plain tab-separated data |
wt config [list|get|set|unset] |
read and update validated persistent settings stored in .pool/config; environment variables still take precedence at runtime |
wt migrate [--dry-run] |
normalize any legacy worktree names to the current scheme (safe to run repeatedly) |
wt remove [--repo PATH] [--all] [--force] [--dry-run] |
delete free or stale pooled worktrees to reclaim disk (a live or dirty worktree is kept unless --force) |
wt remove --lru [--repo PATH|--all] [--max-free=N] [--max-disk=SIZE] [--dry-run] |
remove the oldest reusable worktrees only until the configured count and disk limits are met; options override WT_MAX_FREE and WT_MAX_DISK |
wt remove NAME|REF... [--force] [--dry-run] (or --worktree NAME / --branch REF) |
delete one or more specific pooled worktrees, each named by its worktree name/path, its checked-out branch, or . (auto-detected), whichever session owns it |
wt path [NAME|REF] (or --worktree NAME / --branch REF) |
print a pooled worktree's path with no side effects, by name/branch/., or (no argument) the worktree the current directory is in, else the one this session owns; handy as cd "$(wt path <branch>)" |
wt doctor [--fix] |
report inconsistencies (orphaned locks, leftover mutexes, dangling worktrees); --fix cleans up orphaned locks and mutexes and prunes git worktree metadata |
run wt <command> --help for the options of a command
run from inside a pooled worktree, a bare wt release or wt path (no argument) acts on that worktree, so you never have to name the one you are standing in
when a branch is checked out in worktrees for several repos, a bare NAME|REF or --branch REF resolves the ambiguity from the current directory: the worktree you are standing in, otherwise the one for the current repo
optional niceties once bin is on your PATH:
source ~/Developer/worktrees/.pool/bin/wt-completion.bashautoload -Uz compinit && compinit
source ~/Developer/worktrees/.pool/bin/wt-completion.zshsource ~/Developer/worktrees/.pool/bin/wt-completion.fisheach integration completes subcommands, flags, worktree names, and branches, and defines wtcd; for example, wtcd pr-68663-slotted enters that worktree in the current shell
WT="$(wt claim --repo ~/Developer/WebKit --branch dcrousso/my-task --note 'bug 262834')"
cd "$WT"
# ... make edits now ...
wt build -- Tools/Scripts/build-webkit --release
wt build -- Tools/Scripts/run-webkit-tests --release --no-build inspector
# leave the branch for review and return the worktree:
git switch --detach
wt release --repo ~/Developer/WebKitcall wt claim again at the start of each turn during a long session
it is re-entrant, so it returns the same worktree and refreshes the heartbeat; passing --note also replaces the existing note
a bare claim never creates a new worktree; pass a branch REF (or --create for a detached worktree) when allocating another worktree is intentional
wt claim my-branch --timeout=300 waits up to five minutes for a reusable worktree, then checks out my-branch; --timeout=0 is a single reuse-only attempt
--prefer=release, debug, warm, or recent adjusts reusable-worktree scoring; auto prefers branch/base affinity, warm caches, free slots, and recency in that order
while waiting, --progress=10 reports pool state every ten seconds, --notify=bell rings on acquisition, and --notify-command=CMD runs a user-configured command with WT_WORKTREE, WT_BRANCH, and WT_WAIT_SECONDS
wt config list, get, set, and unset manage pool-local settings in .pool/config
| key | default | environment override | purpose |
|---|---|---|---|
max-free |
unset | WT_MAX_FREE |
reusable worktree limit for lru cleanup |
max-disk |
unset | WT_MAX_DISK |
scoped disk limit for lru cleanup |
prune-on-release |
false |
WT_PRUNE_ON_RELEASE |
run best-effort global lru cleanup after release |
claim-prefer |
auto |
WT_CLAIM_PREFER |
default reusable-worktree scoring mode |
seed-cache |
none |
WT_SEED_CACHE |
new-worktree cache seeding mode (none, auto, clone, or copy) |
timeout-progress |
10 |
WT_TIMEOUT_PROGRESS |
seconds between timeout progress reports; 0 disables |
notify |
none |
WT_NOTIFY |
acquisition notification mode (none or bell) |
notify-command |
unset | WT_NOTIFY_COMMAND |
shell command run after a timed claim succeeds |
build-notify |
none |
WT_BUILD_NOTIFY |
build completion notification mode (none or bell) |
build-notify-command |
unset | WT_BUILD_NOTIFY_COMMAND |
shell command run after a build finishes |
open-command |
unset | WT_OPEN_COMMAND |
shell command used by wt open, with the path passed as $1 |
edit-command |
unset | WT_EDIT_COMMAND |
shell command used by wt edit, with the path passed as $1 |
status-sort |
name |
WT_STATUS_SORT |
default status ordering (name, repo, age, or size) |
color |
auto |
WT_COLOR |
human status color mode (auto, always, or never) |
hyperlinks |
auto |
WT_HYPERLINKS |
OSC 8 file hyperlink mode (auto, always, or never) |
the pool also reads these environment-only settings:
WT_POOL_ROOTsets the pool root (default~/Developer/worktrees)WT_STALE_HOURSsets the idle hours before a lock is reclaimable (default18)WT_BUILD_DIRSsets the paths thatwt statustreats as a warm build cache, colon separated and relative to a worktreeWT_SESSION_IDis set from any tool that has its own per session idWT_SESSION_ENVis the ordered list of explicit variables checked before automatic agent and terminal detection (defaultWT_SESSION_ID COPILOT_AGENT_SESSION_ID)
a session is the first explicit WT_SESSION_ENV value, then an automatic copilot/codex/claude identity, then TERM_SESSION_ID
when none is set, the pool falls back to the controlling terminal, and then to the parent process
so it works out of the box for a human at a terminal, GitHub Copilot, Codex, and Claude
after explicit variables, the pool searches all non-empty COPILOT_*, CODEX_*, and CLAUDE_* values; session/thread/conversation/task/run IDs outrank terminal identity, while generic prefixed environments are a final sorted-hash fallback so raw values never enter lockfiles
for any other agent, point the pool at that tool's own session value once:
export WT_SESSION_ID="<your tool's stable per session id>"or add the tool's variable to the search list:
export WT_SESSION_ENV="WT_SESSION_ID MY_AGENT_SESSION_ID TERM_SESSION_ID"each worktree has one lockfile at .pool/locks/<name>.lock, kept outside the worktree so it never appears in git status
a claim is atomic
the full lock body is written to a temp file and hardlinked into place with ln, which fails if the target already exists, so two sessions can never both win and a lock is never seen without its contents
reclaiming a free or stale lock is serialized by a short lived mutex, so two racing sessions can never both take over the same worktree
a claim first reuses a worktree this session already owns, then scores every free or stale worktree before trying the best candidate
branch and base matches outrank cache preferences, followed by the requested release/debug/warm policy, free-slot safety, and last-use recency
it creates a new worktree only when every reusable candidate is unavailable and creation was explicitly allowed with a branch REF, --branch REF, or --create; --timeout=SECONDS suppresses creation and retries once per second
the lock for a new index is acquired before git worktree add, so racing sessions cannot collide on the same index
--seed-cache=auto attempts only filesystem clone/reflink operations, while copy explicitly permits a full recursive copy of configured build directories from the newest warm worktree
releasing resets the worktree to a clean detached base with git clean -fd
there is no -x, so ignored build output such as WebKitBuild/ stays in place
the lockfile is removed and the worktree remains for the next claimant
wt remove --lru orders reusable worktrees by their last release or stale heartbeat and removes only enough oldest entries to satisfy --max-free and --max-disk
live, dirty, and current worktrees count toward disk usage but are never removed by lru cleanup
when prune-on-release is enabled, release runs the same lru policy after freeing worktrees; cleanup is best effort and never changes whether the release itself succeeded
state filters are alternatives, so wt status --free --stale shows both unlocked and reclaimable worktrees
age and size sorts place the largest value first, while name and repo sorts are ascending; wt status --watch reruns the human view until interrupted
human output uses COLUMNS when explicitly set, otherwise reads the controlling terminal with stty; when no live terminal width exists, values are not truncated
color highlights free, stale, dirty, current, and session-owned worktrees; worktree names use OSC 8 file:// links when enabled
auto enables tty features only on an interactive non-dumb terminal, NO_COLOR disables non-explicit color, and --porcelain never emits color or hyperlink escapes
each turn refreshes the lock heartbeat
a lock idle for longer than WT_STALE_HOURS (default 18) is treated as abandoned and may be reclaimed
free worktrees normally outrank stale ones, while explicit branch or cache preferences can select a better stale candidate
locks created by stable local terminal sessions also record host and parent PID; when that same-host process exits, the lock is reclaimable immediately, while agent or explicit session identities continue to rely only on heartbeats
wt build serializes builds machine wide through an fcntl lock, because flock(1) is not available on macOS
concurrent WebKit builds already saturate the CPU and share Xcode's compilation cache, so running several at once only thrashes CPU and IO
serializing keeps each one fast
build completion hooks release the global build lock before ringing a bell or running build-notify-command; hooks receive WT_BUILD_EXIT, WT_BUILD_SECONDS, WT_WORKTREE, and WT_BRANCH
wt exec runs sequentially and continues after failures; reusable worktrees are temporarily owned for the command and returned afterward
wt open passes the selected path as $1 to a trusted configured shell command, or uses open on macOS and xdg-open elsewhere
wt edit passes the selected path as $1 to edit-command, or launches $EDITOR with the path last; both receive WT_WORKTREE and WT_BRANCH
wt adopt rejects the main worktree and dirty worktrees by default, reserves a pool lock before moving, and preserves ignored build output
when --base is omitted, the base is detected from origin/HEAD, falling back to main, then master, then the current HEAD
as of Xcode 26, content addressable compilation caching is built into LLVM and enabled by WebKit (COMPILATION_CACHE_ENABLE_CACHING)
that cache lives at ~/Library/Developer/Xcode/DerivedData/CompilationCache.noindex (tens of GB) and is already shared across every worktree on the machine, which is why both incremental and fresh worktree builds get cache hits automatically
a separate ccache would be redundant, so it is intentionally omitted
- pooled worktrees are named
<repo>-<N>and are keyed by the repo basename, including dot-prefixed names such as.pool-1, so a WebKit worktree is never confused with a playwright one - task named worktrees such as
wk-262834are never touched by the pool - the commands target macOS system
bash(3.2) andpython3, so they also run on Linux without changes test/selftest.shruns the whole suite, and each file undertest/(claim.sh,release.sh,migrate.sh, and so on) also runs on its own- every test builds its own isolated pool against throwaway repos and never touches the real pool