# Manage and remove workspaces

> List children, trace ancestors, remove created rifts to trash, unregister source roots with `-f`, `--children` mode, and `rift gc` physical deletion.

- Repository: anomalyco/rift
- GitHub: https://github.com/anomalyco/rift
- Human docs: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662
- Complete Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/llms-full.txt

## Source Files

- `README.md`
- `specs.md`
- `crates/cli/src/main.rs`
- `crates/core/src/lib.rs`
- `crates/core/src/registry.rs`

---

---
title: "Manage and remove workspaces"
description: "List children, trace ancestors, remove created rifts to trash, unregister source roots with `-f`, `--children` mode, and `rift gc` physical deletion."
---

`rift list`, `rift ancestors`, `rift remove`, and `rift gc` operate on the SQLite registry and filesystem together. Each command resolves the target workspace by searching upward from the current working directory (or an explicit path) for a `.rift` marker, then reads or updates active and trash records. Removal is logical: directories move into adjacent `.trash` storage until `rift gc` deletes them physically.

## Workspace lifecycle

Rift tracks two registry tables: `rift` for active workspaces and `trash` for logically removed ones. A created workspace stays active until `remove` moves it (and any nested descendants) into trash. A registered source root stays on disk when unregistered; only its `.rift` marker and active registry records are cleared.

```mermaid
stateDiagram-v2
    [*] --> Active: create or init
    Active --> Trash: remove / remove --children
    Trash --> [*]: gc
    Active --> Unregistered: remove -f on source root
    Unregistered --> [*]
```

| State | On disk | In `rift` table | In `trash` table |
| --- | --- | --- | --- |
| Active created rift | `<storage>/<name>/` with `.rift` | Yes | No |
| Active source root | Source directory with `.rift` | Yes (`parent_id = NULL`) | No |
| Trashed rift | `<storage>/.trash/<id>-<name>/` | No | Yes |
| Unregistered source | Source directory, no `.rift` | No | Descendants may be in trash |

Default storage for created workspaces is a sibling `.rifts/<workspace-name>/` directory. Trash lives beside active storage at `.trash/` under the same storage parent, so custom `--into` paths keep trash on the same filesystem.

```text
~/code/app/                              source root
~/code/.rifts/app/parser-fix/            active created rift
~/code/.rifts/app/.trash/01H...-parser-fix/   trashed rift (after remove)
```

## List direct children

`rift list` returns only **direct** active children of the resolved workspace — not nested grandchildren. Results are ordered by `created_at`, then `id`.

<RequestExample>

```bash
cd ~/code/.rifts/app/parser-fix
rift list
```

</RequestExample>

<RequestExample>

```bash
rift list ~/code/app
```

</RequestExample>

<ResponseExample>

```text
/home/you/code/.rifts/app/parser-fix
/home/you/code/.rifts/app/schema-work
```

</ResponseExample>

<ParamField body="of" type="path">
Optional workspace path. Defaults to the current working directory. Rift walks upward to the nearest `.rift` marker.
</ParamField>

`list` does not recurse the provenance tree. To see nested descendants, run `rift list` from each child workspace, or use `rift ancestors` from a deeper rift to walk upward.

<Note>
If no `.rift` marker is found, the command fails with guidance to run `rift init`. If a registry entry exists but the marker was deleted, Rift reports a missing marker and suggests `rift init` to restore it.
</Note>

## Trace ancestors

`rift ancestors` walks the `parent_id` chain from the resolved workspace to the registered source root. Output is ordered **nearest parent first**, ending at the root.

<RequestExample>

```bash
cd ~/code/.rifts/app/parser-fix/deep-nested
rift ancestors
```

</RequestExample>

<ResponseExample>

```text
/home/you/code/.rifts/app/parser-fix
/home/you/code/app
```

</ResponseExample>

<ParamField body="of" type="path">
Optional workspace path. Defaults to the current working directory.
</ParamField>

An empty result means the resolved workspace is the registered source root (`parent_id = NULL`). Ancestors are useful for understanding provenance after creating rifts from other rifts, and for shell integration that needs a parent directory after removing the current rift.

## Remove workspaces

`rift remove` has two core behaviors depending on whether the target is a **created rift** or the **registered source root**.

### Remove a created rift

When `at` resolves to a workspace with a non-null `parent_id`, `remove` moves the **entire descendant subtree** (deepest-first) into trash. The source directory is unaffected.

<Steps>
<Step title="Change into the rift to remove">

