> ## Documentation Index
> Fetch the complete documentation index at: https://aspect.build/llms.txt
> Use this file to discover all available pages before exploring further.

# aspect worktree add

> Take a git worktree from a pool of reused paths with aspect worktree add, so Bazel's server, analysis cache and external repositories are already in place.

<Warning>
  New and experimental. File bugs and suggestions to [aspect-build/aspect-cli/issues](https://github.com/aspect-build/aspect-cli/issues).
</Warning>

Create a git worktree in a pooled slot and check out `<branch>` in it. The slot is a path the pool reuses, so Bazel's server, analysis cache and `external/` tree are often already there.

```shell theme={null}
aspect worktree add fix/login                          # a branch this clone or a remote has
aspect worktree add fix/signup --create=origin/main    # a new branch, from a fresh main
aspect worktree add v2.3.0 --detach                    # any commit, on no branch
```

Which branch you get follows `git worktree add`'s rules, so what you know about that command still holds; [the comparison below](#compared-with-git-worktree-add) covers its flags one by one.

Entering it in one step:

```shell theme={null}
slot=$(aspect worktree add fix/login --output=path) && cd "$slot"
```

`--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

```
INFO: worktree ready: ~/.aspect/worktrees/github.com/acme/web/aspect-worktree-f066bb8445cc
  branch: fix/signup, created from origin/main (e567ac6b)
  upstream: none, deliberately. To set one, in the slot:
    git push -u origin fix/signup   # make fix/signup on the remote and track it
    git branch --set-upstream-to=origin/main   # track origin/main
  warm: no server, but this slot's Bazel output base is still here, last leased for fix/logout
  created 12d ago, used 9 times before this

  # enter it
  slot=$(aspect worktree path fix/signup) && cd "$slot"

  # or add and enter in one step: --output=path prints only the directory
  slot=$(aspect worktree add <branch> --create --output=path) && cd "$slot"

  # when done, release it: from inside the slot
  aspect worktree release
  # or from the clone, or anywhere in it
  aspect worktree release fix/signup
```

A created branch tracks nothing until you say what. `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`](/docs/cli/worktrees/list) 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:

| | |
| - | - |
| An identical tree | Warm, and the slot last held this exact commit, so its outputs are exactly right |
| The same branch | Warm, and the slot last held this branch, so returning to it lands on the outputs built for it |
| Anything else | Warm, from some other work at some other point |
| `MODULE.bazel.lock` differs | Warm, but repository fetches miss and `external/` is rebuilt — the expensive part — though the action cache still helps |
| Cold | No output base now, never built or since collected, so nothing survives whatever the slot last held |

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](/docs/cli/guides/worktrees#tuning-which-slot-you-get) has an example and the fields a hook is given.

The warmth line is read from disk, not assumed:

| Line | What the slot already has |
| - | - |
| `hot` | A live Bazel server, still holding the analysis cache. The expensive thing — as long as your flags match the last build's, and the server is still up. Bazel shuts one down once it has been idle past `--max_idle_secs`, which turns a hot slot into a warm one with nothing lost but the analysis cache. |
| `warm` | No server, but the slot's Bazel output base is still there. |
| `cold` | Nothing yet. The first build pays for loading, analysis and fetches. |

## Branches

`<branch>` is resolved as `git worktree add <path> <branch>` resolves it:

| You ask for | You get |
| - | - |
| A branch this clone has | That branch, checked out as it is |
| A branch only a remote has — a teammate's, a pull request's | A local branch of that name, created from the remote's and tracking it, so a bare `git push` goes back where the work came from. `origin` wins when several remotes have it; otherwise the refusal asks you to choose |
| A name nothing has | Refused with `no_such_branch`, naming the closest branch there is, and the two ways to create it: `--create=<remote>/<default>` from the remote's default branch, or a bare `--create` from the branch (or, detached, the commit) checked out where you run it. A typo never quietly becomes a branch |
| A tag, a commit, or a remote branch spelled `origin/…` | Refused with `not_a_branch`, whose `kind` says which. For `origin/x` it offers `add x` first, unless x is already checked out somewhere; for any of them, `--create` to start a branch there or `--detach` to look at it |
| A name differing only in case from a branch | Refused with `not_a_branch`, `kind` `branch_case`, naming the branch's spelling — a case-insensitive filesystem would otherwise check one branch out twice |

`--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:

```shell theme={null}
git fetch origin
aspect worktree add fix/signup --create=origin/main
```

A created branch has **no upstream**, deliberately, and `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:

```shell theme={null}
git push -u origin fix/signup
```

A branch that already tracks one of a different name gets an `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.

```
INFO: worktree ready: ~/.aspect/worktrees/github.com/acme/web/aspect-worktree-1135398e6688
  detached at origin/main (e567ac6b), on no branch: commits made here need one before release — git switch -c <branch>
  cold: this slot has no Bazel state yet, so the first build pays for it
  new slot: this is the first worktree it has held

  # enter it
  slot=$(aspect worktree path 1135398e6688) && cd "$slot"

  # or add and enter in one step: --output=path prints only the directory
  slot=$(aspect worktree add <branch> --create --output=path) && cd "$slot"

  # when done, release it: from inside the slot
  aspect worktree release
  # or from the clone, or anywhere in it
  aspect worktree release 1135398e6688
```

With no branch to be named by, the slot is addressed by its id — `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`](/docs/cli/worktrees/release#detached-slots) 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.

<Warning>
  A branch can only be checked out in one worktree at a time. Asking for one that is already in use names where it is, before anything is changed. What it says depends on who is holding it. Your own session is pointed back
  at the slot it already has:

  ```
  ERROR: fix/login is already held by this session, checked out at
         ~/.aspect/worktrees/…/aspect-worktree-18a11aa20e8d. If you took it,
         `aspect worktree path fix/login` prints that path again. Subagents share their
         parent's session, so if you did not, another agent of this session did — use a
         different branch.
  ```

  Another session gets named, and is told the slot by its id, not its path: the checkout is the holder's to work in. A running one is left to whoever drives it, with no command to paste:

  ```
  ERROR: fix/login is checked out in slot 18a11aa20e8d, held by a running claude-code session
         b22fe20c-1111-4000-8000-00000000b22f. That session is still running, so leave the slot
         to it, or ask whoever is driving it; claiming it anyway is `--take-over`, and the user's
         decision.
  ```

  A subagent asking for a sibling's branch is told the same way:

  ```
  ERROR: fix/login is checked out in slot 18a11aa20e8d, held by claude-code agent worker-a in
         session b22fe20c-1111-4000-8000-00000000b22f, another agent of this session. Leave it and
         take a branch of your own; the session itself can release it or take it over.
  ```

  The same applies to worktrees the pool does not manage, including your main checkout:

  ```
  ERROR: main is already checked out at ~/code/repo, which is not managed by aspect worktree,
         and a branch can be checked out in only one place. Switch that checkout to another
         branch and try again, or:

    aspect worktree add <new-branch> --create=main   # new work starting there
    aspect worktree add main --detach   # build or read it on no branch
  ```
</Warning>

## 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:

| `git worktree add` | `aspect worktree add` | |
| - | - | - |
| `<path> <branch>` | `<branch>` | The same resolution for a branch: a local one is checked out, a remote-only one is created tracking the remote's, an unknown name is refused. Where several remotes have it, `origin` wins rather than git's `checkout.defaultRemote`. One difference: git checks out a tag, a commit or `origin/x` detached, and `add` refuses those with `not_a_branch` unless you say `--detach` |
| `-b <branch>` | `--create` | Refuses a name this clone has, as `-b` does, and also one a remote has (`remote_branch_exists`), since the first push would collide with it. The flag's value is where the branch starts — `HEAD` bare, or `--create=<ref>` — not its name, which is the positional; `not_a_branch` says so when the two look swapped |
| `--detach` | `--detach` | The same. The slot is then addressed by its id, or by the ref while no other slot is at it |
| `--track` / `--no-track` | — | Chosen for you: a created branch never tracks, a remote-only one tracks its own remote branch. Tracking the base is how a bare push lands on `main` |
| `--guess-remote` | — | Not applicable: git uses it to guess a branch from the path when none is given, and `add` always takes a branch. Checking out a remote-only branch, which git does without a flag, `add` does too |
| `--lock`, `--reason` | — | Always on: every leased slot holds git's lock, with a reason naming the pool, so `git worktree remove` of it refuses. `release` lifts it |
| `-q` | `--output` | `text` writes to stderr and leaves stdout empty already; `json` and `path` are for machines |
| `-f` | — | Not offered. It puts one branch in two worktrees, which is how two agents end up committing over each other. `--detach` is the safe way to look at a branch held elsewhere |
| `-B` | — | Not offered. It moves an existing branch, losing commits only that branch had; `git reset` inside the slot does it knowingly |
| `--orphan` | — | Not offered. An unborn branch has nothing for Bazel to keep warm |
| `--no-checkout` | — | Not offered. A slot with no files cannot build, which is the whole use of one |
| `--relative-paths` | — | Not offered. Bazel keys its output base on the absolute path, so slot paths stay absolute |

`--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, so [`aspect worktree list`](/docs/cli/worktrees/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:

```shell theme={null}
export ASPECT_AGENT_KIND=my-harness
export ASPECT_AGENT_ID="$SESSION_ID"
```

or pass them per call:

```shell theme={null}
aspect worktree add fix/login --agent-kind=my-harness --agent-id="$SESSION_ID"
```

Flags beat the environment, field by field, and both beat detection — see [agent detection](/docs/cli/worktrees/agents). A harness that reports nothing is simply unlabelled.

A slot whose holder is **no longer running**, has been given a **day** to come back, and whose checkout is **clean** is taken back rather than the pool growing a new slot: its checkout is removed and yours is checked out fresh in the same slot. The day — `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](/docs/cli/worktrees/agents#what-makes-a-session-resumable-and-what-makes-a-slot-reclaimable). 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.

```
INFO: taking back fix/login's slot from claude-code session b22fe20c-1111-4000-8000-00000000b22f:
      the process that held it is not running and the checkout was clean
```

That is `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:

```
ERROR: fix/login is checked out in slot 18a11aa20e8d, held by claude-code session
       b22fe20c-1111-4000-8000-00000000b22f, which is not running. Resume that session:

  cd ~/src/web && claude --resume b22fe20c-1111-4000-8000-00000000b22f

Claiming the slot for this session instead is the user's decision, since it inherits whatever
that session left uncommitted: `aspect worktree add fix/login --take-over`.

It was not taken back because the checkout holds work that would be lost:
  ?? notes.txt
```

Resuming is offered first because a session that has stopped is usually a closed terminal, with work in it waiting to be picked up rather than finished with.

`--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.

```shell theme={null}
aspect worktree add fix/login --take-over
```

```
WARNING: taking fix/login over from claude-code session b22fe20c-1111-4000-8000-00000000b22f;
         the checkout is untouched, and these uncommitted changes in it are now yours:
  ?? notes.txt
INFO: worktree ready: ~/.aspect/worktrees/github.com/acme/web/aspect-worktree-18a11aa20e8d
```

Without the flag, the refusal says the session is running and to ask whoever drives it. `--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:

```
INFO: pruned slot (idle 41d): old-experiment
```

A slot someone is working in is never touched, nor is one a live Bazel server still holds, nor one whose checkout still holds uncommitted work or that git cannot read (`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`](/docs/cli/worktrees/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:

```
WARNING: all 8 of this clone's slots are in use, so it now has 9 checkouts and output
         bases against an advisory limit of 8. `aspect worktree list` shows who holds
         each; releasing those you are done with lets later adds reuse them instead of
         growing, and `aspect worktree prune` then deletes free slots left idle.
         `Worktrees.max_slots` in config.axl sets the limit.
```

Free slots that could not be reused — holding work, or a directory that would not go, each warned about above it — are counted apart from the slots in use:

```
WARNING: 7 of this clone's 8 slots are in use and 1 free one could not be reused — it
         holds work, or a directory that would not go, as warned above, so it now has 9
         checkouts and output bases against an advisory limit of 8. …
```

The limit, and the other `Worktrees` settings, are set in `.aspect/config.axl`, which has to load the trait first:

```python title=".aspect/config.axl" theme={null}
load("@aspect//traits.axl", "Worktrees")

def config(ctx: ConfigContext):
    ctx.traits[Worktrees].max_slots = 12
    ctx.traits[Worktrees].abandoned_grace_hours = 48
```

Refusing would send you back to `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

<ParamField path="branch" type="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.
</ParamField>

<ParamField path="--create" type="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.
</ParamField>

<ParamField path="--detach" type="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.
</ParamField>

<ParamField path="--agent-kind" type="string" default="">
  Name of the agent harness taking this worktree, recorded on the lease. Detected automatically for recognized harnesses, or set `ASPECT_AGENT_KIND`.
</ParamField>

<ParamField path="--agent-id" type="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`.
</ParamField>

<ParamField path="--take-over" type="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.
</ParamField>

<ParamField path="--output" type="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.
</ParamField>

## JSON output

```shell theme={null}
aspect worktree add fix/signup --create=origin/main --output=json
```

```json theme={null}
{
  "schema_version": 1,
  "path": "/Users/you/.aspect/worktrees/github.com/acme/web/aspect-worktree-f066bb8445cc",
  "slot": "f066bb8445cc",
  "branch": "fix/signup",
  "detached": "",
  "created": true,
  "base": "origin/main",
  "base_sha": "e567ac6b2a18bed22ddf9e220a7e35a62f04e024",
  "head": "e567ac6b2a18bed22ddf9e220a7e35a62f04e024",
  "upstream": "",
  "pool": "github.com/acme/web",
  "score": 90,
  "reused_from": "fix/logout",
  "agent": {
    "kind": "claude-code",
    "id": "b22fe20c-1111-4000-8000-00000000b22f",
    "pid": 48213,
    "pid_start": "Mon Oct  5 09:14:02 2026",
    "cwd": "/Users/you/src/web",
    "project": "",
    "source": "env",
    "seen_ms": 1791209206994,
    "harness_id": "b22fe20c-1111-4000-8000-00000000b22f"
  },
  "server_pid": 0,
  "output_base_present": true,
  "lease_count": 10,
  "created_days": 12,
  "output_base": "/Users/you/Library/Caches/bazel/_bazel_you/e9a33320d289064646ca04497ef4fcec",
  "warnings": []
}
```

`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`:

| `code` | Meaning |
| - | - |
| `upstream_elsewhere` | The branch tracks one of a different name, so a bare `git push` could land there. Push with the `git push -u` the message names |
| `pool_over_limit` | The pool grew past `Worktrees.max_slots`; `slots` is how many it now has and `max_slots` the limit. The message counts free slots that could not be reused apart from slots in use. Release what you are done with |
| `taken_over` | `--take-over` re-assigned another session's lease, named under `previous_holder`; `uncommitted` lists what it left in the checkout, now yours |
| `lease_reclaimed` | The slot was taken back from a session that had stopped with a clean checkout; `branch` is what it held and `previous_holder` the session |
| `score_hook_refused_all` | A repository's `score_slot` hook refused every free slot with the pool at its limit, so one of them was reused rather than growing the pool |
| `score_hook_failed` | A repository's `score_slot` hook failed, returned something other than an int, or took more than 2 seconds for this `add` in all, so the built-in scoring picked the slot. The message carries the hook's error, escaped so a control or bidirectional character in it is shown rather than acted on, or its time |

`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:

| `error` | Exit | Meaning |
| - | - | - |
| `pool_busy` | 75 | Another call holds the pool lock. Retryable: wait a moment and try again |
| `reservation_lost` | 1 | Another `add` judged this one stopped and took its slot while the checkout ran. Retryable: run the same command again |
| `not_a_repository` | 1 | The working directory is not a clone |

And these come from `add` in particular:

| `error` | Exit | Meaning |
| - | - | - |
| `branch_pending` | 75 | Another `add` is checking that branch out right now, into a new slot or a reused one — or creating it with `--create`, which a plain `add` of the same name is told rather than `no_such_branch`. Retryable: it will be a lease, or yours, shortly. Under `--create` it exits 1 and is not retryable — the branch will be the other call's, so new work needs another name. `same_session` says when the `add` in progress is this session's own — a sibling subagent, or this command run twice |
| `no_such_branch` | 1 | No branch by that name here or on a remote this clone has fetched. The message names the closest, in `closest`, with the command for it: `aspect worktree path <branch>` when this session already holds it, `aspect worktree add <branch>` otherwise. It also names `git fetch` for a branch somebody else pushed, and `--create` |
| `not_a_branch` | 1 | The name is a tag, a commit or a remote branch spelled `origin/…`. The message offers `--create` and `--detach` |
| `ambiguous_remote_branch` | 1 | Only remotes have the branch, several of them, and none is `origin`. `--create=<remote>/<branch>` picks one |
| `branch_exists` | 1 | `--create` was given a branch this clone has. Leave the flag off to check it out |
| `remote_branch_exists` | 1 | `--create` was given a name a remote has. Leave the flag off to check out the remote's |
| `no_such_ref` | 1 | `--create=<ref>`, or what `--detach` was asked for, is not something this clone can resolve |
| `conflicting_flags` | 1 | `--detach` with `--create` or `--take-over`, which are both about a branch |
| `branch_held_by_this_session` | 1 | Your own session already holds a slot on that branch. `aspect worktree path <branch>` prints it again. Subagents report their parent's session, so this can be a sibling's slot |
| `invalid_agent` | 1 | `--agent-kind` or `ASPECT_AGENT_KIND` is not a name: ASCII letters, digits, `-`, `_` and `.`, starting with a letter or digit, at most 64 long. Or `--agent-id` or `ASPECT_AGENT_ID` is all spaces, longer than 128 characters, or has a character a terminal would act on or hide — a C0 or C1 control character, DEL, the soft hyphen, the combining grapheme joiner, a Hangul or Khmer filler, an Arabic or Mongolian format mark, a zero-width character or joiner, a line or paragraph separator, a bidirectional override or isolate, an invisible operator, a variation selector, a musical formatting character, an interlinear annotation mark, a byte-order mark or a tag character — since an id is printed in tables, messages and suggested commands. The message shows the value in double quotes, each such character escaped once, as `\u202e`, or as `\U` and eight hex digits above U+FFFF. Every command checks both, since they say who holds a lease |
| `invalid_config` | 1 | `Worktrees.abandoned_grace_hours` is negative, or `Worktrees.max_slots` is below 1. `setting` and `value` name which |
| `registry_newer` | 1 | The pool's registry was written by a newer aspect CLI, which this one would rewrite without what it does not understand. `list`, `path`, `inspect` and `prune --dry-run` still read it. A version that is not a positive int is damage instead: the registry is rebuilt from git, with a warning |
| `branch_in_use` | 1 | Another session holds that branch — another agent of your own session included, which only the session itself can take over (`same_session: true`). The message names it and how to resume it; when that session has stopped, it gives the `--take-over` command and says what in its checkout kept the slot from being taken back. A running session's refusal gives no command to paste: two agents in one checkout is for whoever drives both to decide. The facts carry the slot's `path` only to the holder itself, to the session itself when the holder is its subagent, and for a lease a person took — a subagent gets no `path` for its parent session's slot; any other agent — another session, or a sibling subagent — is told the slot exists, not the way into it, and the message names the slot by its id. `process` says whether the holder's recorded process is `running`, `stopped`, or `unknown` where none was recorded — apart from `session_state`, which asks the harness about the session |
| `branch_elsewhere` | 1 | The branch is checked out in a worktree the pool does not manage, such as your main clone — or in a pool slot no lease of this clone accounts for, whose message names `aspect worktree release <slot>`. A slot another `add` has reserved for this same branch, and is part way through checking it out into, gives `branch_pending` instead |
| `checkout_failed` | 1 | `git worktree add` failed, or the slot chosen still held a checkout with uncommitted work, which is left alone. The message says which |
| `invalid_branch` | 1 | Not a name git will create a branch from, said before git is asked. `HEAD` and the other refs git reserves are refused too, since a branch by that name makes every later reference to it ambiguous. So is a `--create` name that is or starts with a remote's (`origin`, `origin/fix`, in any case), that is or starts with `refs`/`remotes`, that equals a slot id in any case — and, without `--create`, a bare remote name or `refs`/`remotes` that is no branch here, which would otherwise resolve to the remote's default branch — or that has a part longer than 250 bytes — git adds `.lock` to it while it writes |

The other commands raise these:

| `error` | Exit | Raised by | Meaning |
| - | - | - | - |
| `no_such_worktree` | 1 | `release`, `path`, `prune`, `inspect` | Nothing in this pool answers to that branch or slot. The message says what this clone does have out — and, for a branch whose lease of your session ended in the last day, that the lease ended, in which slot and how long ago, with `aspect worktree add <branch>` to take it again; the facts carry those leases as `ended` |
| `slot_stranded` | 1 | `add`, `release`, `path`, `prune`, `inspect` | The slot, however it was named, is leased for a clone that is gone and whose checkout no clone here lists, so nothing can lease or release it again. Or it is leased for this clone's path but this clone's git has no worktree there: a clone moved away and this one was cloned where it was, so the checkout is the moved clone's. Either way, if that clone moved, run the command from where it is now, which takes the slot back; otherwise the message names `aspect worktree prune <slot> --force=all`. For a slot recorded for this clone's path, the message also allows that the checkout is this clone's own and damaged, and names `git worktree repair <path>` to try first |
| `slot_not_leased` | 1 | `path` | The slot resolved, but it is free, so there is no worktree to enter. `release` of a free slot is not an error: it exits 0 with `already_free` |
| `slot_setting_up` | 1 | `release`, `path`, `prune` | An `add` is still checking a worktree out into that slot. Retryable while it runs; `retryable: false` with `abandoned: true` once it has stopped, which `add` of that branch recovers — and `release` and `prune` reclaim such a slot themselves |
| `held_by_another_session` | 1 | `release`, `path`, `prune` | Another session holds the slot and has not verifiably stopped — or, for a subagent, another agent of its own session holds it, stopped or not, which is the session's to release and which no flag overrides. Otherwise `--force=all` ends its lease. `prune` raises it for a stranded slot whose lease's session is still running, which says its clone moved, whatever `--force` says — and for one whose process was never captured, unless `--force=all` is given. `path` raises it when the caller is an agent and the slot is another session's, a sibling subagent's or a person's — the session itself may get its own subagents' slots, and a person at a terminal any slot — and its facts carry no `path`, nor do `release`'s; when the caller's own session held the same branch in that slot before, the message says that lease ended — released, taken over, or taken back — and its commits are on the branch. `release` and `path` carry `process`, `running`, `stopped` or `unknown`, beside `session_state` |
| `unreferenced_commits` | 1 | `release` | Commits only the worktree's HEAD reaches — at HEAD now, or left behind when HEAD moved on, which only the checkout's reflog still reaches. `tips` lists the fewest whose branches keep them all. `--force` discards your own; another session's needs `--force=all` |
| `slot_in_use` | 1 | `prune` | The named slot holds a worktree. The message names `release` for ending the lease instead |
| `worktree_dirty` | 1 | `release`, `prune`, `add --take-over` | The worktree has uncommitted changes, which are listed — or git cannot read the checkout, so work cannot be ruled out, or its directory is gone while git still keeps work for it (`VG`). Those two are said as what the slot "holds", and a gone one's message gives `git worktree repair <its new path>` for a moved checkout. `--force` discards your own; another session's needs `--force=all`, as does any on `prune`. When the session itself releases its own subagent's slot, discarding is the session's decision, and the message says so |
| `needs_force` | 1 | `prune` | There is no terminal to confirm at, so it will not guess. `--force` proceeds, `--dry-run` looks |
| `slot_warm` | 1 | `prune` | The named slot is free but a Bazel server is still running in it. Unlinking an output base from under a live server leaves it writing into directories that are gone, so this is refused however it was asked for — `--force` covers not having a terminal, not overruling that. The message names the server and offers `aspect gc <hash>`, which stops it and takes the base in one step |
| `owned_elsewhere` | 1 | `release`, `path`, `prune`, `inspect` | The slot belongs to another clone of this repository that still exists, however it was named — branch, slot id or directory, or for `path`, `prune` and `inspect` an id prefix. It is that clone's to manage |

**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.

```shell theme={null}
aspect worktree add fix/login --output=json
```

```json theme={null}
{
  "schema_version": 1,
  "error": "pool_busy",
  "retryable": true,
  "message": "another `aspect worktree` call is holding this pool's lock; try again"
}
```

`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.

<Warning>
  Check the exit status. `cd "$(aspect worktree add …)"` **reports success when `add` fails**, because the command substitution is empty and `cd ""` is a no-op that returns 0 — so a script carries straight on in whatever directory it was already in. Use `slot=$(aspect worktree add … --output=path) && cd "$slot"`, which propagates the failure.
</Warning>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.