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

> Delete pooled worktree slots and their Bazel output bases: name one to delete it however recently it was used, or name nothing to reclaim every free slot that has gone stale.

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

Delete a slot and the Bazel output base behind it. Name one to delete that one, or name nothing to take every free slot that has gone stale.

```shell theme={null}
aspect worktree prune --dry-run
```

```
INFO: 1 slot can be reclaimed, under ~/.aspect/worktrees/github.com/acme/web:
    slot          why       last branch
    0b8751be74ee  idle 41d  old-experiment
INFO: dry run; nothing was deleted
```

`--dry-run` only reads, so it does not wait for the pool lock while another call holds it.

Without `--dry-run` it asks, then says what it removes as it goes:

```
  [1/1] removing ~/.aspect/worktrees/github.com/acme/web/aspect-worktree-0b8751be74ee
INFO: pruned 1 slot
```

Each slot costs a checkout plus an output base, tens of gigabytes on a large repo, so a pool that grew during a busy week keeps paying for it afterwards.

The `why` column says why each slot is eligible, rather than quoting one threshold at all of them: `idle 41d` for a slot nothing has used in that long, `named` for a slot you named, and `no recorded age, nothing on disk` for an entry with no checkout, no output base and nothing to age it by.

## Deleting one slot

Name a branch, a slot id or a slot's directory — the first two are on screen in [`aspect worktree list`](/docs/cli/worktrees/list) — and how long it has been idle is not consulted. Naming it is the decision.

```shell theme={null}
aspect worktree prune fix/logout       # the branch a free slot last held
aspect worktree prune 416e4a0ec302     # or the slot itself
```

A slot somebody is working in is refused, because deleting it would take the checkout and the warm output base with it. Ending the lease is the reversible thing, and the refusal says so:

```
ERROR: slot 18a11aa20e8d holds fix/login, which claude-code session
b22fe20c-1111-4000-8000-00000000b22f is working in. Pruning it would delete their checkout
along with whatever Bazel state the slot has built up. If that session is finished with it,
ending the lease is the reversible step and theirs to run:

  aspect worktree release fix/login
```

<Note>
  This also runs automatically during [`aspect worktree add`](/docs/cli/worktrees/add), at the default threshold. Ordinary use keeps the pool trimmed; this command is for doing it now, or at a tighter threshold.
</Note>

A dry run applies the same exemptions a real one does — a live server, a checkout holding uncommitted work — so it previews the outcome rather than listing everything old enough to be considered.

Without a terminal to ask — a script, CI, an agent — `prune` refuses rather than guessing, with the `needs_force` token and exit 1:

```json theme={null}
{
  "schema_version": 1,
  "error": "needs_force",
  "retryable": false,
  "message": "refusing to delete 2 worktree slots and their Bazel output bases with no terminal to confirm at. Pass --force to proceed, or --dry-run to look without changing anything."
}
```

## What it will not delete

**Uncommitted work.** A free slot should hold no checkout. If one does and git reports work in it, or cannot read it at all, the slot is kept and reported rather than deleted — the bookkeeping is what is wrong, not the work. A repository of its own at the slot's path — a `.git` directory rather than the `.git` file a linked worktree has — counts as a checkout git cannot read (`UR`), 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 slot whose directory is gone 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`) is kept too. Only naming the slot with `--force=all` overrides that.

**A slot someone is working in.** Only free slots are considered, whatever their age, and whether you named one or not — except a stranded slot, which can be named. A lease means a checkout that may have uncommitted work in it, and neither a timer nor an argument is grounds for deleting that — [`aspect worktree release`](/docs/cli/worktrees/release) ends the lease first.

**A slot being set up.** An `aspect worktree add` claims a slot before it checks a worktree out into it, and deleting the output base in between would break a command that is still running. Such a slot reads `setup` in [`aspect worktree list`](/docs/cli/worktrees/list).

**A slot whose Bazel server is still running.** Unlinking an output base from under a live server leaves it writing into directories that no longer exist. Such a slot is also the warmest thing in the pool, which is the whole point of having one, so it is reported and kept:

```
WARNING: kept 18a11aa20e8d: a Bazel server (pid 73024) still holds it
```

It becomes prunable on its own once Bazel's idle timeout fires.

## Slots stranded by a clone that is gone

A slot belongs to one clone, because `git worktree add` is scoped to a single object store, and records it by path. Move, rename or delete that clone and the next `add`, `release`, `path` or `prune` in a clone of the repository takes back what it can:

* **A free slot** goes to that clone, warm output base and all, and from then on ages like any other free slot. It holds no checkout, so it is nobody's in particular.
* **A leased slot** goes only to the clone whose git still lists its checkout — the moved clone itself — and `git worktree repair` points the checkout back at it, so the lease and its work carry on there. When the slot has moved too, recovering the lease takes `git worktree repair` run by hand. A slot moved away on its own whose git records hold none of the commits only its HEAD reflog reaches, refs only it has, an operation in progress or a staged index has its git registration dropped, so the moved copy's git link breaks; its files are kept.