```bash
cd ~/code/.rifts/app/parser-fix
```

</Step>

<Step title="Run remove">

```bash
rift remove
```

</Step>

<Step title="Verify">

```bash
rift list ~/code/app
```

The removed path no longer appears. Its data now lives under `.trash/<id>-<name>/` until garbage collection.

</Step>
</Steps>

Removing a parent rift also trashes all nested children created from it. After removal, `rift list` on the original source root no longer includes any member of that subtree.

### Unregister a source root (`-f`)

When `at` resolves to the registered source root (no ancestors), `remove` **unregisters** the workspace:

- The source directory stays on disk.
- The `.rift` marker is deleted.
- Each **existing** registered descendant is moved to trash.
- Descendants already absent from disk are tolerated (registry cleanup proceeds).
- The root's active registry record is deleted.

<Warning>
Unregistering a source root is destructive to Rift metadata and all child rifts. The CLI requires explicit confirmation.
</Warning>

<RequestExample>

```bash
rift remove -f ~/code/app
```

</RequestExample>

<ResponseExample>

```text
Unregistered  /home/you/code/app
```

</ResponseExample>

Without `-f` (or `--force`), the CLI refuses root unregistration:

```text
This is the root workspace.

Unregistering it removes Rift metadata and trashes all child rifts.
Run `rift remove -f` to continue.
```

<ParamField body="at" type="path">
Optional workspace path. Defaults to the current working directory.
</ParamField>

<ParamField body="-f, --force" type="flag">
Required when removing a registered source root. Not enforced by the core library or FFI bindings.
</ParamField>

### Remove descendants only (`--children`)

`rift remove --children` preserves the selected workspace and trashes every managed descendant. In the core API and JavaScript bindings, this mode is `all: true`.

<RequestExample>

```bash
rift remove --children ~/code/app
```

</RequestExample>

<ResponseExample>

```text
/home/you/code/.rifts/app/parser-fix
/home/you/code/.rifts/app/schema-work
```

</ResponseExample>

Each removed path is printed to stdout, one per line. The source root (or selected intermediate rift) remains active with an empty child list.

`--children` works on any managed workspace, including the source root. When run from a nested rift, only descendants of that rift are trashed; the selected rift itself is preserved.

## Trash mechanics

Removal never deletes data immediately. For each rift in the subtree scope, Rift:

1. Verifies the directory exists and its `.rift` marker matches the registry.
2. Renames `<storage-parent>/<name>` → `<storage-parent>/.trash/<id>-<name>`.
3. Deletes active `rift` records and inserts matching `trash` records in one transaction.

The ULID prefix in trash paths prevents name collisions when multiple rifts shared a directory name across removal cycles.

<Info>
Subtree moves are deepest-first. If a filesystem rename fails partway through, earlier moves are rolled back to their original locations.
</Info>

### Removal constraints

| Condition | Error | Behavior |
| --- | --- | --- |
| Recorded descendant path missing on disk | `missing_rift` | Removal refused; active tree may be out of sync with the filesystem |
| `.rift` marker does not match registry | `marker_mismatch` | Removal refused |
| Unknown ULID in marker | `unknown_marker` | Removal refused |
| Trash target path already exists | `already_exists` | Removal refused |

If a descendant was moved outside Rift's storage (for example with `mv`), removal of its parent fails with `missing_rift` until the filesystem and registry are reconciled.

## Garbage collection (`rift gc`)

`rift gc` performs **physical deletion** of trashed directories and prunes stale active records. It does not touch active workspaces.

<RequestExample>

```bash
rift gc
```

</RequestExample>

<ResponseExample>

```text
/home/you/code/.rifts/app/.trash/01HXYZ-parser-fix
```

</ResponseExample>

Each deleted path is printed to stdout, one per line. An empty result means nothing was eligible for collection.

### Trash deletion

For each `trash` record, `gc`:

1. Deletes the filesystem directory if it still exists (platform strategy handles btrfs subvolumes, reflink trees, and ordinary directories).
2. Removes the `trash` registry record regardless of whether the directory was already gone.

On btrfs, Rift attempts immediate subvolume deletion via `BTRFS_IOC_SNAP_DESTROY`. If permissions deny deleting a populated subvolume, it empties contents first, then removes the empty subvolume.

### Active-record pruning

`gc` also scans active `rift` records whose paths no longer exist on disk. A missing active record is pruned **only when no recorded descendant still exists on the filesystem**. This prevents orphaning a surviving nested rift whose parent directory was deleted externally.

