aspect describe '<command>', generated from the binary it is actually running, and from the error messages, which name the flag that fixes them. So nothing here goes stale when the CLI moves, and you never have to re-copy it.
Setting it up
Pick one. The first is the least work and stays current on its own.1
Register the docs MCP server, then add one line
Follow Aspect docs in your AI assistant to register the anonymous endpoint, That line is all you maintain. The agent reads this page live, so it tracks the docs rather than a copy you took once.
https://aspect.build/mcp. It needs no account and has one-click installers for Cursor and VS Code.Then put one line in your AGENTS.md, CLAUDE.md or equivalent:2
Or paste the whole thing into AGENTS.md
No MCP server, no network at agent time. Copy everything below the line into
AGENTS.md or CLAUDE.md. Works anywhere, including offline CI, at the cost of re-copying when this page changes.3
Or save it as a skill file
For harnesses that load skills on demand rather than keeping instructions in context. Save the text with frontmatter as Then the body from below the line.
.claude/skills/aspect-worktree/SKILL.md (Claude Code and the Agent SDK) or under .agents/skills/:add setup and release rules where the text marks them. The CLI cannot know that your checkouts need a symlinked .env or a direnv allow, and it cannot know whether the work in a slot matters.
Pooled worktrees
Never rungit worktree add. A worktree at a path Bazel has not seen is a cold build, and deleting it strands an output base that nothing in Bazel reclaims. aspect worktree hands out slots: directories at reused paths, so Bazel’s state survives from one task to the next and many tasks cost one output base rather than one each.
Run aspect describe 'worktree add' (or aspect worktree --help) for the current flags. This page is the workflow; that is the reference. Every command takes --output=json and answers with one document, which is how you read a result or a refusal without parsing prose.
Fetching this page programmatically? Append
.md to the URL for the source. The rendered page may come back summarized, and a summary of a workflow is not a workflow.Safety boundary
- The main clone is not a work directory. Work happens in a slot.
- Never
git stash. The stash stack is shared by every worktree of the clone, so another session can pop what you pushed. - Never discard someone’s uncommitted work.
aspect worktree releaserefuses a dirty slot and lists the files; only pass--forcewhen the user has said to. Untracked build output is not counted and never blocks a release: thebazel-*symlinks, and aMODULE.bazel.lockthe repo does not track. A tracked file that changed always counts, lock file included — if that is all the refusal lists, committing it is usually the way through, and that is a decision for the user. - A slot
aspect worktree listshows as held by another session is not yours, and nor is a taken slot with no agent beside it: a person took that one at a terminal — unless you took it yourself and your harness is not detected, whichinspectin your own slot shows. Taking it over, or ending its lease withrelease --force=all, is a decision for the user, and never one to make while that session is still running. Plain--forcenever touches another session’s lease. - A git worktree inside a slot — one your harness makes under an ignored
.claude/worktrees/, say — is work in that slot, listed asNW(“a worktree nested in this one”): removing the slot would delete it.releaserefuses without--force, and the slot is not taken back or pruned while it is there. - A slot whose directory is gone — moved with
mv, or deleted — while git still keeps commits only its HEAD reflog reaches (DH), refs only it has (WR), an operation in progress (IP) or a staged index (IX) keeps its lease.releaserefuses it, saying the slot “holds a checkout gone from its directory, with work git still keeps for it”, listed underVGwith aDHline naming the commits: if it was moved,git worktree repair <its new path>from the clone brings it back; otherwise thegit branch <name> <sha>theDHline names keeps them.add --take-overrefuses it too.--forcediscards them and frees the branch, and that is the user’s decision. - Never run
aspect worktree pruneunless asked. It deletes warm state other sessions are relying on. - Never delete a Bazel output base by hand, and never run
aspect gcunless asked: it removes idle output bases across every repository on the machine.aspect gc --dry-runshows what it would take.
Take a slot
add behaves as git worktree add does: it checks out a branch that exists — in this clone, or only on a remote, which it then tracks — and refuses a name that does not. New work says so with --create=origin/main, which starts it from a fresh main — bare --create would start from wherever you stand:
--create off:
path and release take the slot id add prints, or the ref you gave while no other slot is at it. Anything you commit there needs a branch before release — git switch -c <branch> — and release refuses with unreferenced_commits until it has one.
Use the two-step form. cd "$(aspect worktree add …)" returns 0 even when the command fails, because the substitution is empty and cd "" does nothing, so you would carry on in the main clone believing you had moved.
If your harness refuses compound shell commands, run the aspect worktree add … --output=path on its own and cd into the path it prints in the next command. Check the exit status first: on failure nothing is printed. Where it refuses cd <slot> && git …, run git as git -C <slot> … instead.
Every command writes its human-readable output to stderr. Stdout carries only --output=json, --output=path, and the directory aspect worktree path prints, so capturing any of them is safe; aspect worktree list 2>/dev/null prints nothing.
If it fails, branch on the error token in the JSON, not on the sentence. When retryable is true for pool_busy or branch_pending (exit 75), or for slot_setting_up or reservation_lost, wait a moment and run the same command again. slot_warm, from prune, is retryable too but clears only when Bazel’s idle timeout fires, which can be hours: report it rather than wait. Everything else is a decision, and the table says whose:
path and release take a branch, a slot id, or the slot’s directory — whichever you have. A slot found by its branch is found by the branch checked out in it, so one you ran git switch in answers to the new name.
warnings in the JSON of a successful add are worth reading. pool_over_limit means the pool grew past its budget: release any slot you are done with, and tell the user. upstream_elsewhere means an existing branch tracks a branch of another name, so a bare git push could land there: push with the explicit git push -u it names. score_hook_failed means the repository’s scoring hook is broken or too slow; mention it to the user. lease_reclaimed means the slot was taken back from a stopped session, whose branch and previous_holder it names. Files git ignores go with a checkout on release without being listed: keep nothing you need in one.
Fetching from any worktree of the clone updates them all. add says what a new branch was created from, and what its upstream is.
A branch made with --create has no upstream, so your first push must name it: git push -u origin <branch>. A bare git push has nothing to target, which is deliberate. A branch picked up from a remote tracks that remote branch already.
Working in parallel
Subagents report their parent’s session, so tolist every slot your siblings hold looks like yours — and filtering by session would hand you theirs to release. When you fan work out, give each agent its own id and use it on every call:
aspect worktree command takes --agent-id. Setting ASPECT_AGENT_ID once in each agent’s environment does the same. path will not hand an agent another session’s slot, a sibling’s, or one a person took at a terminal: only the session itself gets its own subagents’ slots. The ids are what tells siblings apart, and the commands a refusal suggests carry the id.
A subagent’s lease records your session as its parent_id, and list --verbose lists it under your session with the subagent named. Its liveness is your session’s, so a subagent that crashed still reads as running: whether it stopped is yours to know. Run as the session itself, without an --agent-id of your own, you can act on a subagent’s slot as on your own: aspect worktree release <branch> ends its lease (--force to discard its uncommitted work), and aspect worktree add <branch> --take-over takes the slot with its work — your call, not the user’s, since the work is your session’s own. A sibling subagent cannot: release and --take-over of another sibling’s slot are refused, with same_session: true, and the message says to leave it to the session. Neither add’s branch_in_use nor path’s held_by_another_session gives an agent the path of a slot it does not hold: a subagent gets only its own — not a sibling’s, and not its parent session’s — and only the session itself gets its subagents’. It is told the slot’s id, not the way into it. In refusals, same_session: true marks a holder in your harness session.
Keep the slot and path each add returns, and act on those. Take slots with --output=json rather than --output=path when you start several at once: it gives both.
add takes a slot back on its own from a session whose process stopped more than Worktrees.abandoned_grace_hours ago — counted from the slot’s last activity — and whose checkout holds nothing to lose. Files git ignores do not count as something to lose: its checkout is removed and the branch asked for is checked out fresh there, which INFO: taking back … says. Work in a checkout always keeps the slot. Once a stopped session’s slot is taken back, a plain add of its branch gives you that branch, so make sure it is yours to work on. The grace period is set in .aspect/config.axl, by the user rather than by an agent:
path, inspect anywhere in the clone or its slots, or add of a branch already leased, every lease of the session — its subagents’ included — moves to its new process, provided the one recorded has stopped. Leases are matched by the session id the harness reported, never by an --agent-id you chose; a session whose harness reports no session id has none moved.
Finding your slot again
If you lose track of where you were working:aspect worktree inspect, run from the clone, lists the slots this session holds with their branches and paths — run as the session itself, its subagents’ too, each named — then the leases of this session that ended in the last day: released, taken over, or taken back. Their commits are on their branches. A held slot whose checkout directory is gone is marked(checkout gone: `aspect worktree release` says why);releaseof it names what git still keeps for it and how to keep that. With--output=jsonthese areheldandended, eachheldentry{slot, branch, path, agent_id, gone},gonetrue for such a slot, and eachendedentry{branch, slot, ended_ms, agent_id},agent_idnaming the subagent and empty for the session’s own lease.aspect worktree path <branch>prints one slot’s directory again.- After a resume (
claude --resume), runaspect worktree inspectearly, from the clone or a slot. Run by your session from a new process, it moves every lease of your session — your subagents’ included — off a recorded process that has stopped onto the new one, so the slots read as running and are not taken back;aspect worktree path <branch>andaddof a leased branch do the same. A lease whose process was never captured is left as it is. To keep working on a branch listed underended,addit again. aspect worktree inspect --output=json, run inside the slot, gives its full state:branch,head,head_on_remote,uncommittedandbase_sha.
Work in it
Nothing in the slot has to be an Aspect command. Bazel derives its output base from the directory, sobazel, aspect, an IDE or a script all reach the same warm state.
If a fresh checkout in your repo needs setting up — symlinking ignored files such as
.env, running direnv allow, installing hooks — say so here. The pool hands over a checkout and a warm output base; it has no idea what else your repo expects.Release it
Releasing removes the checkout and keeps the branch: every commit stays in the clone, so the only thing a release can lose is work you have not committed. Decide by what the work needs, not by fear of losing it.aspect worktree inspect --output=json, run in the slot, gives the evidence: head, head_on_remote, uncommitted, and where a branch you created started — base as you named it, base_sha as the commit it was then.
To archive, bundle only what the branch added:
git bundle create <file> <base_sha>..<branch>, using base_sha from inspect — the commit, since a ref like origin/main moves on the next fetch. base_sha is empty for a branch that already existed in the clone; then start from $(git merge-base origin/main <branch>), or bundle the whole branch with git bundle create <file> <branch>. For a branch picked up from a remote, base_sha is the remote’s tip when you took it, so the bundle holds only your commits on top. Run git bundle from the clone or the slot — branches are shared — and check it with git bundle verify <file>. If you commit again after archiving, archive again: the bundle stops at the tip it was made from. Say where you left it, and how to restore it: git fetch <file> <branch>:<branch> in a clone that has base_sha.
A branch you only picked up to read — a teammate’s, a pull request’s — leaves a local copy behind after release; git branch -D <branch> from the clone removes it.
If you started a Bazel server in the slot and want it gone, bazel shutdown there first: once the slot is released there is nowhere to run it from. cd back to the directory you ran add from — usually the main clone — before releasing. A release from inside the slot removes the directory your shell is standing in, and every later command from there fails; release warns when that has happened and names where to go back to. An aspect command run from the removed directory fails with “the current directory no longer exists — if it was a worktree that was released, cd back into the clone”.
release says when the branch’s commits are on no remote, which is the row above to act on.
Reclaiming disk
aspect worktree list shows how long each free slot has been idle. aspect gc reclaims output bases machine-wide; aspect worktree prune retires a whole slot — all the stale ones, or just the one you name. Running either is the user’s call (see Safety boundary); --dry-run on either only looks. When add warns that the pool is over its advisory limit, release what you are done with and tell the user; prune --dry-run shows what could go, and deletes nothing.
aspect worktree inspect reports the slot you are standing in — its Bazel state,
its history, and how to get back to the session holding it. aspect worktree list --all covers every pool on the machine, which is the one way to answer
“what did I leave alone because another session held it”.Report back
- the slot path and the branch;
- what you released, and what you held with the reason and the files;
- any archive you created, and how to restore it;
- anything you left alone because another session held it.