Until then, [`aspect worktree list`](/docs/cli/worktrees/list) tags such slots `clone moved`. `prune` says what it took over, and `prune --dry-run` what it would, rather than listing them for deletion:

```
INFO: would take over 2 slots recorded for a clone no longer at its path: 1f43c7cc1585, c53ca230ae36
```

Ownership of a lease is asked of git, not of the path. A clone moved away and another cloned where it was leaves the path existing, and the two kinds of slot part ways:

* **The moved clone's free slots** are recorded for that path and hold no checkout, so they go to the new clone, warm bases and all: a free slot is anybody's.
* **Its leased slots** stay the moved clone's. Only the moved clone's git lists their checkouts, so they go back to it, and the new clone, which cannot read them, is never handed them as its own. Such a slot is recorded for the new clone's path while the new clone's git has no worktree there. From the new clone, `add`, `release` and `path` refuse it with `slot_stranded`, and [`aspect worktree list`](/docs/cli/worktrees/list) tags it `another clone's`. The next `add`, `release`, `path` or `prune` run from the moved clone takes it back.

From the new clone's side that refusal has two readings, and the message gives both: the checkout of a clone that was here and moved, or this clone's own checkout, damaged. If the clone moved, run the command from where it is now; if the checkout is this clone's, `git worktree repair <path>` from here may make it readable. Failing both, `aspect worktree prune <slot> --force=all` deletes it once you have copied out anything you need, refused while the session holding it is still running.

What is left is **stranded**: a leased slot whose clone is no longer at its path and that no clone here lists — deleted, or moved somewhere this clone cannot see — and, outside any clone, where nothing is taken over, any slot of a clone that is gone. A stranded slot is deleted by naming it, whatever its age — and so is a slot recorded for this clone's path that this clone's git does not list. It takes `--force=all` when its checkout holds work, and a checkout git cannot read always does, which is what a leased slot of a clone that is gone has. A leased one whose session is still running is refused with `held_by_another_session`, `--force=all` or not: a session at work in it says the clone moved rather than went, and a command run from where it is now takes the slot back. One whose process was never captured cannot be shown to have stopped, so it is refused the same way unless `--force=all` is given. Both are asked before whether a Bazel server holds the slot. Whether the holder runs is asked again once a prompt is answered: a named stranded lease whose holder started running again meanwhile is not deleted, and `prune` exits 1.

**From outside any clone**, `prune` takes a slot id, a unique prefix of one of at least four characters, or a slot's directory, as `aspect worktree list --all` prints it, and finds the pool on disk. That is the way to delete slots for a repository you no longer have a clone of. With nothing named it refuses, since there is no pool to sweep:

```
$ aspect worktree list --all
...
  1 slot is stranded: the clone that owned it is no longer at its path, and no clone here can
  take it. If that clone moved, any command run there takes its slots back. If it was deleted,
  `aspect worktree prune <slot>` deletes one, run from a clone of the repository or from
  outside any clone; a leased one takes `--force=all` once you have copied out anything you
  need: 0e470b08e6fc

$ aspect worktree prune 0e470b08e6fc --force=all
```

A checkout whose clone is gone cannot be read by git, so uncommitted work in it cannot be ruled out: deleting it takes `--force=all`, after you have copied out anything you need.

A second clone of the same repository that still exists keeps its own slots: it shares the pool directory but not the slots in it, and reclaiming them is not this clone's call. Naming one says so rather than pretending not to find it:

```
ERROR: slot df01c9d286a9 belongs to another clone of this repository, at /Users/you/src/repo2,
which still exists — it is that clone's to manage. Run this there, or let
`aspect worktree prune` collect the slot once that clone is gone.
```

## Refusals

