<branch> in it. The slot is a path the pool reuses, so Bazel’s server, analysis cache and external/ tree are often already there.
git worktree add’s rules, so what you know about that command still holds; the comparison below covers its flags one by one.
Entering it in one step:
--output=path writes only the directory to stdout, which is what $(...) captures. The report below still goes to stderr, so it stays on the terminal, minus the ways to enter the slot, since that is what the one-liner is doing.
What it reports
git push -u makes the branch of the same name on the remote and tracks it; git branch --set-upstream-to=<branch> tracks an existing branch, and the one it started from is suggested when it is a branch, local or remote — not a tag or a commit, which git will not track. When that is a local branch with an upstream of its own, tracking that upstream is offered too, and one with no upstream is said to have none. An existing branch that tracks nothing gets the same two commands, with <branch> left for you to fill in.
The created … used line is the slot’s record rather than this worktree’s: how long it has existed, and how many worktrees it has held before yours. A brand-new slot says new slot: this is the first worktree it has held instead. It is the pool’s claim stated as a number — nine reuses is nine cold starts not paid for — and aspect worktree list --verbose breaks it down into which branch and which agent session had the slot each time.
Which slot you get
add scores each free slot your clone owns on how much of its Bazel state a build of the branch would reuse, best first:
Any usable free slot beats creating one, however it scores: a bad reuse pays the same
external/ rebuild a new slot would, and skips a new server and one more output base on disk. So the pool grows only when every slot is in use. A slot is unusable while it still holds uncommitted work or a checkout git cannot read, or when a score_slot hook returns 0 or less for it. A repo can replace the scoring with a score_slot hook on the Worktrees trait; the guide has an example and the fields a hook is given.
The warmth line is read from disk, not assumed:
Branches
<branch> is resolved as git worktree add <path> <branch> resolves it:
--create makes the branch, as git’s -b does, and refuses a name that already exists here or on a remote. Bare, it starts from the HEAD of the directory you run in; --create=<ref> starts from that ref. For fresh work, fetch and start from the remote’s main:
add says so. Cut from origin/main with git’s default tracking, it would record origin/main as its upstream, and under push.default=upstream a bare git push would land there. The first push names the branch instead:
upstream_elsewhere warning for the same reason.
Detached
--detach checks out any commit-ish — a tag, a SHA, origin/main — on no branch, for building or reading something without working on it. Git allows any number of worktrees at one commit, so this never conflicts with a branch checked out elsewhere.
aspect worktree path 1135398e6688, aspect worktree release 1135398e6688 — or by the ref it was taken at, while no other slot is at it. Listings show it as (detached at origin/main), as git words a detached HEAD. A commit only its HEAD reaches is refused by release rather than dropped.
Those commits are found in the slot’s HEAD reflog. It is read through git, so a reftable repository is covered; on an unborn branch, where git will not read it, the files backend’s logs/HEAD is read directly; and a reflog that exists but cannot be read counts as work, with the count unknown. Every slot is checked out with core.logAllRefUpdates=always so that the reflog exists whatever the repository sets. A commit that any ref outside the per-worktree namespaces (refs/worktree/, refs/bisect/, refs/rewritten/) reaches does not count, and neither does one another worktree’s HEAD is at. A commit git cannot count counts as work. The refusal names the fewest tips whose branches keep them all (git merge-base --independent), the first 20 in the message and every one in tips. In a list of work these are the DH lines.
Compared with git worktree add
add takes a branch where git takes a path, since the path is the pool’s to choose. The rest maps flag for flag, and where it differs, the difference is the point:
--take-over and the agent flags have no git counterpart: they are about the lease, which git does not have.
Who took it
The lease records the agent session that took the worktree, soaspect worktree list can name it later. Nothing needs configuring under a harness the CLI recognizes — Claude Code exports a session id, and otherwise the process tree is walked until a known harness turns up.
For anything else, set the vendor-neutral variables once in a wrapper:
Worktrees.abandoned_grace_hours, 24 by default — counts from the slot’s last activity: the lease being taken, Bazel using its output base, or a git operation in the checkout. Until then the refusal for its branch says how long it is kept, and release of it by anyone else takes --force=all. A resumed session — claude --resume, say — runs in a new process the lease does not record; when it runs add of a branch that is already leased, path for a slot it may enter, or inspect anywhere in the clone or its slots, every lease of that session, its subagents’ included, whose recorded process is no longer running moves to the caller’s live process, so the session reads as running and keeps its slots. Leases are matched by the session id the harness reported, never by an id a caller chose, so a session whose harness reports none has no leases moved. A lease with no session id, or whose process was never captured, is never moved. See agent detection. 0 takes such a slot back at once; a negative value is refused with invalid_config, so a stray minus cannot switch the protection off. A clean checkout is the condition that never bends: commits survive git worktree remove because they are in the branch, so a clean tree has nothing in it to lose. Files git ignores do not count: what a repository ignores is by its own account disposable. Clean does mean nothing git status leaves out: a populated submodule, whose repository lives inside the worktree; edits hidden by --assume-unchanged or --skip-worktree; refs only that worktree has; a merge, rebase, cherry-pick, revert or bisect in progress, a multi-commit cherry-pick or revert waiting between picks included; and a worktree git lists inside that one (NW), under an ignored .claude/worktrees/ say, which removing it would delete. A slot whose checkout is gone from its directory — moved with mv, deleted, or the directory emptied — keeps its lease and its git registration while git still keeps commits only its HEAD reflog reaches, refs only it has, an operation in progress or a staged index for it (VG): it is not taken back, and --take-over refuses it. Another clone’s leases are kept while that clone’s git says it holds work for them. Repository settings cannot narrow the check — status.showUntrackedFiles=no and submodule.<name>.ignore are overridden, and an exported GIT_DIR is ignored. When in doubt the slot is kept, and the refusal for its branch names what kept it.
add of the branch the stopped session held. When another branch’s add reclaims the slot instead, the line reads INFO: reclaimed the slot held by fix/login: its claude-code session … is not running and the checkout was clean. Either way the JSON carries a lease_reclaimed warning naming the previous holder, so a caller reading it knows the lease it now holds was somebody else’s.
A warm slot already shaped for the work beats a cold new one, so this is preferred to expanding the pool — an agent restarting the task it died in lands back on the analysis cache built for it.
If that checkout is dirty, there is something to lose and add refuses instead, naming the holder and both ways forward:
--take-over re-assigns the lease and leaves the checkout exactly as it is. The slot already holds that branch, so nothing is removed or re-checked-out: whatever the previous session had staged or untracked is still there, and now belongs to you.
--take-over does not refuse a running session, and its warning does not repeat that, so it is yours to use carefully.
It does refuse a checkout whose directory is gone while git still keeps work for it (VG), with worktree_dirty, as release does: the message says the slot “holds a checkout gone from its directory, with work git still keeps for it”, lists that work, and says git worktree repair <its new path> brings a moved one back, and that aspect worktree release <branch> --force=all discards it. One with nothing kept is freed and allocated as usual.
Pool housekeeping
Three things happen on the way through, so that the pool stays bounded without a separate cleanup habit. Stale slots are dropped. Free slots nothing has touched in 30 days are deleted along with their output bases, one line each:UR), nor one whose directory is gone while git still keeps work for it (VG). A repository of its own at a slot’s path — a .git directory, not the .git file a linked worktree has — counts as one git cannot read, since nothing here can vouch for its branches and history; so do a plain file at the slot’s path, and a checkout whose git reads another directory as its work tree (core.worktree set in the worktree’s own config). A free slot in either state is also skipped when choosing where your worktree goes, with a warning naming it. Run aspect worktree prune to do this on demand or at a tighter threshold.
Several agents can add at once. The pool lock is held while a slot is chosen and recorded, not for the checkout — though choosing includes aging out any free slot unused for 30 days, which deletes its output base and can take a while the first time it happens — so adds that start together run their git worktree add in parallel rather than taking turns. release and prune do hold it while they remove a checkout, which on a large repository takes seconds, so many releases at once queue behind each other. pool_busy arrives after about a minute of waiting, and is worth one retry. The lock is staged with its owner record and renamed into place, so it is never seen without an owner. A lock whose holder has exited is reclaimed by the next call, with a warning; one with no owner record — made by hand, say — is reclaimed once it is 10 seconds old. A lock moved aside while its holder still runs is put back before anyone takes the lock — unless it is the caller’s own, which is dropped rather than restored — and staged or moved-aside lock directories whose owner has exited are removed.
The pool’s limit is advisory. Worktrees.max_slots (8 by default) is a disk budget, not a concurrency limit, so at the limit with everything in use you still get a worktree and a warning. A slot is free only once its release has finished removing the checkout, so adds that run while releases are still in progress can grow the pool by a slot or two, which later adds then reuse:
Worktrees settings, are set in .aspect/config.axl, which has to load the trait first:
.aspect/config.axl
git worktree add, which costs a cold build and strands an output base. That is worse than using the disk.
The warning names the way back under the budget: release what you have finished with, then prune deletes free slots left idle. It offers no ready-to-paste --force, because deleting slots other sessions rely on is the user’s decision, not an agent’s. prune on its own is not enough at that moment anyway — it only ever deletes free slots holding no work, and the warning fires when there are none it could reuse — which is why release comes first.
Flags
string
required
Branch to check out: one this clone has, or one only a remote has, which is checked out tracking it. A name neither has is refused;
--create makes it. With --detach, any commit-ish.string
default:"off"
Create the branch, refusing if one by that name already exists here or on a remote. Bare, it starts from the
HEAD of the directory you run from — so running this inside one worktree branches from whatever that worktree has checked out, not from your main clone’s branch. --create=<ref> starts from that ref; --create=origin/main for fresh work.The new branch has no upstream. The exception is --create=<remote>/<branch> for a branch several remotes have, which picks that remote’s and tracks it.boolean
default:"false"
Check out
<branch> — or any commit, tag or remote branch — at a detached HEAD, on no branch. The slot is then addressed by its id. Refused alongside --create or --take-over, which are both about a branch.string
default:""
Name of the agent harness taking this worktree, recorded on the lease. Detected automatically for recognized harnesses, or set
ASPECT_AGENT_KIND.string
default:""
Session identifier of the agent taking this worktree, so a slot can be traced back to the session holding it. Detected automatically where the harness exports one, or set
ASPECT_AGENT_ID.boolean
default:"false"
Claim a slot this pool already leases for the branch, re-assigning it to this session. The checkout is left exactly as it is, so anything the previous session had uncommitted is still there. Refused without this flag, because the other session may only be suspended and resumable.
text | json | path
default:"text"
text writes a human-readable report to stderr and leaves stdout empty. json writes one document to stdout. path writes only the worktree directory, so a shell can step into it.JSON output
branch is empty for a --detach checkout, and detached names the commit-ish it is at instead. created says whether this call made the branch, base what it was made from as you named it, and base_sha the commit that was — which a later fetch does not move; upstream is empty for a branch that tracks nothing, which is how a created one starts. warnings, each with a code and a message:
reused_from is the branch the slot last held, empty for a new slot. score is the reuse heuristic’s own number and is advisory — a repo that installs a score_slot hook decides what it means. server_pid is 0 when no server is holding the slot.
agent is the session the lease was recorded for, with source naming which rung of the detection ladder answered — flag, env or ancestry. lease_count includes the lease just handed over, so a brand-new slot reports 1; the text output subtracts it and talks about prior use instead. output_base is where Bazel will put its state for this slot, derived rather than configured, which is what makes it the same directory every time this slot is used.
Refusals
Every refusal honours--output=json and carries a stable error token, so a harness branches on the token rather than on prose. These are the ones every command that changes the pool can raise:
And these come from
add in particular:
The other commands raise these:
Worth retrying:
pool_busy and branch_pending, which exit 75;
slot_setting_up, which exits 1 with retryable: true and clears once the add
setting the slot up finishes; reservation_lost, which exits 1 with retryable: true;
and slot_warm, whose own first suggestion is to
wait for Bazel’s idle timeout.
Everything else is a decision to make rather than a wait.
Two failure shapes are not refusals at all. A bad flag or a missing argument is
rejected by the argument parser before the task runs, so it exits 2 and
--output=json has nothing to write — a leading-dash branch name lands here,
since the CLI reads it as a flag. And an error token always arrives on stdout
as a document under --output=json, never only as prose.
message is escaped as text output is: a character a terminal would act on or hide is written as \uXXXX, or \UXXXXXXXX above U+FFFF, with line breaks and tabs kept. The facts beside it keep their raw values.
75 is EX_TEMPFAIL. Retry when the JSON says retryable: true — exit 75, slot_setting_up and reservation_lost — and treat any other non-zero exit as a real error. slot_warm is retryable too, but clears only when Bazel’s idle timeout fires, which can be hours: report it rather than wait.
A refusal is the pool declining a request it understood. An unexpected failure — git itself erroring, a workspace the pool cannot read — is reported as a plain message and a non-zero exit with no document, because there is no contract to offer for it.