| Scenario | `gc` behavior |
| --- | --- |
| Trashed directory on disk | Delete directory, remove trash record, print path |
| Trashed directory already gone | Remove trash record, print path |
| Active path missing, no descendants on disk | Remove active record, print original path |
| Active parent missing but child still on disk | No prune; registry preserved |

<Check>
Run `rift gc` after `remove` when you want disk space back. Trashed rifts continue to consume storage until garbage collection runs.
</Check>

## CLI output and shell integration

| Command | stdout | stderr |
| --- | --- | --- |
| `rift list` | One child path per line | — |
| `rift ancestors` | One ancestor path per line (nearest first) | — |
| `rift remove` (created rift) | Empty | — |
| `rift remove -f` (source root) | Empty | `Unregistered  <path>` |
| `rift remove --children` | One removed path per line | — |
| `rift gc` | One deleted/pruned path per line | — |

With shell integration (`eval "$(rift shell-init zsh)"`), the wrapper passes `--shell-cwd` to `remove`. When the current directory is inside a removed rift, Rift prints the parent workspace path (or the source root after unregistration) to stdout so the shell can `cd` there automatically.

## JavaScript API

The `rift-snapshot` package exposes the same core semantics. Path defaults match the CLI.

```ts
import { list, ancestors, remove, gc } from "rift-snapshot";

// Direct children of the resolved workspace
const children = list({ of: "/home/you/code/app" });

// Nearest parent first
const lineage = ancestors({ of: "/home/you/code/.rifts/app/parser-fix" });

// Trash a created rift (no -f required in FFI)
remove({ at: "/home/you/code/.rifts/app/parser-fix" });

// Unregister source root — no force flag in FFI
remove({ at: "/home/you/code/app" });

// Trash descendants, preserve the selected workspace
const removed = remove({ at: "/home/you/code/app", all: true });

// Physical deletion
const deleted = gc();
```

<ParamField body="of" type="string">
Workspace to query. Defaults to `process.cwd()`.
</ParamField>

<ParamField body="at" type="string">
Workspace to remove. Defaults to `process.cwd()`.
</ParamField>

<ParamField body="all" type="boolean">
When `true`, maps to `rift remove --children`. Returns the array of trashed paths.
</ParamField>

<ParamField body="database" type="string">
Optional override for the SQLite registry path (hidden CLI flag `--database` uses the same location).
</ParamField>

Operation failures throw `RiftError` with a `code` (for example `missing_rift`, `marker_mismatch`, `workspace_not_initialized`) and an optional `path`.

<Tip>
The `-f` guard is CLI-only. Programmatic callers using `remove({ at: sourceRoot })` can unregister a source root without a separate confirmation step. Build your own guard if your application needs it.
</Tip>

## Common failure modes

**`no initialized workspace found`** — No `.rift` marker exists in any ancestor of the supplied path. Initialize with `rift init` first.

**`this workspace is missing its .rift marker`** — The directory is registered in SQLite but the marker file was deleted. Run `rift init` on the root to restore it.

**`cannot remove subtree while a recorded rift path is missing`** — A descendant in the registry no longer exists at its recorded path (manual deletion or external move). Restore the directory or prune via `rift gc` if no descendants remain on disk.

**`rift marker does not match the registry`** — The `.rift` ULID does not match the registry entry for that path. Do not manually edit markers.

**`This is the root workspace` (CLI only)** — Attempted to unregister a source root without `-f`. Re-run with `rift remove -f`.

## Related pages

<CardGroup>
<Card title="Workspaces and registry" href="/workspaces-and-registry">
ULID identity, `.rift` markers, SQLite schema, and parent-child provenance.
</Card>
<Card title="Storage layout" href="/storage-layout">
Default `.rifts` paths, custom `--into` storage, trash naming, and database location.
</Card>
<Card title="Create workspaces" href="/create-workspaces">
Create child workspaces that `list` and `remove` operate on.
</Card>
<Card title="CLI reference" href="/cli-reference">
Full subcommand inventory, flags, stdout/stderr behavior, and exit codes.
</Card>
<Card title="Shell integration" href="/shell-integration">
Automatic `cd` after `remove` via `rift shell-init`.
</Card>
<Card title="Error codes" href="/error-codes">
Complete `RiftError` catalog including `missing_rift` and `marker_mismatch`.
</Card>
</CardGroup>