| `error` | Exit | Meaning |
| - | - | - |
| `slot_in_use` | 1 | The named slot holds a worktree. The message names `release` for ending the lease instead |
| `worktree_dirty` | 1 | The named slot still holds a checkout with uncommitted work — or one git cannot read (`UR`), such as a repository of its own at the slot's path, a plain file there, or a checkout whose git reads another directory as its work tree, said as such — which is listed. `--force=all` deletes it with the slot |
| `slot_warm` | 1 | The named slot is free but a Bazel server still holds its output base. Refused however it was asked for. Retryable: the server exits on Bazel's idle timeout, and the message also offers `aspect gc <hash>` |
| `held_by_another_session` | 1 | The named slot is stranded and leased by a session that is still running, so its clone most likely moved: run the command from where that clone is now, which takes the slot back. `--force=all` does not get past that. A lease whose process was never captured (pid 0) cannot be shown to have stopped, and is refused the same way unless `--force=all` is given. Asked before the Bazel-server check |
| `owned_elsewhere` | 1 | The named slot belongs to another clone of this repository that still exists |
| `needs_force` | 1 | There is no terminal to confirm at. `--force` proceeds, `--dry-run` looks |
| `no_such_worktree` | 1 | Nothing in this pool answers to that name — or, outside a clone, no pool on this machine has that slot; a branch name names a slot only inside a clone of its repository. An id prefix that fits several slots is refused with them listed |
| `slot_stranded` | 1 | The branch named is in a slot whose clone is gone. Name the slot id the message gives; the message adds `--force=all` for a leased one, whose checkout git cannot read. |
| `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. `--dry-run` still reads it |
| `invalid_agent` | 1 | `--agent-kind` / `ASPECT_AGENT_KIND` is not a name, or `--agent-id` / `ASPECT_AGENT_ID` is all spaces, longer than 128 characters, or has a character a terminal would act on or hide: a 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. 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. Fix the value |
| `not_a_repository` | 1 | Outside a clone with no slot named |
| `slot_setting_up` | 1 | The named slot is still being checked out by an `add`. Retryable; a slot whose `add` has stopped is reclaimed instead |
| `pool_busy` | 75 | Another call holds the pool lock. Retryable |

## Flags

<ParamField path="target" type="string">
  A branch, a slot id or a unique prefix of one (four or more characters), or a slot's directory to delete, as `aspect worktree list` shows them. How long it has been idle is not consulted — naming it is the decision. Leave it out to work by idle time across every free slot.
</ParamField>

<ParamField path="--max-idle-days" type="int" default="30">
  Delete free slots untouched for at least this many days, when no slot is named. `0` considers every free slot.
</ParamField>

<Note>
  "Untouched" is the newer of two things: Bazel's last activity in the slot's output base — the newest mtime among the files Bazel writes as it works, the evidence `aspect gc` measures a base by — and the slot's last lease, taken or ended. It is what [`aspect worktree list`](/docs/cli/worktrees/list) shows as `last used`, and never reaches back before the slot was created. Because a lease counts here and not to `gc`, a slot leased recently whose base Bazel has not touched in a month can have that base removed by `gc` while `prune` keeps the slot: it goes cold, not away.
</Note>

<ParamField path="--dry-run" type="boolean" default="false">
  Report what would be deleted and delete nothing.
</ParamField>

<ParamField path="--force" type="false | true | all" default="false">
  Delete without asking. `true`, which bare `--force` means, is required when stdin is not a terminal, so a script or an agent has to opt in rather than have a prompt answered on its behalf. `all` also deletes a named slot whose checkout holds uncommitted work, which is otherwise refused with the files listed — the same two grades as [`release --force`](/docs/cli/worktrees/release).
</ParamField>

<ParamField path="--agent-kind, --agent-id" type="string" default="">
  The session asking, as given to `add` with the same flags. Needed only when the lease was taken with them, so this command recognises it as yours; detected automatically otherwise.
</ParamField>

<ParamField path="--output" type="text | json" default="text">
  `text` writes the report to stderr and leaves stdout empty. `json` writes one document to stdout.
</ParamField>

## JSON output

`candidates` is what a run would delete, `taken_over` the slots of a moved or deleted clone it took over (or, with `--dry-run`, would), `kept` what it spared and why, and `declined` says whether you were asked and said no — which an empty `pruned` on its own cannot tell you. `candidates` is reported whether or not anything was deleted, so a dry run says what it would do rather than only that it did nothing. A candidate's `idle_days` is `null` for a slot with no recorded age, and `leased` is the branch a stranded slot still holds a lease on, empty for a free one. A leased candidate's `last_branch` may be empty, since the slot's last branch is recorded when a lease ends: read the name from `leased`.

```json theme={null}
{
  "schema_version": 1,
  "pool": "github.com/acme/web",
  "dry_run": false,
  "max_idle_days": 30,
  "candidates": [
    {
      "slot": "0b8751be74ee",
      "path": "/Users/you/.aspect/worktrees/github.com/acme/web/aspect-worktree-0b8751be74ee",
      "idle_days": 41,
      "last_branch": "old-experiment",
      "leased": "",
      "reason": "idle 41d"
    }
  ],
  "taken_over": [],
  "pruned": [
    "/Users/you/.aspect/worktrees/github.com/acme/web/aspect-worktree-0b8751be74ee"
  ],
  "kept": [],
  "declined": false
}
```

## Relationship to `aspect gc`

The two reclaim different things, and neither makes the other redundant.

`aspect gc` never treats a pooled slot as **orphaned**. A free slot looks exactly like an abandoned worktree, because the worktree is gone on purpose and that is what keeps the server warm, so the rule that reaps orphans skips pool slots — except one nothing will lease again, its directory gone and its pool holding no record of it. It does still collect them by idle time: a pooled base unused past `--base-max-idle-days` (30 by default) is removed like any other idle base, which leaves the slot in place and reading `cold`.

`aspect worktree prune` removes the **slot**: its worktree, its registry entry and its output base together. That is why it deletes the base itself rather than leaving it for `gc` — a prune that reclaimed no disk and told you to go run something else would not be a prune.

```shell theme={null}
aspect output-bases          # every base, pool slots grouped and attributed
aspect gc                    # reclaim orphaned and idle bases
aspect worktree prune        # retire whole slots
```

`aspect output-bases` lists every Bazel output base on the machine, pooled or not, and `aspect gc` reclaims the idle ones.


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