# Rift Documentation

> Reference for Rift copy-on-write development workspaces: CLI commands, JavaScript FFI API, registry metadata, platform copy backends, and workspace lifecycle operations.

## Context Links

- [Agent index](https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/llms.txt)
- [Human interactive docs](https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662)
- [GitHub repository](https://github.com/anomalyco/rift)

## Repository Metadata

- Repository: anomalyco/rift

- Generated: 2026-06-17T23:36:20.738Z
- Updated: 2026-06-18T05:51:33.347Z
- Runtime: Grok CLI
- Format: Documentation
- Pages: 19

## Page Index

- 01. [Overview](https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/01-overview.md) - What Rift exposes (CLI, npm package, FFI), supported platforms and backends, and the shortest path from init to create.
- 02. [Installation](https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/02-installation.md) - Install via npm/bun global package or Cargo build script; platform support matrix and prebuilt binary layout.
- 03. [Quickstart](https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/03-quickstart.md) - Initialize a source workspace, create a filtered copy-on-write child, list it, and remove it with expected stdout signals.
- 04. [Workspaces and registry](https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/04-workspaces-and-registry.md) - Managed workspace model: ULID identity, `.rift` markers, SQLite registry schema, parent-child provenance tree, and upward resolution.
- 05. [Copy strategies and platforms](https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/05-copy-strategies-and-platforms.md) - Platform-specific copy-on-write backends (btrfs subvolumes, Linux reflinks, APFS clonefile), filtered vs exact copy modes, and unsupported platforms.
- 06. [Storage layout](https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/06-storage-layout.md) - Default `.rifts` sibling storage, custom `--into` paths, trash relocation naming, and central SQLite database location.
- 07. [Git integration](https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/07-git-integration.md) - Git repository detection, detached HEAD behavior, marker exclusion, unsafe source states, and preserved index/working-tree semantics.
- 08. [Initialize a workspace](https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/08-initialize-a-workspace.md) - Run `rift init` with Git-root vs `--here` selection, btrfs subvolume conversion progress, marker restoration, and success signals.
- 09. [Create workspaces](https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/09-create-workspaces.md) - Create child workspaces with `--name`, `--into`, filtered vs `--copy-all` copy, `--no-hooks`, random name generation, and Git preparation.
- 10. [Manage and remove workspaces](https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/10-manage-and-remove-workspaces.md) - List children, trace ancestors, remove created rifts to trash, unregister source roots with `-f`, `--children` mode, and `rift gc` physical deletion.
- 11. [Shell integration](https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/11-shell-integration.md) - Install Bash, Zsh, or Nushell wrappers via `rift shell-init` for automatic `cd` after init, create, and remove operations.
- 12. [Postcreate hooks](https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/12-postcreate-hooks.md) - Configure `.rift.toml` v1 postcreate hooks, hook environment variables, sequential execution, and failure behavior after workspace registration.
- 13. [CLI reference](https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/13-cli-reference.md) - All `rift` subcommands, flags, defaults, stdout/stderr behavior, hidden `--database` and `--shell-cwd` flags, and exit codes.
- 14. [JavaScript API reference](https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/14-javascript-api-reference.md) - `rift-snapshot` exports, Bun vs Node conditional bindings, FFI request protocol, function signatures, and Node.js 26.1+ FFI requirements.
- 15. [Configuration reference](https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/15-configuration-reference.md) - `.rift.toml` schema: `version`, `hooks.postcreate` array, validation rules, and CLI/API hook skip flags.
- 16. [Error codes](https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/16-error-codes.md) - Complete `RiftError` code catalog from FFI and TypeScript bindings, CLI user-facing messages, and path-bearing error variants.
- 17. [Troubleshooting](https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/17-troubleshooting.md) - Common failure modes: uninitialized workspaces, missing markers, CoW unavailable, unsafe Git state, hook failures, and platform limitations.
- 18. [Development](https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/18-development.md) - Build and test the Rust workspace, install the CLI locally, CI test matrix, and filesystem integration test environment variables.
- 19. [Benchmarking](https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/19-benchmarking.md) - Measure `rift create` performance with `create` and `compare` Cargo benches, sample counts, JSON output schema, and candidate comparison workflow.

## Source File Index

- `.github/workflows/ci.yml`
- `.github/workflows/release.yml`
- `Cargo.toml`
- `crates/cli/src/main.rs`
- `crates/cli/tests/filesystem_e2e.rs`
- `crates/core/benches/AUTORESEARCH.md`
- `crates/core/benches/compare.rs`
- `crates/core/benches/create.rs`
- `crates/core/Cargo.toml`
- `crates/core/src/config.rs`
- `crates/core/src/filter.rs`
- `crates/core/src/git.rs`
- `crates/core/src/hook.rs`
- `crates/core/src/id.rs`
- `crates/core/src/lib.rs`
- `crates/core/src/marker.rs`
- `crates/core/src/name.rs`
- `crates/core/src/registry.rs`
- `crates/core/src/strategy/apfs.rs`
- `crates/core/src/strategy/btrfs.rs`
- `crates/core/src/strategy/linux.rs`
- `crates/core/src/strategy/mod.rs`
- `crates/core/src/strategy/reflink.rs`
- `crates/ffi/src/lib.rs`
- `npm/rift-snapshot/bin/rift.js`
- `npm/rift-snapshot/bun/index.js`
- `npm/rift-snapshot/index.d.ts`
- `npm/rift-snapshot/node/index.js`
- `npm/rift-snapshot/package.json`
- `README.md`
- `scripts/ci/linux-fs.sh`
- `scripts/install.sh`
- `specs.md`

---

## 01. Overview

> What Rift exposes (CLI, npm package, FFI), supported platforms and backends, and the shortest path from init to create.

- Page Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/01-overview.md
- Generated: 2026-06-17T23:32:08.448Z

### Source Files

- `README.md`
- `specs.md`
- `Cargo.toml`
- `crates/core/src/lib.rs`
- `npm/rift-snapshot/package.json`

---
title: "Overview"
description: "What Rift exposes (CLI, npm package, FFI), supported platforms and backends, and the shortest path from init to create."
---

Rift is a Rust workspace (`rift` core, `rift-cli`, `rift-ffi`) that registers source directories as managed workspaces, clones them with platform copy-on-write backends, and tracks parent-child provenance in a central SQLite registry. Consumers reach the same semantics through the `rift` CLI binary, the `rift-snapshot` npm package (CLI shim plus Bun/Node FFI bindings), or direct use of the `rift` Rust crate.

<Warning>
This repository is experimental. Behavior, interfaces, and implementation details may change without notice.
</Warning>

## What Rift provides

Rift replaces heavyweight full-directory copies (and complements Git worktrees) with instant, space-efficient workspace clones. A registered source workspace gets a `.rift` marker and a ULID identity in SQLite; each `create` operation produces a filtered or exact copy-on-write child, records the immediate parent, and optionally runs `.rift.toml` postcreate hooks.

Four interfaces share one implementation and metadata model:

| Interface | Artifact | Role |
| --------- | -------- | ---- |
| Native library | `rift` crate (`crates/core`) | Core API: `Manager`, `init`, `create`, `remove`, `list`, `ancestors`, `gc` |
| CLI | `rift` executable (`crates/cli`) | Shell commands, Git-root/`--here` selection, progress output, shell integration |
| Bun FFI | `rift-snapshot` → `bun/index.js` | `dlopen` of `librift_ffi` / `rift_ffi.dll`; JSON request protocol |
| Node FFI | `rift-snapshot` → `node/index.js` | Node 26.1+ experimental `node:ffi`; same JSON protocol |

The npm launcher publishes as **`rift-snapshot`** (version `0.0.10` at time of writing). It installs a `rift` bin shim that spawns a bundled prebuilt executable from `prebuilds/<platform>-<arch>/`. Conditional exports route `import "rift-snapshot"` to the Bun or Node binding. No install lifecycle scripts are required.

```text
  Application / shell                npm rift-snapshot
        |                                  |
        v                                  v
   rift CLI (clap)              bin/rift.js → prebuilds/.../rift
        |                                  |
        +----------+    JSON FFI     +------+------+
                   |  rift_ffi_call  |             |
                   v                 v             v
              rift-ffi (cdylib)   bun/index.js  node/index.js
                   |
                   v
              rift core (Manager)
                   |
         +---------+---------+
         |         |         |
    Strategy   SQLite     .rift markers
   (CoW copy)  registry   (ULID per workspace)
```

## Platform and backend support

Prebuilt npm artifacts target four platform-arch pairs: `linux-x64`, `darwin-x64`, `darwin-arm64`, and `windows-x64`. Workspace **creation** requires a production copy-on-write strategy on the host OS and filesystem.

| Platform | Backend | `init` behavior | `create` behavior |
| -------- | ------- | ----------------- | ----------------- |
| Linux x64 on btrfs | `BtrfsStrategy` | Converts ordinary directories into btrfs subvolumes via reflink import; registers existing subvolumes in place | Exact: writable subvolume snapshots; filtered: reflink import of included paths |
| Linux x64 on reflink-capable FS (e.g. XFS) | `LinuxReflinkStrategy` | Verifies native reflink (`FICLONE`) support; registers in place | Per-file reflink tree clone |
| macOS arm64 / x64 (APFS) | `ApfsStrategy` | Registers source directory | Exact: `clonefile` directory clone; filtered: per-entry clone |
| Windows x64 | `UnsupportedStrategy` | Package publishes; CoW cloning not implemented | Fails with `cow_unavailable` |

Linux selects btrfs vs reflink at runtime via `statfs` magic detection. macOS always uses `ApfsStrategy`. Other platforms compile `UnsupportedStrategy`, which rejects `create` with no byte-copy fallback.

<Note>
On btrfs, `create` requires the source to already be a subvolume. If `from` is an ordinary btrfs directory, run `rift init` first.
</Note>

## Shortest path: init to create

The minimal CLI workflow registers a source workspace, then clones it. Defaults use the current working directory, filtered copy (excluding regenerable artifacts like `node_modules` and `target`), and adjacent `.rifts/<workspace-name>/` storage.

<Steps>
<Step title="Install">

<CodeGroup>
```bash title="npm"
npm install -g rift-snapshot
```

```bash title="bun"
bun add -g rift-snapshot
```

```bash title="Cargo (from source)"
./scripts/install.sh
```
</CodeGroup>

The Cargo script installs an optimized `rift` binary to `${CARGO_HOME:-$HOME/.cargo}/bin/rift`. Release archives are also published on [GitHub Releases](https://github.com/anomalyco/rift/releases/latest).

</Step>

<Step title="Initialize the source workspace">

```bash
cd ~/code/app
rift init
```

`rift init` walks upward from the current directory. Unless `--here` is passed, it selects the nearest existing managed ancestor, or the nearest Git root when no Rift root exists. Core `init` then registers the directory, writes a `.rift` marker (ULID), and on Linux btrfs may convert an ordinary directory into a subvolume.

**Success signals (stderr unless noted):**

- First-time btrfs conversion: progress lines (`Creating BTRFS subvolume...`, `Importing workspace...`), then `Ready  <path>`
- Already registered: `Already initialized  <path>`
- Restored deleted marker: `Restored marker  <path>`

</Step>

<Step title="Create a child workspace">

```bash
rift create
# or
rift create --name parser-fix
```

`rift create` resolves upward to the nearest `.rift` marker, copy-on-write clones the managed workspace, inserts a registry child record, prepares Git state (detached `HEAD` when applicable), and prints the **new workspace path to stdout**.

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

Filtered copy omits heavyweight dependency and build artifacts while preserving manifests, lockfiles, and dirty working-tree state. Use `--copy-all` for exact copies including `node_modules`, `target`, and similar paths.

</Step>

<Step title="Verify and clean up">

```bash
rift list          # direct children of current workspace
rift ancestors     # parent chain, nearest first
rift remove        # move current created rift to .trash
rift gc            # physically delete trashed workspaces
```

</Step>
</Steps>

## JavaScript API surface

The `rift-snapshot` package exposes the same operations as the CLI core, with path defaults to `process.cwd()`:

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

init({ at: "/path/to/app" });
const child = create({ from: "/path/to/app", name: "schema-work" });
console.log(list({ of: "/path/to/app" }));
remove({ at: child });
gc();
```

<ParamField body="init" type="(options?: { at?: string; database?: string }) => null">
Initializes exactly `at`. Git-root selection and `--here` are CLI-only behavior.
</ParamField>

<ParamField body="create" type="(options?: CreateOptions) => string">
Returns the new workspace path. `copyAll` and `hooks` default to filtered copy and running postcreate hooks.
</ParamField>

Failures throw `RiftError` with a `code` (e.g. `cow_unavailable`, `workspace_not_initialized`, `hook_failed`) and optional `path`.

<Tabs>
<Tab title="Bun">

Conditional export `bun` loads `librift_ffi` via `bun:ffi` `dlopen`. No extra runtime flags.

</Tab>
<Tab title="Node.js">

Requires Node.js **26.1+** with the experimental FFI API:

```bash
node --experimental-ffi app.mjs
```

With Node's permission model, also pass `--allow-ffi`.

</Tab>
</Tabs>

## Registry and storage at a glance

Every managed workspace has a `.rift` file at its root containing its ULID. A central SQLite database at the platform user data directory (`<data-local>/rift/rift.sqlite`) stores `id`, `parent_id`, `path`, and trash records. Workspace operations locate their root by searching upward for `.rift`.

Default child storage sits beside the registered source root:

```text
~/code/app/                         source workspace
~/code/.rifts/app/parser-fix/       created workspace
~/code/.rifts/app/.trash/           removed workspace storage
```

Custom destinations use `rift create --into` or the `into` API field.

## CLI command inventory

| Command | Purpose |
| ------- | ------- |
| `rift init` | Register (and possibly convert) a source workspace |
| `rift create` | Copy-on-write clone with optional `--name`, `--into`, `--copy-all`, `--no-hooks` |
| `rift list` | List direct child workspaces |
| `rift ancestors` | List parent workspaces, nearest first |
| `rift remove` | Trash a created rift, or unregister a source root with `-f` |
| `rift remove --children` | Trash descendants while preserving the selected workspace |
| `rift gc` | Physically delete trashed workspaces and prune missing registry entries |
| `rift shell-init <bash\|zsh\|nushell>` | Emit shell wrapper for auto-`cd` after init/create/remove |

Hidden global flags `--database` and `--shell-cwd` support testing and shell integration.

## Related pages

<CardGroup>
<Card title="Installation" href="/installation">
Install via npm, bun, Cargo, or GitHub release archives; prebuilt binary layout.
</Card>
<Card title="Quickstart" href="/quickstart">
End-to-end init, create, list, and remove with expected stdout signals.
</Card>
<Card title="Copy strategies" href="/copy-strategies">
Btrfs subvolumes, Linux reflinks, APFS clonefile, filtered vs exact modes.
</Card>
<Card title="JavaScript API" href="/javascript-api">
FFI request protocol, exports, and Node.js 26.1+ requirements.
</Card>
<Card title="CLI reference" href="/cli-reference">
All subcommands, flags, defaults, and exit behavior.
</Card>
</CardGroup>

---

## 02. Installation

> Install via npm/bun global package or Cargo build script; platform support matrix and prebuilt binary layout.

- Page Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/02-installation.md
- Generated: 2026-06-17T23:31:57.097Z

### Source Files

- `README.md`
- `npm/rift-snapshot/package.json`
- `npm/rift-snapshot/bin/rift.js`
- `scripts/install.sh`
- `.github/workflows/release.yml`

---
title: "Installation"
description: "Install via npm/bun global package or Cargo build script; platform support matrix and prebuilt binary layout."
---

The `rift-snapshot` npm package ships a Node CLI shim (`bin/rift.js`) that resolves and executes a platform-specific prebuilt `rift` binary from `prebuilds/<platform>-<arch>/`. The same package bundles FFI shared libraries for Bun and Node conditional exports. No install lifecycle scripts run during `npm install`; artifacts are selected at runtime from the published tarball.

<Warning>
This repository is experimental. Behavior, interfaces, and implementation details may change without notice.
</Warning>

## Installation methods

<Tabs>
<Tab title="npm">

```bash
npm install -g rift-snapshot
```

The global `rift` command points at `bin/rift.js`, which spawns the bundled native executable with inherited stdio.

</Tab>
<Tab title="bun">

```bash
bun add -g rift-snapshot
```

Bun uses the same launcher shim and prebuilt CLI binary layout as npm.

</Tab>
<Tab title="Cargo">

From a repository checkout:

```bash
./scripts/install.sh
```

`scripts/install.sh` runs:

```bash
cargo install --path crates/cli --root "${CARGO_HOME:-$HOME/.cargo}" --force --locked
```

The optimized CLI lands at `${CARGO_HOME:-$HOME/.cargo}/bin/rift`. This path builds only `rift-cli`; it does not bundle FFI libraries for JavaScript.

</Tab>
<Tab title="GitHub Releases">

Release archives are published on version tags (`v*`) to [GitHub Releases](https://github.com/anomalyco/rift/releases/latest).

| Archive name pattern | Target triple | Contents |
| --- | --- | --- |
| `rift-<tag>-x86_64-unknown-linux-gnu.tar.gz` | `x86_64-unknown-linux-gnu` | `rift` |
| `rift-<tag>-x86_64-apple-darwin.tar.gz` | `x86_64-apple-darwin` | `rift` |
| `rift-<tag>-aarch64-apple-darwin.tar.gz` | `aarch64-apple-darwin` | `rift` |
| `rift-<tag>-x86_64-pc-windows-msvc.zip` | `x86_64-pc-windows-msvc` | `rift.exe` |

Each release includes `SHA256SUMS` checksums for all archives. Extract the archive and place the executable on your `PATH`.

</Tab>
</Tabs>

<Note>
The npm package name is temporarily `rift-snapshot`. When the `rift` npm name is available, only the launcher package name changes; the prebuild layout and shim behavior stay the same.
</Note>

## Platform support matrix

Rift publishes installable artifacts for four host targets. Workspace copy-on-write behavior depends on the host OS and filesystem, not only on whether installation succeeded.

| Host | Published artifacts | Copy-on-write backend | Workspace behavior after install |
| --- | --- | --- | --- |
| Linux x64 | npm prebuild `linux-x64`, GitHub `x86_64-unknown-linux-gnu` | Writable btrfs subvolumes or native per-file reflinks | `rift init` converts an ordinary btrfs directory into a subvolume, or verifies reflink support and registers in place on other Linux filesystems (including XFS when `FICLONE` succeeds). |
| macOS x64 | npm prebuild `darwin-x64`, GitHub `x86_64-apple-darwin` | APFS `clonefile` | `rift init` registers the source directory. |
| macOS arm64 | npm prebuild `darwin-arm64`, GitHub `aarch64-apple-darwin` | APFS `clonefile` | Same as macOS x64. |
| Windows x64 | npm prebuild `windows-x64`, GitHub `x86_64-pc-windows-msvc` | None implemented | The package installs and the CLI runs, but `create` fails with `cow_unavailable` because no copy-on-write strategy is implemented for Windows. |

<Warning>
Linux arm64 is not in the release or npm prebuild matrix. Although `package.json` lists `arm64` under `cpu`, there is no `prebuilds/linux-arm64/` directory in published packages. Installation on Linux arm64 fails at runtime when the shim cannot locate a binary.
</Warning>

On Linux, the production strategy selects btrfs or reflink backends at runtime based on the workspace filesystem. On macOS, `ApfsStrategy` handles cloning. On other host OS values (including Windows), `UnsupportedStrategy` rejects copy operations.

## npm package layout

The published `rift-snapshot` tarball contains the launcher, conditional FFI entry points, type definitions, and all prebuild directories.

```text
rift-snapshot/
├── bin/rift.js              # CLI shim: spawnSync prebuilt rift
├── bun/index.js             # Bun FFI binding
├── node/index.js            # Node experimental FFI binding
├── index.d.ts
└── prebuilds/
    ├── linux-x64/
    │   ├── rift
    │   └── librift_ffi.so
    ├── darwin-x64/
    │   ├── rift
    │   └── librift_ffi.dylib
    ├── darwin-arm64/
    │   ├── rift
    │   └── librift_ffi.dylib
    └── windows-x64/
        ├── rift.exe
        └── rift_ffi.dll
```

### Platform and architecture resolution

All launchers map `os.platform()` and `os.arch()` to prebuild directory names:

| `os.platform()` | Prebuild prefix | CLI binary | FFI library |
| --- | --- | --- | --- |
| `darwin` | `darwin-<arch>` | `rift` | `librift_ffi.dylib` |
| `linux` | `linux-<arch>` | `rift` | `librift_ffi.so` |
| `win32` | `windows-<arch>` | `rift.exe` | `rift_ffi.dll` |

Only `arm64` and `x64` architectures are accepted. Any other `platform-arch` pair exits the CLI shim with code `1` or throws in FFI bindings.

### Conditional exports

`package.json` routes JavaScript imports by runtime:

| Export condition | Entry | Native artifact |
| --- | --- | --- |
| `bun` | `./bun/index.js` | FFI library via `import` with `{ with: { type: "file" } }` |
| `node` | `./node/index.js` | FFI library via `node:ffi` `dlopen` |
| `default` | `./node/index.js` | Same as `node` |

The npm release workflow builds `rift-cli` and `rift-ffi` for every matrix target, copies both artifacts into the matching `prebuilds/<platform>/` directory with `scripts/prepare-npm-prebuild.mjs`, restores executable bits on Unix targets, and publishes with provenance. Crate and npm versions must match the release tag (currently `0.0.10` for tag `v0.0.10`).

### Constraints

- No `postinstall`, `preinstall`, or other lifecycle scripts. Native code is not compiled on the installing machine.
- `files` in `package.json` limits the published tarball to `bin`, `bun`, `node`, `index.d.ts`, and `prebuilds`.
- `os` and `cpu` fields declare `darwin` / `linux` / `win32` and `arm64` / `x64`, but only the four prebuild directories above are actually shipped.

## Verify installation

<Steps>
<Step title="Confirm the CLI is on PATH">

```bash
which rift
```

For npm/bun global installs, this should resolve to the package `bin/rift.js` symlink. For Cargo installs, expect `${CARGO_HOME:-$HOME/.cargo}/bin/rift`.

</Step>
<Step title="Run the CLI help">

```bash
rift --help
```

A successful install prints Clap help for subcommands (`init`, `create`, `remove`, `list`, `ancestors`, `gc`, `shell-init`). A missing prebuild prints:

```text
Unable to locate the Rift binary for <platform>-<arch>. Reinstall rift-snapshot.
```

and exits with code `1`.

</Step>
<Step title="Optional: verify JavaScript FFI (Node)">

Node requires the experimental FFI API (Node.js 26.1+):

```bash
node --experimental-ffi -e "import('rift-snapshot').then(m => console.log(Object.keys(m)))"
```

With Node's permission model, also pass `--allow-ffi`.

</Step>
</Steps>

## Install failure modes

| Symptom | Cause | Remedy |
| --- | --- | --- |
| `Unsupported Rift platform: <os>-<arch>` | Host outside `darwin`/`linux`/`win32` × `arm64`/`x64` | Use a supported host triple or build from source with Cargo on an unsupported arch (FFI/npm prebuilds still unavailable). |
| `Unable to locate the Rift binary for <platform>-<arch>` | Missing or corrupt prebuild directory (common on Linux arm64) | Reinstall `rift-snapshot`, or install via GitHub Release / Cargo for CLI-only use. |
| `Unable to locate the Rift Node library for <platform>-<arch>` | FFI shared library missing from prebuilds | Reinstall the npm package; verify `prebuilds/<platform>-<arch>/` contains the platform library file. |
| `cow_unavailable` on first `rift create` (Windows) | Windows has no implemented copy-on-write strategy | Expected on Windows x64 today; use Linux or macOS for workspace creation. |
| Version mismatch during release | Tag does not match crate/npm version | Release CI rejects mismatched tags; install only from published `v*` releases. |

## Related pages

<CardGroup>
<Card title="Overview" href="/overview">
What Rift exposes (CLI, npm package, FFI), supported platforms and backends, and the shortest path from init to create.
</Card>
<Card title="Quickstart" href="/quickstart">
Initialize a workspace, create a filtered child, list it, and remove it with expected stdout signals.
</Card>
<Card title="Copy strategies and platforms" href="/copy-strategies">
Platform-specific copy-on-write backends, filtered vs exact copy modes, and unsupported platforms.
</Card>
<Card title="Development" href="/development">
Build and test the Rust workspace, install the CLI locally, and CI test matrix details.
</Card>
<Card title="JavaScript API" href="/javascript-api">
`rift-snapshot` exports, Bun vs Node bindings, and Node.js 26.1+ FFI requirements.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Common failure modes after installation: uninitialized workspaces, CoW unavailable, and platform limitations.
</Card>
</CardGroup>

---

## 03. Quickstart

> Initialize a source workspace, create a filtered copy-on-write child, list it, and remove it with expected stdout signals.

- Page Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/03-quickstart.md
- Generated: 2026-06-17T23:31:50.068Z

### Source Files

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

---
title: "Quickstart"
description: "Initialize a source workspace, create a filtered copy-on-write child, list it, and remove it with expected stdout signals."
---

The `rift` CLI registers a source directory, creates a filtered copy-on-write child under adjacent `.rifts` storage, lists direct children on stdout, and moves removed children into `.trash` without printing removal paths in the default invocation.

## Prerequisites

| Requirement | Detail |
| --- | --- |
| Platform | macOS (APFS) or Linux (btrfs subvolumes or native reflinks). Workspace creation is not implemented on Windows. |
| Filesystem | Copy-on-write must succeed for the selected backend; btrfs directories require `rift init` before `rift create`. |
| Install | Global npm/bun package `rift-snapshot`, or a local Cargo build via `scripts/install.sh`. |

<Tabs>
<Tab title="npm">

```bash
npm install -g rift-snapshot
```

</Tab>
<Tab title="bun">

```bash
bun add -g rift-snapshot
```

</Tab>
<Tab title="cargo">

```bash
./scripts/install.sh
```

Installs an optimized binary to `${CARGO_HOME:-$HOME/.cargo}/bin/rift`.

</Tab>
</Tabs>

<Warning>
This repository is marked experimental. CLI behavior and interfaces may change without notice.
</Warning>

## Resulting layout

After the workflow below, a source workspace `app` and one named child `parser-fix` produce this layout:

```text
~/code/
  app/                              source workspace (.rift marker)
  .rifts/
    app/
      parser-fix/                   created child workspace
      .trash/                       removed workspaces (after `rift remove`)
```

The central SQLite registry lives in the platform user data directory (`rift/rift.sqlite` under `dirs::data_local_dir()`). Paths in stdout are absolute canonical paths.

## Walkthrough

<Steps>
<Step title="Initialize the source workspace">

From your project directory:

```bash
cd ~/code/app
rift init
```

`rift init` resolves the target path before calling core `init`:

- If a managed ancestor exists, it initializes that root.
- Otherwise it selects the nearest Git root (directory containing `.git`).
- Pass `--here` to initialize exactly the current directory instead.

On first registration, Rift writes a `.rift` marker (ULID) and inserts a root record (`parent_id = NULL`) into the registry. On macOS and non-btrfs Linux, registration is in place. On btrfs, first init of an ordinary directory reflink-imports into a new subvolume and swaps it into the same path.

<RequestExample>

```bash
cd ~/code/app
rift init
```

</RequestExample>

<ResponseExample>

macOS or already-registered workspace (stderr):

```text
Ready  /Users/you/code/app
```

First-time btrfs conversion (stderr, progress then ready):

```text
Initializing  /Users/you/code/app

First-time setup can take a moment.
New rifts will be instant.

Creating BTRFS subvolume...
Importing workspace...

Ready  /Users/you/code/app
```

When run from inside the initialized tree (stderr):

```text
run `cd /Users/you/code/app` to enter the initialized workspace
```

Already initialized (stderr):

```text
Already initialized  /Users/you/code/app
```

Restored missing marker (stderr):

```text
Restored marker  /Users/you/code/app
```

</ResponseExample>

Exit code: `0` on success; `1` on failure with a message on stderr.

</Step>

<Step title="Create a filtered child workspace">

Create a child from the source. Filtered copy is the default: heavyweight regenerable artifacts are omitted; manifests and lockfiles are preserved.

```bash
rift create --name parser-fix --no-hooks
```

| Flag | Default | Quickstart use |
| --- | --- | --- |
| `--name` | Random `adjective-noun` (e.g. `amber-badger`) | `--name parser-fix` for predictable paths |
| `--copy-all` | off (filtered) | Omit; filtered is default |
| `--no-hooks` | hooks run when `.rift.toml` exists | Skip hooks when no config is present |
| `--into` | `<parent>/.rifts/<workspace-name>/` | Omit for default storage |

`rift create` searches upward for `.rift`, copies the resolved managed workspace, records the immediate parent in the registry, and prints the new workspace path to **stdout** (one line, absolute).

<RequestExample>

```bash
cd ~/code/app
rift create --name parser-fix --no-hooks
```

</RequestExample>

<ResponseExample>

stdout:

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

</ResponseExample>

Filtered copies exclude paths whose components match built-in artifact names at any depth, including `node_modules`, `target`, `.venv`, `dist`, `build`, `coverage`, and Yarn cache paths under `.yarn/`. Lockfiles such as `package-lock.json` and `Cargo.lock` are kept.

<Note>
If the source is a Git repository, the child receives detached `HEAD` at the same commit with index and working-tree state preserved. See [Git integration](/git-integration).
</Note>

</Step>

<Step title="List direct children">

From the source workspace (or any path inside it):

```bash
rift list
```

`rift list` resolves `of` upward to the nearest `.rift` marker (default: current working directory) and prints **one child path per line** on stdout, in registry order. No header or labels.

<RequestExample>

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

</RequestExample>

<ResponseExample>

stdout:

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

When no children exist, stdout is empty and exit code is `0`.

</ResponseExample>

</Step>

<Step title="Remove the child workspace">

From inside the child, or with an explicit path:

```bash
cd /Users/you/code/.rifts/app/parser-fix
rift remove
```

For a **created** rift (not the source root), `rift remove` moves the full descendant subtree into adjacent `.trash` storage as `<storage-parent>/.trash/<id>-<name>`, updates the registry, and produces **no stdout** in the default CLI mode.

<RequestExample>

```bash
cd /Users/you/code/.rifts/app/parser-fix
rift remove
```

</RequestExample>

<ResponseExample>

stdout: *(empty)*

stderr: *(empty in default mode)*

Exit code: `0`

</ResponseExample>

After removal, `rift list` from the source prints nothing. The trashed directory remains on disk under `.trash` until garbage collection.

<Warning>
Removing the **source root** requires `rift remove -f` and prints `Unregistered  <path>` on stderr. That flow unregisters the source, removes its `.rift` marker, and trashes all descendants. It is not part of this quickstart.
</Warning>

</Step>

<Step title="Garbage-collect trashed storage (optional)">

Physically delete trashed directories:

```bash
rift gc
```

Each deleted trash path is printed on stdout, one per line.

<RequestExample>

```bash
rift gc
```

</RequestExample>

<ResponseExample>

stdout:

```text
/Users/you/code/.rifts/app/.trash/01HXXXXXXXXXXXXXX-parser-fix
```

</ResponseExample>

</Step>
</Steps>

## Stdout and stderr reference

| Command | Stream | Signal | When |
| --- | --- | --- | --- |
| `rift init` | stderr | `Ready  <path>` | Successful registration or conversion |
| `rift init` | stderr | `Already initialized  <path>` | Root already registered, marker intact |
| `rift init` | stderr | `Restored marker  <path>` | Registry entry exists but marker was missing |
| `rift init` | stderr | `run \`cd <path>\` to enter...` | Init from inside tree; default CLI (no shell wrapper) |
| `rift init` | stderr | BTRFS progress lines | First-time btrfs subvolume conversion |
| `rift create` | **stdout** | `<absolute-child-path>` | Always on success |
| `rift list` | **stdout** | `<child-path>` per line | One line per direct active child |
| `rift remove` | stdout | *(none)* | Default mode, created rift |
| `rift remove -f` | stderr | `Unregistered  <path>` | Source root unregistration only |
| `rift gc` | **stdout** | `<trash-path>` per line | Each physically deleted trash entry |

All failures print a message to **stderr** and exit with code **1**.

## Verification checklist

| Step | Filesystem check | Registry-backed check |
| --- | --- | --- |
| After `init` | `app/.rift` exists | `rift list` returns empty stdout |
| After `create` | `.rifts/app/parser-fix/` exists with its own `.rift` | `rift list` prints the child path |
| After `remove` | Child path gone; `.rifts/app/.trash/<id>-parser-fix` exists | `rift list` returns empty stdout |
| After `gc` | Trash directory deleted | `rift gc` printed the trash path |

Child `.rift` files contain a new ULID distinct from the source marker.

## Common failures

| Symptom (stderr) | Cause | Fix |
| --- | --- | --- |
| `no initialized workspace found; run \`rift init\` from the root folder` | No `.rift` marker or registry entry above CWD | Run `rift init` from the project root |
| `this workspace must be initialized first; run \`rift init\` from its root folder` | btrfs source not yet converted to subvolume | Run `rift init` on the btrfs directory first |
| `this workspace is missing its \`.rift\` marker; run \`rift init\` to restore it` | Registry entry exists, marker deleted | Run `rift init` to restore |
| `copy-on-write cloning unavailable: ...` | Unsupported platform or filesystem | Use a supported OS/backend; see [Copy strategies](/copy-strategies) |
| `postcreate hook failed at ...` | Hook command in `.rift.toml` failed | Fix hook or pass `--no-hooks`; workspace remains registered |

## End-to-end script

```bash
cd ~/code/app
rift init
CHILD=$(rift create --name parser-fix --no-hooks)
rift list
cd "$CHILD"
rift remove
rift gc
```

Capture `CHILD` from stdout for scripting. After `rift remove`, expect silent success; run `rift list` from the source to confirm the child is gone.

## Related pages

<CardGroup>
<Card title="Installation" href="/installation">
Install `rift-snapshot` globally or build the CLI from source.
</Card>
<Card title="Initialize a workspace" href="/initialize-workspace">
Git-root vs `--here` selection, btrfs conversion progress, and marker restoration.
</Card>
<Card title="Create workspaces" href="/create-workspaces">
`--name`, `--into`, `--copy-all`, hooks, and random name generation.
</Card>
<Card title="Manage and remove workspaces" href="/manage-workspaces">
List, ancestors, trash semantics, `-f`, `--children`, and `rift gc`.
</Card>
<Card title="Storage layout" href="/storage-layout">
Default `.rifts` sibling storage, trash naming, and database location.
</Card>
</CardGroup>

---

## 04. Workspaces and registry

> Managed workspace model: ULID identity, `.rift` markers, SQLite registry schema, parent-child provenance tree, and upward resolution.

- Page Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/04-workspaces-and-registry.md
- Generated: 2026-06-17T23:31:49.083Z

### Source Files

- `specs.md`
- `crates/core/src/lib.rs`
- `crates/core/src/marker.rs`
- `crates/core/src/registry.rs`
- `crates/core/src/id.rs`

---
title: "Workspaces and registry"
description: "Managed workspace model: ULID identity, `.rift` markers, SQLite registry schema, parent-child provenance tree, and upward resolution."
---

Rift tracks every managed workspace in a central SQLite registry and binds each directory to a stable ULID through a root-level `.rift` marker. The `Manager` in `crates/core` opens the registry, resolves a path to its nearest managed ancestor, and uses `parent_id` links to form a provenance tree from registered source roots down through created child workspaces.

## Managed workspace model

A **managed workspace** is any directory that Rift has registered: a **source root** registered by `init`, or a **created rift** registered by `create`. Management requires two coupled artifacts:

| Artifact | Role |
| --- | --- |
| `.rift` marker | Filesystem identity file at the workspace root; contains the workspace ULID |
| SQLite `rift` row | Authoritative record of `id`, `parent_id`, `path`, and `created_at` |

Operations such as `create`, `list`, `ancestors`, and `remove` resolve the caller's path upward to the nearest `.rift` marker, load the matching registry row, and enforce marker–registry consistency before mutating state.

```text
/projects/app/                    source root (parent_id = NULL)
/projects/.rifts/app/task-a/      created rift (parent_id = app's id)
/projects/.rifts/app/task-b/      created rift (parent_id = app's id)
```

<Note>
Default child storage lives under a sibling `.rifts/<workspace-name>/` directory associated with the **registered source root**, not beside each descendant. See [Storage layout](/storage-layout) for path rules and custom `--into` destinations.
</Note>

## ULID identity

Each workspace receives a **ULID** at first registration or creation. The `RiftId` type wraps `ulid::Ulid::new()` and stores the canonical string form.

| Property | Behavior |
| --- | --- |
| Generation | New ULID on `init` of an unregistered directory; new ULID on every `create` |
| Stability | Identity persists across renames only while the marker and registry stay consistent |
| Uniqueness | `id` is the primary key in SQLite; `path` is a separate, mutable location field |

Created workspaces always get a fresh ULID. The copied `.rift` marker from the source is replaced with the new workspace's identifier before registry insertion.

## `.rift` marker files

The marker is a single file named `.rift` at the workspace root.

```text
01HXXXXXXXXXXXXXXXXXXXXXXXXX\n
```

| Operation | Marker behavior |
| --- | --- |
| `init` (new) | Writes a new ULID after filesystem preparation |
| `init` (existing root, marker missing) | Restores the marker from the existing registry `id` |
| `init` (existing root, marker present) | Verifies marker matches registry before continuing |
| `create` | Writes a new ULID to the destination after copy |
| `remove` (source root) | Deletes the marker; directory remains on disk |
| `remove` (created rift) | Marker moves with the directory into trash |

Git repositories add `/.rift` to `.git/info/exclude` so the marker does not appear in local Git status. See [Git integration](/git-integration).

### Marker verification

Before destructive operations, Rift calls `marker::verify`, which reads `.rift` and compares it to the expected registry `id`. A mismatch surfaces as `marker_mismatch` (FFI) or the `rift marker does not match the registry` error (core).

## SQLite registry

The registry lives in a platform-specific user data directory, opened by `Manager::open_default()`:

```text
<data-local-dir>/rift/rift.sqlite
```

On Linux this resolves through `dirs::data_local_dir()` (typically `~/.local/share/rift/rift.sqlite`, or `$XDG_DATA_HOME/rift/rift.sqlite` when set). The CLI accepts a hidden `--database` flag to override the path for testing or multi-registry isolation.

### Connection settings

On open, the registry applies:

| PRAGMA | Value |
| --- | --- |
| `busy_timeout` | `2000` ms |
| `journal_mode` | `WAL` |
| `foreign_keys` | `ON` |

WAL mode and a busy timeout support concurrent access from multiple CLI processes, agents, or FFI callers without a custom locking protocol.

### Schema

```sql
CREATE TABLE rift (
  id TEXT PRIMARY KEY,
  parent_id TEXT REFERENCES rift(id) ON DELETE CASCADE,
  path TEXT NOT NULL UNIQUE,
  created_at INTEGER NOT NULL
);

CREATE INDEX rift_parent_id_idx ON rift(parent_id);

CREATE TABLE trash (
  id TEXT PRIMARY KEY,
  path TEXT NOT NULL UNIQUE,
  removed_at INTEGER NOT NULL
);
```

```mermaid
erDiagram
  rift ||--o{ rift : "parent_id"
  rift {
    TEXT id PK
    TEXT parent_id FK
    TEXT path UK
    INTEGER created_at
  }
  trash {
    TEXT id PK
    TEXT path UK
    INTEGER removed_at
  }
```

| Column | Semantics |
| --- | --- |
| `rift.id` | Stable ULID; matches `.rift` marker contents |
| `rift.parent_id` | `NULL` for source roots; set to immediate source `id` on `create` |
| `rift.path` | Current absolute filesystem path; not identity |
| `rift.created_at` | Unix epoch milliseconds at insertion |
| `trash.id` | ULID of the removed workspace |
| `trash.path` | Relocated trash directory path |
| `trash.removed_at` | Unix epoch milliseconds when moved to trash |

`ON DELETE CASCADE` on `parent_id` ensures deleting an active ancestor row cascades to descendant rows. Normal removal instead moves subtrees to `trash` first, so no surviving active record depends on deleted ancestry.

## Provenance tree

Provenance is a rooted tree keyed by `parent_id`:

| Workspace type | `parent_id` | Registered by |
| --- | --- | --- |
| Source root | `NULL` | `init` → `insert_root` |
| Created rift | Immediate source `id` | `create` → `insert_child` |

When `create` runs from a nested path or an existing child rift, Rift copies the **resolved source workspace** (nearest `.rift` ancestor), not an earlier root. The new row's `parent_id` is the immediate source's `id`.

```text
app (root, id=A, parent_id=NULL)
├── task-a (id=B, parent_id=A)
└── task-b (id=C, parent_id=A)

create from task-a → task-c (id=D, parent_id=B)
```

`Manager::root` walks `parent_id` upward until `NULL` to locate the registered source root. Default child storage (`default_storage`) is derived from that root, keeping all descendants under `.rifts/<root-name>/` regardless of which managed ancestor was the copy source.

### Tree queries

| API | Scope | Ordering |
| --- | --- | --- |
| `list` | Direct children (`parent_id = ?`) | `created_at`, then `id` |
| `ancestors` | All ancestors up to root | Immediate parent first, then root |
| `subtree` (internal) | Recursive descendants via CTE | Deepest first (`depth DESC, id`) for safe removal |

The recursive subtree query powers `remove`, `remove_all`, and `gc` orphan pruning.

## Upward resolution

`workspace_from_optional` resolves any directory path to the nearest managed workspace by walking `path.ancestors()` from the requested path toward the filesystem root.

```mermaid
flowchart TD
  Start["Requested path"] --> Walk["For each ancestor directory"]
  Walk --> HasMarker{".rift exists?"}
  HasMarker -->|yes| LookupId["Load registry row by marker ULID"]
  LookupId --> KnownId{"Row exists?"}
  KnownId -->|no| UnknownMarker["Error: unknown_marker"]
  KnownId -->|yes| PathMatch{"row.path == directory?"}
  PathMatch -->|no| MarkerMismatch["Error: marker_mismatch"]
  PathMatch -->|yes| Found["Return Record"]
  HasMarker -->|no| RegistryOnly{"Registry row at path?"}
  RegistryOnly -->|yes| MissingMarker["Error: missing_marker"]
  RegistryOnly -->|no| MoreAncestors{"More ancestors?"}
  MoreAncestors -->|yes| Walk
  MoreAncestors -->|no| NotInit["Return None → workspace_not_initialized"]
```

### Resolution outcomes

| Condition | Core error | CLI message |
| --- | --- | --- |
| No marker and no registry row in any ancestor | `WorkspaceNotInitialized` | `no initialized workspace found; run rift init from the root folder` |
| Registry row at path but `.rift` missing | `MissingMarker` | `this workspace is missing its .rift marker; run rift init to restore it` |
| Marker ULID not in registry | `UnknownMarker` | (default error text) |
| Marker ULID exists but `path` column differs | `MarkerMismatch` | (default error text) |
| Marker and registry agree | Success | — |

<Warning>
A marker whose ULID is registered to a **different** directory path is rejected. Copying `.rift` between directories without a matching registry update produces `marker_mismatch`.
</Warning>

### CLI path selection vs core resolution

The CLI defaults omitted path arguments to the current working directory. Core operations always resolve through upward marker search.

`rift init` is special: unless `--here` is passed, the CLI selects the nearest managed ancestor, nearest Git root, or the requested path before calling core `init` on that exact directory. Core `init` itself does not search parents.

## Registry lifecycle operations

| Operation | Active `rift` table | `trash` table | Marker |
| --- | --- | --- | --- |
| `init` (new) | `insert_root` | — | Write new ULID |
| `init` (restore) | Unchanged | — | Rewrite existing ULID |
| `create` | `insert_child` | — | Write new ULID at destination |
| `remove` (child) | Delete subtree rows | Insert trash rows | Moves with directory |
| `remove` (root, `-f`) | Delete root row | Trash existing descendants | Delete marker |
| `gc` | Prune missing paths (conditional) | Delete after filesystem removal | — |

`trash_moved` runs in a transaction: insert into `trash`, then delete from `rift` for each moved record. Filesystem moves to `<storage-parent>/.trash/<id>-<name>` happen before the transaction; failed transactions roll back directory renames.

## Consistency guarantees

Rift treats the marker and registry as a coupled pair:

1. **Identity** is the ULID in both `.rift` and `rift.id`.
2. **Location** is `rift.path`, which must equal the directory containing the marker.
3. **Provenance** is `parent_id`, forming a single rooted tree.
4. **Removal** verifies every existing directory in a subtree has a matching marker before moving to trash.

External deletion is handled by `gc`: active rows whose paths no longer exist are pruned only when no recorded descendant still exists on disk, preventing orphaned provenance links.

## FFI and JavaScript bindings

The same registry model backs the native library, CLI, and FFI packages. Registry-related failures map to stable error codes:

| Code | Meaning |
| --- | --- |
| `workspace_not_initialized` | No `.rift` ancestor found |
| `missing_marker` | Registry row without marker |
| `unknown_marker` | Marker ULID not in registry |
| `marker_mismatch` | Marker ULID does not match expected path |
| `not_managed` | Ancestor chain references a missing parent row |

Postcreate hooks receive `RIFT_ID` and `RIFT_PARENT_ID` from the destination and source registry records. See [Postcreate hooks](/postcreate-hooks) and [JavaScript API reference](/javascript-api).

## Related pages

<CardGroup>
  <Card title="Initialize a workspace" href="/initialize-workspace">
    Register a source root, restore deleted markers, and write the first registry row.
  </Card>
  <Card title="Create workspaces" href="/create-workspaces">
    Copy from a resolved source, assign a new ULID, and record parent provenance.
  </Card>
  <Card title="Storage layout" href="/storage-layout">
    Default `.rifts` paths, trash naming, and central database location.
  </Card>
  <Card title="Manage and remove workspaces" href="/manage-workspaces">
    List children, trace ancestors, trash subtrees, and run garbage collection.
  </Card>
  <Card title="Error codes" href="/error-codes">
    Full `RiftError` catalog including marker and registry variants.
  </Card>
</CardGroup>

---

## 05. Copy strategies and platforms

> Platform-specific copy-on-write backends (btrfs subvolumes, Linux reflinks, APFS clonefile), filtered vs exact copy modes, and unsupported platforms.

- Page Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/05-copy-strategies-and-platforms.md
- Generated: 2026-06-17T23:32:41.613Z

### Source Files

- `specs.md`
- `README.md`
- `crates/core/src/strategy/mod.rs`
- `crates/core/src/strategy/btrfs.rs`
- `crates/core/src/strategy/reflink.rs`
- `crates/core/src/strategy/apfs.rs`
- `crates/core/src/strategy/linux.rs`

---
title: "Copy strategies and platforms"
description: "Platform-specific copy-on-write backends (btrfs subvolumes, Linux reflinks, APFS clonefile), filtered vs exact copy modes, and unsupported platforms."
---

Rift routes workspace copying through a `Strategy` trait in `crates/core/src/strategy/`. `Manager::open` selects a platform backend via `default_strategy()`, then delegates `init`, `create`, and `gc` filesystem work to that implementation. There is no full-byte-copy fallback: if no copy-on-write backend succeeds, `create` fails with `CowUnavailable`.

## Strategy architecture

`Strategy` defines three operations:

| Method | Used by | Default behavior |
| --- | --- | --- |
| `copy_directory(from, to, mode)` | `create` | Platform-specific CoW clone |
| `initialize_directory(path, progress)` | `init` | No-op (`AlreadyNative`) unless the backend converts the workspace |
| `remove_directory(path)` | Failed `create` rollback, `gc` | `fs::remove_dir_all` |

`default_strategy()` resolves at compile time:

| OS target | Backend |
| --- | --- |
| Linux | `LinuxStrategy` (filesystem router) |
| macOS | `ApfsStrategy` |
| Other (e.g. Windows) | `UnsupportedStrategy` |

```mermaid
classDiagram
    class Strategy {
        <<trait>>
        +copy_directory(from, to, mode)
        +initialize_directory(path, progress)
        +remove_directory(path)
    }
    class LinuxStrategy {
        +filesystem detection via statfs
    }
    class BtrfsStrategy {
        +subvolume snapshots
        +reflink import
    }
    class LinuxReflinkStrategy {
        +FICLONE per-file reflinks
    }
    class ApfsStrategy {
        +clonefile syscall
    }
    class UnsupportedStrategy {
        +always CowUnavailable
    }
    Strategy <|.. LinuxStrategy
    Strategy <|.. ApfsStrategy
    Strategy <|.. UnsupportedStrategy
    LinuxStrategy --> BtrfsStrategy : btrfs
    LinuxStrategy --> LinuxReflinkStrategy : other
```

On Linux, `LinuxStrategy` calls `statfs` and compares `f_type` to `BTRFS_SUPER_MAGIC` (`0x9123683e`). Btrfs paths use `BtrfsStrategy`; all other filesystems use `LinuxReflinkStrategy`.

## Platform support matrix

| Platform | Filesystem | `init` behavior | `create` (filtered, default) | `create` (exact, `--copy-all`) |
| --- | --- | --- | --- | --- |
| Linux | btrfs | Convert ordinary directory to subvolume via reflink import, or register if already a subvolume | New subvolume + filtered reflink import | Writable btrfs snapshot (`BTRFS_IOC_SNAP_CREATE`) |
| Linux | XFS and other reflink-capable | Verify `FICLONE` support; register in place | Per-file reflink tree walk with filter | Full per-file reflink clone |
| macOS arm64 / x64 | APFS | Register in place | Per-entry `clonefile` with filter | Directory `clonefile` |
| Windows x64 | — | Registers workspace metadata only if reached | **Not implemented** — `CowUnavailable` | **Not implemented** |

<Note>
The npm package publishes Windows binaries, but workspace creation has no CoW backend. `create` always fails on unsupported platforms.
</Note>

## Copy modes

`CopyMode` controls what `create` materializes. The default is filtered copying.

| Mode | CLI flag | API field | Behavior |
| --- | --- | --- | --- |
| `Filtered` | *(default)* | `copyAll: false` / omitted | Omit heavyweight regenerable artifacts; preserve source manifests, lockfiles, dirty/staged/untracked files, and ignored paths not in the exclusion set |
| `All` | `--copy-all` | `copyAll: true` | Exact tree copy including all dependency and build artifacts |

```bash
rift create                  # filtered (default)
rift create --copy-all       # exact copy
```

```ts
create({ from: "/path/to/workspace", copyAll: true })
```

### Filtered artifact exclusions

`CopyFilter` matches path components at any depth. A path is excluded when any component matches, or when a `.yarn/<artifact>` pair matches.

| Category | Excluded names / paths |
| --- | --- |
| Node / JS | `node_modules`, `.pnpm-store`, `.yarn/cache`, `.yarn/unplugged`, `.yarn/install-state.gz`, `.yarn/build-state.yml`, `.next`, `.nuxt`, `.svelte-kit`, `.turbo`, `.vite`, `.parcel-cache` |
| Rust | `target` |
| Python | `.venv`, `venv`, `.tox`, `.nox`, `__pycache__`, `.pytest_cache`, `.mypy_cache`, `.ruff_cache` |
| Build output | `dist`, `build`, `coverage`, `.cache` |

Filtered mode still copies symlinks, hard links (within the included set), permissions, ownership, timestamps, and extended attributes on included entries.

## Btrfs strategy

`BtrfsStrategy` is the production backend for btrfs workspaces on Linux.

### Subvolume detection

A btrfs path is treated as a subvolume when `metadata(path).ino() == 256`. Ordinary btrfs directories fail `create` with `InitializationRequired` until `rift init` converts them.

### Initialization (`rift init`)

For an ordinary btrfs directory:

1. Create staging subvolume `.rift-init-{ULID}` beside the workspace
2. Reflink-import the full workspace into staging (reports `InitProgress::ImportedEntries`)
3. Move the original aside to `.rift-init-original-{ULID}`
4. Atomically rename staging into the original path
5. Copy metadata and remove the original backup

If the path is already a subvolume, initialization is a no-op (`StrategyInit::AlreadyNative`). If the path is not on btrfs, initialization returns `CowUnavailable`.

The CLI surfaces progress for btrfs conversion: `Creating BTRFS subvolume...`, `Importing workspace...`, then `Ready <path>`.

### Creation

| `CopyMode` | Mechanism |
| --- | --- |
| `All` | `BTRFS_IOC_SNAP_CREATE` writable snapshot from source subvolume |
| `Filtered` | `BTRFS_IOC_SUBVOL_CREATE` on destination, then `import_directory_linux_filtered`, then metadata copy |

### Removal and garbage collection

`remove_directory` attempts `BTRFS_IOC_SNAP_DESTROY` first. On `PermissionDenied` or `Unsupported`, it empties the subvolume contents and removes the directory with ordinary `remove_dir`. Non-subvolume paths use `remove_dir_all`.

## Linux reflink strategy

`LinuxReflinkStrategy` handles non-btrfs Linux filesystems that support the `FICLONE` ioctl (`0x40049409`), including XFS when kernel reflink support succeeds.

### Initialization

`initialize_directory` writes a probe file, attempts a reflink clone, and deletes the probes. Failure produces `CowUnavailable` with a message that the path does not support Linux copy-on-write reflinks. Success returns `AlreadyNative` — no directory replacement.

### Creation

Before copying, Rift verifies:

1. Source and destination parent share the same `dev` (same filesystem)
2. The destination parent passes the reflink probe

The import walk (`WalkDir`, `follow_links(false)`) handles each entry:

| Entry type | Action |
| --- | --- |
| Directory | `create_dir`, metadata copied bottom-up |
| Regular file | `FICLONE` reflink; hard links deduplicated via `(dev, ino)` map |
| Symlink | Recreated with `symlink`; metadata on symlink target |
| Other (FIFO, device, etc.) | `UnsupportedEntry` |

Both `All` and `Filtered` modes use the same reflink machinery; filtered mode applies `CopyFilter` during the walk.

### Removal

Non-btrfs paths use `fs::remove_dir_all`.

<Warning>
Reflink cloning requires source and destination on the same filesystem. If the registered source workspace is a mount root, the default sibling `.rifts/` storage may sit on a different filesystem and CoW will fail. Pass `--into` (or `into` in the API) on a path colocated with the source.
</Warning>

## APFS strategy (macOS)

`ApfsStrategy` uses the `clonefile(2)` syscall for CoW cloning.

| `CopyMode` | Mechanism |
| --- | --- |
| `All` | Single `clonefile` on the source directory tree |
| `Filtered` | Create destination root, walk included entries, `clonefile` per file, recreate symlinks and hard links, copy metadata |

Initialization uses the default `Strategy` implementation — APFS workspaces register in place with no conversion step.

Removal uses `fs::remove_dir_all`.

## Unsupported platforms

On targets other than Linux and macOS, `UnsupportedStrategy` implements `copy_directory` by returning:

```
copy-on-write cloning unavailable: no copy-on-write strategy has been implemented for this platform
```

`initialize_directory` still returns `AlreadyNative`, so `init` can register metadata, but `create` cannot produce a child workspace. Rift does not fall back to `fs::copy` or external copy commands.

## Error codes and CLI messages

Strategy failures surface through the core `Error` enum and FFI `RiftError` codes:

| Error variant | FFI code | Typical cause |
| --- | --- | --- |
| `CowUnavailable` | `cow_unavailable` | Unsupported platform, missing reflink/FICLONE, cross-filesystem destination, btrfs ioctl failure |
| `InitializationRequired` | `initialization_required` | `create` on btrfs ordinary directory that was never converted by `rift init` |
| `UnsupportedEntry` | `unsupported_entry` | FIFO, device node, or other non-file/dir/symlink in the tree |

CLI user-facing overrides:

| Error | CLI message |
| --- | --- |
| `InitializationRequired` | `this workspace must be initialized first; run rift init from its root folder` |
| `CowUnavailable` | Full error string via `error.to_string()` (prefix: `copy-on-write cloning unavailable:`) |

On copy failure, `Manager::create_with_options` removes a partially created destination directory via `strategy.remove_directory` before returning the error.

## CoW lifecycle by operation

```text
init
  Linux btrfs (ordinary dir)  →  reflink import into new subvolume, atomic swap
  Linux btrfs (subvolume)     →  register only
  Linux other (reflink OK)    →  probe FICLONE, register only
  macOS APFS                  →  register only
  Windows                     →  register only (create will still fail)

create (filtered)
  btrfs   →  new subvolume + filtered reflink import
  reflink →  filtered per-file FICLONE walk
  APFS    →  filtered per-entry clonefile walk

create (--copy-all)
  btrfs   →  writable subvolume snapshot
  reflink →  full per-file FICLONE clone
  APFS    →  directory clonefile

gc / remove rollback
  btrfs subvolume  →  SNAP_DESTROY, or empty + rmdir fallback
  other            →  remove_dir_all
```

## Verification signals

Use these observable outcomes to confirm the active backend:

| Signal | Backend |
| --- | --- |
| `Creating BTRFS subvolume...` during `rift init` | Btrfs conversion in progress |
| `Ready <path>` after first btrfs `init` | Subvolume conversion completed |
| `create` completes in sub-second time on large trees | CoW backend active (snapshot, reflink, or clonefile) |
| `copy-on-write cloning unavailable` on `create` | Platform unsupported, reflink probe failed, or cross-filesystem `--into` |
| `initialization_required` / CLI init hint | Btrfs source not yet converted to subvolume |
| `unsupported_entry` with a path | Tree contains a non-cloneable entry type (e.g. FIFO) |

Integration tests gate on environment variables: `RIFT_REQUIRE_BTRFS_TESTS`, `RIFT_REQUIRE_REFLINK_TESTS`, and `RIFT_REQUIRE_APFS_TESTS` assert the corresponding backends are available in CI.

## Related pages

<CardGroup>
  <Card title="Initialize a workspace" href="/initialize-workspace">
    Btrfs subvolume conversion, reflink verification during init, and progress signals.
  </Card>
  <Card title="Create workspaces" href="/create-workspaces">
    `--copy-all`, `--into`, and filtered vs exact copy from the CLI perspective.
  </Card>
  <Card title="Installation" href="/installation">
    Platform support matrix and prebuilt binary targets.
  </Card>
  <Card title="Error codes" href="/error-codes">
    Full `RiftError` catalog including `cow_unavailable` and `initialization_required`.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    CoW unavailable failures, cross-filesystem storage, and platform limitations.
  </Card>
</CardGroup>

---

## 06. Storage layout

> Default `.rifts` sibling storage, custom `--into` paths, trash relocation naming, and central SQLite database location.

- Page Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/06-storage-layout.md
- Generated: 2026-06-17T23:32:38.104Z

### Source Files

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

---
title: "Storage layout"
description: "Default `.rifts` sibling storage, custom `--into` paths, trash relocation naming, and central SQLite database location."
---

Rift separates three on-disk concerns: the source workspace at its original path, created child workspaces under a storage parent (default sibling `.rifts/<basename>/` or a custom `--into` directory), and a single central SQLite registry at `<user-data>/rift/rift.sqlite` that tracks active paths, trash paths, and parent-child provenance.

## On-disk layout

Every managed workspace carries a `.rift` marker at its root containing a ULID. The registry stores that same ULID plus the workspace’s current filesystem path. Created workspaces are never stored inside the workspace being copied, because an exact copy would recursively include existing rifts.

```text
~/code/                              parent of source root
├── app/                             registered source workspace
│   ├── .rift                        ULID identity marker
│   └── src/                         project files
└── .rifts/
    └── app/                         default storage parent for app’s children
        ├── parser-fix/              active created rift (--name parser-fix)
        ├── amber-brook/             active rift (random name when --name omitted)
        └── .trash/                  removed rifts awaiting gc
            ├── 01HXYZ...-parser-fix
            └── 01HABC...-amber-brook
```

<Note>
Storage parent resolution always walks up to the **registered source root**, not the immediate parent rift. Creating from a child rift still stores new workspaces under the root’s `.rifts/<basename>/`, not beside the child.
</Note>

### Default storage path

When `into` is omitted, Rift computes the storage parent as:

```text
<parent-of-root>/.rifts/<basename-of-root>/
```

For a source root at `/projects/app/`, the default storage parent is `/projects/.rifts/app/`. Each `rift create` adds a single path segment for the rift name under that directory.

| Component | Resolved from | Example |
| --- | --- | --- |
| Storage parent | Registered root’s parent + `.rifts` + root basename | `/projects/.rifts/app/` |
| Created rift path | Storage parent + rift name | `/projects/.rifts/app/parser-fix/` |
| Trash directory | Storage parent + `.trash/` | `/projects/.rifts/app/.trash/` |

Rift creates the storage parent directory if it does not exist, then canonicalizes it before joining the rift name.

### Identity markers

Each workspace root contains a `.rift` file with one ULID line (no JSON wrapper). On create, the copied marker is replaced with the new workspace’s ULID. The registry and marker must agree; mismatches block removal and other managed operations.

## Custom storage with `--into`

Override the default storage parent by passing `--into` on the CLI or `into` through the FFI/JavaScript API.

<ParamField body="into" type="AbsolutePath">
Parent directory for the new rift. Relative paths resolve from the current working directory. The final destination is `<canonical-into>/<name>/`.
</ParamField>

<RequestExample>

```bash
rift create --name hotfix --into /fast/rifts
```

</RequestExample>

<ResponseExample>

```text
/fast/rifts/hotfix
```

</ResponseExample>

Custom storage is fully supported: trash relocation uses the rift’s actual storage parent, so removed rifts from custom `--into` paths land in `<into>/.trash/<id>-<name>` on the same filesystem.

### Constraints

| Constraint | Behavior |
| --- | --- |
| Same filesystem | Copy-on-write requires source and destination on the same filesystem. Cross-device `--into` fails with `copy-on-write cloning unavailable`. |
| Not inside source | Destination paths that fall inside the source tree are rejected with `cannot copy a workspace into itself`. |
| Single path segment name | Names must be one path component: no `/`, no `.`, no `..`. |
| No collision | If `<into>/<name>` already exists, create fails with `rift directory already exists`. |
| Mount-root sources | When the registered root has no parent (filesystem mount root), the default sibling `.rifts/` path may not share a CoW-capable filesystem with the source. Pass `--into` on the same filesystem. |

## Trash relocation

`rift remove` does not delete created rift directories immediately. It renames each rift in the removed subtree from its active path into adjacent trash storage, deepest-first, then updates the registry.

### Trash path format

```text
<storage-parent>/.trash/<ulid>-<original-basename>
```

| Field | Source |
| --- | --- |
| `storage-parent` | Parent directory of the rift’s active path (default `.rifts/<basename>/` or custom `--into`) |
| `ulid` | The rift’s registry ID from its `.rift` marker |
| `original-basename` | Final path segment of the active rift directory |

Example: removing `/projects/.rifts/app/parser-fix/` moves it to `/projects/.rifts/app/.trash/01HXYZABCDEF-parser-fix/`.

### Registry transition on remove

After successful filesystem renames:

1. Active `rift` table rows for the removed subtree are deleted.
2. Corresponding `trash` table rows are inserted with the **new trash path** (not the original active path).
3. `removed_at` is recorded as a millisecond Unix timestamp.

If a trash target already exists at the computed path, removal fails before any registry change. On partial failure during rename, earlier moves in the batch are rolled back.

### Physical deletion with `rift gc`

`rift gc` walks trash registry entries, physically deletes each trash directory using the platform copy strategy, then deletes the trash record. It also prunes active registry entries whose directories were deleted outside Rift when no recorded descendant still exists on disk.

```text
active rift  --remove-->  .trash/<id>-<name>  --gc-->  (deleted)
```

<Warning>
Unregistering a source root with `rift remove -f` preserves the source directory and removes its `.rift` marker, but moves all existing descendants into trash using the same `<parent>/.trash/<id>-<name>` scheme relative to each rift’s storage parent.
</Warning>

## Central SQLite database

Metadata lives in one shared SQLite database per machine (or per explicit override), not inside individual workspaces. Multiple processes and agents can create, inspect, and remove rifts concurrently; the registry uses WAL journaling, a 2-second busy timeout, and foreign keys.

### Default location

`Manager::open_default()` resolves the database path via `dirs::data_local_dir()`:

```text
<platform-user-data-local>/rift/rift.sqlite
```

| Platform | Typical path |
| --- | --- |
| Linux | `$XDG_DATA_HOME/rift/rift.sqlite` or `~/.local/share/rift/rift.sqlite` |
| macOS | `~/Library/Application Support/rift/rift.sqlite` |
| Windows | `%LOCALAPPDATA%\rift\rift.sqlite` |

The parent `rift/` directory is created automatically on first open if missing.

### Custom database path

Override the default for testing or multi-registry isolation:

| Surface | Parameter | Visibility |
| --- | --- | --- |
| CLI | `--database <path>` | Hidden global flag |
| FFI / JavaScript | `database?: string` | Per-request option |

Integration tests pass `--database` with an isolated file while setting `XDG_DATA_HOME` to verify the default path convention separately.

### Schema

```sql
CREATE TABLE rift (
  id TEXT PRIMARY KEY,
  parent_id TEXT REFERENCES rift(id) ON DELETE CASCADE,
  path TEXT NOT NULL UNIQUE,
  created_at INTEGER NOT NULL
);

CREATE INDEX rift_parent_id_idx ON rift(parent_id);

CREATE TABLE trash (
  id TEXT PRIMARY KEY,
  path TEXT NOT NULL UNIQUE,
  removed_at INTEGER NOT NULL
);
```

| Table | Holds | Path column meaning |
| --- | --- | --- |
| `rift` | Active managed workspaces | Current on-disk location |
| `trash` | Logically removed workspaces | Current trash directory location |

`path` is the workspace’s current filesystem location, not its identity. Identity is the ULID in `id` and the `.rift` marker. `parent_id` links created rifts to their immediate source; the registered source root has `parent_id = NULL`.

## How storage, registry, and markers interact

```mermaid
flowchart LR
  subgraph filesystem [Filesystem]
    SRC["Source root\napp/ + .rift"]
    STORE[".rifts/app/"]
    CHILD["Created rift\nparser-fix/ + .rift"]
    TRASH[".trash/id-name"]
  end

  subgraph registry [SQLite rift.sqlite]
    ACTIVE["rift table"]
    TRASHTBL["trash table"]
  end

  SRC -->|"rift create"| CHILD
  CHILD -->|"stored in"| STORE
  CHILD -->|"ULID + path"| ACTIVE
  CHILD -->|"rift remove"| TRASH
  TRASH -->|"path updated"| TRASHTBL
  TRASH -->|"rift gc"| DELETE["Physical delete"]
```

On `rift create`:

1. Resolve the managed source and its registered root.
2. Choose storage parent (default `.rifts/<basename>/` or `--into`).
3. Copy the workspace with the platform CoW strategy.
4. Write a new `.rift` marker with a fresh ULID.
5. Insert a `rift` row with `parent_id` pointing at the source rift.

On `rift remove` of a created rift:

1. Collect the full active subtree (deepest first).
2. Verify each existing directory’s `.rift` marker matches the registry.
3. Rename each directory into `.trash/<id>-<name>` under its storage parent.
4. Move registry rows from `rift` to `trash`.

## Verification signals

| Operation | Expected filesystem signal | Expected registry signal |
| --- | --- | --- |
| `rift init` | `.rift` appears at source root | One `rift` row with `parent_id = NULL` |
| `rift create` | New directory under storage parent with its own `.rift` | New `rift` row with `parent_id` set |
| `rift list` | (no filesystem change) | Returns direct children by `parent_id` |
| `rift remove` | Active path gone; `.trash/<id>-<name>` exists | Row moved from `rift` to `trash` |
| `rift gc` | Trash directory deleted | `trash` row removed |

## Related pages

<CardGroup>
  <Card title="Workspaces and registry" href="/workspaces-and-registry">
    ULID identity, `.rift` markers, SQLite schema details, and parent-child provenance resolution.
  </Card>
  <Card title="Create workspaces" href="/create-workspaces">
    `--name`, `--into`, filtered vs `--copy-all` copy, and post-create hook execution.
  </Card>
  <Card title="Manage and remove workspaces" href="/manage-workspaces">
    Trash removal, source-root unregister, `--children` mode, and `rift gc` physical deletion.
  </Card>
  <Card title="Copy strategies and platforms" href="/copy-strategies">
    Platform CoW backends and why destination must share the source filesystem.
  </Card>
</CardGroup>

---

## 07. Git integration

> Git repository detection, detached HEAD behavior, marker exclusion, unsafe source states, and preserved index/working-tree semantics.

- Page Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/07-git-integration.md
- Generated: 2026-06-17T23:32:41.870Z

### Source Files

- `specs.md`
- `crates/core/src/git.rs`
- `crates/core/src/lib.rs`
- `README.md`

---
title: "Git integration"
description: "Git repository detection, detached HEAD behavior, marker exclusion, unsafe source states, and preserved index/working-tree semantics."
---

Rift treats Git as an optional integration layer in `crates/core/src/git.rs`. Both `Manager::init` and `Manager::create_with_options` call `check_source` before filesystem work; when the source is a repository, Rift hides the `.rift` marker from local Git status and detaches `HEAD` in created workspaces while preserving copied index and working-tree state. Rift does not create branches, commit changes, or replace normal Git commands.

## Role in the workspace model

Git integration sits beside the core Rift model defined by `.rift` markers, the SQLite registry, and copy-on-write strategies. A directory without `.git` is classified as `Source::PlainDirectory` and skips all Git preparation. A directory with a `.git` directory is `Source::Repository` and receives marker exclusion and, on `create`, detached-HEAD preparation in the destination.

```mermaid
flowchart TB
  subgraph cli [CLI layer]
    InitCmd["rift init"]
    CreateCmd["rift create"]
    GitRoot["git_root() ancestor walk"]
  end

  subgraph core [crates/core]
    Manager["Manager"]
    GitMod["git.rs"]
    Strategy["Strategy::copy_directory"]
    Registry["Registry"]
    Marker["marker.rs"]
  end

  InitCmd --> GitRoot
  GitRoot --> Manager
  CreateCmd --> Manager
  Manager --> GitMod
  GitMod -->|"check_source"| Manager
  Manager --> Strategy
  Manager --> Marker
  Manager --> Registry
  GitMod -->|"hide_marker / detach_destination"| Manager
```

## Repository detection

`check_source(path)` classifies the workspace root:

| Condition | Classification | Result |
| --- | --- | --- |
| No `.git` entry | `PlainDirectory` | Git preparation skipped |
| `.git` exists and is a directory | `Repository` | Unsafe-state checks run; Git preparation enabled |
| `.git` exists but is not a directory (linked worktree file) | — | `Error::UnsafeGit` |

Detection runs on the exact path passed to core `init` or resolved as the copy source for `create`. It does not walk parent directories.

<Info>
Core `init` initializes exactly `at`. Git-root selection is CLI-only: when `rift init` runs without `--here` and no managed ancestor exists, the CLI walks upward for the nearest `.git` entry and passes that path to core `init`.
</Info>

### CLI Git-root selection

`init_target` in `crates/cli/src/main.rs` resolves the initialization path:

1. If `--here` is set, use the canonicalized requested directory.
2. Else if a managed workspace exists above the request, use that Rift root.
3. Else if a registry record exists but the marker is missing, restore at that path.
4. Else walk ancestors for the first directory where `.git` exists; if none is found, use the requested directory.

`git_root` only tests `.git` existence. Linked worktrees (`.git` as a file) may be selected by the CLI but fail during core `init` with `unsafe_git`.

## Marker exclusion

Rift keeps the `.rift` ULID marker out of `git status` by appending `/.rift` to `.git/info/exclude`:

- Creates `.git/info` if needed.
- Appends `/.rift` on its own line when not already present.
- Preserves existing exclude content and adds a newline separator when required.
- Is idempotent: repeated calls do not duplicate the entry.

| Operation | Where `hide_marker` runs |
| --- | --- |
| `init` on a new or re-initialized Git repository | Source workspace |
| `init` on an already-registered Git repository | Source workspace (after marker restore or verification) |
| `create` from a Git repository | Destination workspace, then source workspace |

The destination receives a new `.rift` marker after the copy. Exclusion ensures `git status --porcelain -- .rift` is empty in the child workspace.

<Note>
Marker exclusion is local to each repository via `info/exclude`. It does not modify `.gitignore` or tracked files.
</Note>

## Detached HEAD in created workspaces

After a successful copy and before registry insertion, `detach_destination` runs on the destination when the source is a repository.

```mermaid
sequenceDiagram
  participant M as Manager::create
  participant S as Strategy
  participant G as git.rs
  participant FS as .git/HEAD

  M->>S: copy_directory(from, destination)
  S-->>M: copied tree incl. .git
  M->>M: marker::write(destination)
  M->>G: hide_marker(destination)
  M->>G: detach_destination(destination)
  alt HEAD resolves to a commit
    G->>FS: write "{commit_oid}\n"
  else unborn branch (no commits)
    G-->>M: no HEAD change
  end
  M->>G: hide_marker(from)
  M->>M: registry.insert_child
```

### Resolution path

On Linux and macOS, Rift first tries `git2` via `resolve_head_commit`, which opens the repository and peels `HEAD` to a commit OID. If libgit2 cannot resolve the layout, Rift falls back to the Git CLI:

```bash
git -C <destination> rev-parse --verify HEAD^{commit}
```

When a commit OID is obtained, Rift writes it directly to `.git/HEAD` as a detached HEAD (a raw OID line, not a symbolic ref). Symbolic tag heads such as `refs/tags/release` are peeled to their underlying commit.

When `rev-parse` fails (typical for repositories with no commits yet), `detach_destination` returns without modifying `HEAD`, leaving the unborn branch state unchanged.

### Verification signals

After `rift create` from a Git repository with commits:

- `git symbolic-ref -q HEAD` fails in the destination (HEAD is detached).
- `.git/HEAD` contains the same commit OID as `git rev-parse --verify HEAD^{commit}` on the source at copy time.
- Staged paths, unstaged modifications, and untracked files present in the copied tree remain in the destination.

## Preserved index and working-tree semantics

Git preparation runs after the filesystem copy. Copy-on-write cloning duplicates the `.git` directory and tracked working-tree content; Rift then adjusts only `HEAD` and `info/exclude`.

Default filtered copy (`CopyMode::Filtered`) omits heavyweight regenerable artifacts (`node_modules`, `target`, virtualenvs, framework caches, `dist`, `build`, `coverage`, and related paths defined in `crates/core/src/filter.rs`). It still preserves:

- Manifests and lockfiles
- Staged changes in the index
- Unstaged modifications
- Untracked files outside the excluded artifact set
- Ignored files outside the excluded artifact set

`--copy-all` / `copyAll: true` copies the full tree, including dependency and build artifacts, before the same Git preparation steps.

Postcreate hooks from `.rift.toml` run after workspace creation, Git preparation, and registry insertion.

## Unsafe source states

Rift refuses `init` and `create` when Git state would make a safe exact copy unclear. `check_source` scans `.git` for these markers:

| Marker | Type |
| --- | --- |
| `MERGE_HEAD` | file |
| `CHERRY_PICK_HEAD` | file |
| `REVERT_HEAD` | file |
| `BISECT_LOG` | file |
| `rebase-merge` | directory |
| `rebase-apply` | directory |
| `index.lock` | file |
| `HEAD.lock` | file |

Additionally, linked Git worktrees (`.git` is a `gitdir:` pointer file, not a directory) are always rejected.

On failure, no destination directory is created and no registry row is inserted. Tests confirm the expected child path does not exist and `list` returns empty.

### Error surfaces

| Surface | Code / variant | Typical message |
| --- | --- | --- |
| Rust core | `Error::UnsafeGit` | `unsafe Git source: …` |
| FFI / JavaScript | `unsafe_git` | Same message via `RiftError`; no `path` field |
| CLI | exit code `1` | Prints the core error string to stderr |

Example messages:

- `unsafe Git source: linked Git worktree sources are not supported`
- `unsafe Git source: Git state in progress: MERGE_HEAD`

<Warning>
Finish or abort in-progress merge, rebase, cherry-pick, revert, and bisect operations, and clear Git lock files before running `rift init` or `rift create`.
</Warning>

## What Rift does not do

Per `specs.md` and the implementation:

- Does not create branches in the destination
- Does not commit staged or unstaged changes
- Does not run `git checkout`, `git switch`, or other branch-management commands
- Does not modify `.gitignore` (only `info/exclude` for `/.rift`)
- Does not validate Git cleanliness beyond the unsafe-state markers above

Normal Git workflows remain the developer's responsibility after workspace creation.

## Operation summary

| Operation | Git checks | Git side effects |
| --- | --- | --- |
| `init` | `check_source(at)` | `hide_marker(at)` when repository |
| `create` | `check_source(from)` before copy | `hide_marker(destination)`, `detach_destination(destination)`, `hide_marker(from)` when repository |
| `remove`, `list`, `ancestors`, `gc` | None | None |

The JavaScript `init` function initializes exactly `at`; Git-root selection and `--here` are CLI-only behaviors documented on the initialize-workspace page.

## Related pages

<CardGroup>
  <Card title="Initialize a workspace" href="/initialize-workspace">
    Git-root vs `--here` selection, marker restoration, and init success signals.
  </Card>
  <Card title="Create workspaces" href="/create-workspaces">
    Filtered vs `--copy-all` copy, Git preparation order, and postcreate hooks.
  </Card>
  <Card title="Workspaces and registry" href="/workspaces-and-registry">
    `.rift` marker identity, upward resolution, and parent-child provenance.
  </Card>
  <Card title="Error codes" href="/error-codes">
    Full `RiftError` catalog including `unsafe_git`.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Unsafe Git state failures and common recovery steps.
  </Card>
</CardGroup>

---

## 08. Initialize a workspace

> Run `rift init` with Git-root vs `--here` selection, btrfs subvolume conversion progress, marker restoration, and success signals.

- Page Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/08-initialize-a-workspace.md
- Generated: 2026-06-17T23:33:15.564Z

### Source Files

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

---
title: "Initialize a workspace"
description: "Run `rift init` with Git-root vs `--here` selection, btrfs subvolume conversion progress, marker restoration, and success signals."
---

`rift init` registers a source workspace in the central SQLite registry, writes a `.rift` ULID marker at the workspace root, and—on Linux btrfs—may convert an ordinary directory into a btrfs subvolume before registration. The CLI resolves the target path (Git root vs current directory) before calling core `init` on exactly one absolute directory; the JavaScript `init()` API skips that selection and always initializes `at`.

## Command

```bash
rift init [PATH] [--here]
```

<ParamField body="PATH" type="path">
Optional directory to initialize. Defaults to the current working directory after canonicalization.
</ParamField>

<ParamField body="--here" type="flag">
Initialize exactly `PATH` (or the current directory). Disables upward selection of an existing Rift root or Git root.
</ParamField>

<Note>
Hidden global flags `--database` and `--shell-cwd` exist for testing and shell integration. See [CLI reference](/cli-reference) and [Shell integration](/shell-integration).
</Note>

## Target selection (CLI only)

Without `--here`, the CLI chooses the directory passed to core `init` before any filesystem work runs.

```text
requested = canonicalize(PATH or cwd)

if --here:
  init_at = requested
else if upward search finds .rift marker:
  init_at = that managed workspace root     → outcome: "Already initialized"
else if registry has record but .rift missing:
  init_at = that registered root            → outcome: "Restored marker" (+ optional conversion)
else if upward search finds .git directory:
  init_at = nearest Git root
else:
  init_at = requested
```

<Warning>
Git-root selection walks ancestors for a `.git` entry. Linked Git worktrees (`.git` is a file, not a directory) are rejected during core initialization with `unsafe_git`, not during target selection.
</Warning>

| Mode | From `~/code/app/pkg` (Git root at `app/`, no Rift marker) | Effect |
|------|--------------------------------------------------------------|--------|
| Default | `rift init` | Initializes `~/code/app` |
| `--here` | `rift init --here` | Initializes `~/code/app/pkg` |
| Inside existing root | `rift init` from a child of an initialized workspace | Initializes the existing root; prints `Already initialized` |

The JavaScript `init({ at })` function and FFI `init` request initialize exactly `at` with no upward resolution. Git-root and `--here` behavior is CLI-only.

## What initialization does

Core `init` runs a fixed sequence on the selected absolute directory:

1. **Verify directory** — path must exist and be a directory (`canonicalize`).
2. **Git safety check** — refuse linked worktrees and in-progress merge/rebase/cherry-pick/revert/bisect or lock states.
3. **Registry lookup** — if the path is already registered:
   - Restore `.rift` when the marker file is missing (same ULID from registry).
   - Verify marker matches registry when present.
   - Run platform `initialize_directory` (btrfs conversion may still apply).
   - Add `/.rift` to `.git/info/exclude` when the workspace is a Git repository.
4. **First registration** — if unregistered:
   - Reject stray `.rift` files without a registry record (`marker_mismatch`).
   - Run platform initialization.
   - Write a new ULID marker, register as root (`parent_id = NULL`), hide marker from Git status.

```text
unregistered path
  → strategy.initialize_directory
  → RegisteringWorkspace (core progress)
  → write .rift + insert_root

registered path
  → restore/verify marker
  → strategy.initialize_directory
  → hide marker from Git (if repo)
```

### Platform behavior

| Platform / filesystem | `initialize_directory` | Registration outcome (typical) |
|-----------------------|------------------------|--------------------------------|
| Linux btrfs, already subvolume | No-op | `Registered` or `AlreadyInitialized` |
| Linux btrfs, ordinary directory | Reflink-import into staged subvolume, atomic path swap | `Converted` |
| Linux non-btrfs (XFS, etc.) | Verify `FICLONE` reflink support | `Registered` / `AlreadyInitialized` |
| macOS APFS | Default strategy: no conversion | `Registered` / `AlreadyInitialized` |
| Windows | No CoW strategy | `cow_unavailable` on create; init uses default no-op strategy |

On btrfs, first-time conversion creates staging paths `.rift-init-{ULID}` and `.rift-init-original-{ULID}` beside the workspace, reflink-imports the tree, renames the original aside, activates the subvolume at the original path, copies metadata, and removes the original. Failed activation rolls back to the original directory.

<Info>
After btrfs conversion the workspace path is unchanged, but the directory inode is new. If your shell was inside the directory during conversion, re-enter it—see [Success signals](#success-signals).
</Info>

## Btrfs conversion progress

Progress events are emitted on stderr only during Linux btrfs first-time conversion. The CLI maps `InitProgress` variants to user-visible lines.

<Steps>
<Step title="Creating subvolume">
stderr:

```text
Initializing  /path/to/workspace

First-time setup can take a moment.
New rifts will be instant.

Creating BTRFS subvolume...
```
</Step>

<Step title="Importing workspace">
stderr:

```text
Importing workspace...
```

Core emits `ImportedEntries { entries }` during per-file reflink import; the CLI does not print entry counts.
</Step>

<Step title="Activating and cleanup">
`ActivatingWorkspace`, `RemovingOriginal`, and `RegisteringWorkspace` run without additional CLI lines. On success:

```text
Ready  /path/to/workspace
```

When conversion produced progress output, `Ready` is preceded by a blank line.
</Step>
</Steps>

Non-btrfs platforms and already-converted btrfs subvolumes skip the conversion progress block entirely.

## Marker restoration

A registered workspace can lose its `.rift` file while the SQLite record remains. Upward resolution then returns `MissingMarker` with the registered root path; `rift init` (without `--here`) selects that root automatically.

Restoration behavior:

- Emits core progress `RestoringMarker` (no CLI line).
- Rewrites `.rift` with the **existing** registry ULID (identity is preserved).
- Continues `initialize_directory`—for example, completing a pending btrfs conversion on a partially initialized tree.
- Prints stderr: `Restored marker  <path>` when the outcome is not `Converted`.

If the marker exists and matches the registry on an already-native workspace, the outcome is `AlreadyInitialized` and the CLI prints `Already initialized  <path>`.

### Marker file

```text
/path/to/workspace/.rift
```

Single-line ULID plus newline. The same value is stored in the central registry. See [Workspaces and registry](/workspaces-and-registry).

## Success signals

All completion messages go to **stderr** unless noted. Exit code is `0` on success.

| Outcome | Condition | stderr signal | Extra stdout |
|---------|-----------|---------------|--------------|
| `Registered` | First registration, no btrfs conversion | `Ready  <path>` | — |
| `Converted` | Btrfs ordinary directory → subvolume | Progress lines, then `Ready  <path>` | — |
| `AlreadyInitialized` | Registered, marker OK, no conversion needed | `Already initialized  <path>` | — |
| Marker restored | Registered, marker was missing, no conversion | `Restored marker  <path>` | — |

### Re-enter after conversion

When initialization converts the workspace **and** the current working directory is inside the initialized path, the CLI advises:

```text
run `cd /path/to/workspace` to enter the initialized workspace
```

With shell integration (`rift shell-init` + `--shell-cwd`), the initialized path is printed to **stdout** instead so the wrapper can `cd` automatically. See [Shell integration](/shell-integration).

<Check>
Verify initialization:

```bash
test -f .rift && rift list
```

`list` returns no children for a fresh source root (exit `0`, empty stdout). A restored or new marker contains a ULID readable with `cat .rift`.
</Check>

## Git integration during init

For Git repositories, successful init appends `/.rift` to `.git/info/exclude` so the marker does not appear in `git status`. Init does not detach `HEAD` or alter the index—that happens on `rift create`. See [Git integration](/git-integration).

## Common failures

| Error / code | Typical cause | Guidance |
|--------------|---------------|----------|
| `cow_unavailable` | Linux path not on btrfs (btrfs strategy) or reflink probe failed | Use btrfs or a reflink-capable filesystem; see [Copy strategies](/copy-strategies) |
| `unsafe_git` | Linked worktree or in-progress Git operation | Finish or abort Git state; use a normal repository directory |
| `marker_mismatch` | `.rift` present on an unregistered path | Remove orphan marker or pick the correct directory with `--here` |
| `workspace_not_initialized` | Used by other commands, not `init` itself | Run `rift init` from the project root |
| Non-zero exit | Any `RiftError` | Message on stderr; see [Error codes](/error-codes) |

## JavaScript and FFI

```ts
import { init } from "rift-snapshot";

init({ at: "/absolute/path/to/workspace" });
```

`init` returns `null` on success. It does not surface btrfs progress callbacks; use the CLI for conversion progress output. Optional `database` overrides the default SQLite path (`~/.local/share/rift/rift.sqlite` on Linux). See [JavaScript API reference](/javascript-api).

## Related pages

<CardGroup>
<Card title="Quickstart" href="/quickstart">
Initialize, create a child workspace, list it, and remove it with expected stdout signals.
</Card>
<Card title="Workspaces and registry" href="/workspaces-and-registry">
ULID identity, `.rift` markers, SQLite schema, and parent-child provenance.
</Card>
<Card title="Copy strategies and platforms" href="/copy-strategies">
Btrfs subvolume conversion, Linux reflinks, and APFS clonefile backends.
</Card>
<Card title="Git integration" href="/git-integration">
Repository detection, marker exclusion, and unsafe source states.
</Card>
<Card title="CLI reference" href="/cli-reference">
Full `rift init` flags, exit codes, and hidden options.
</Card>
</CardGroup>

---

## 09. Create workspaces

> Create child workspaces with `--name`, `--into`, filtered vs `--copy-all` copy, `--no-hooks`, random name generation, and Git preparation.

- Page Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/09-create-workspaces.md
- Generated: 2026-06-17T23:33:53.070Z

### Source Files

- `README.md`
- `specs.md`
- `crates/core/src/lib.rs`
- `crates/core/src/filter.rs`
- `crates/core/src/name.rs`
- `crates/core/src/hook.rs`

---
title: "Create workspaces"
description: "Create child workspaces with `--name`, `--into`, filtered vs `--copy-all` copy, `--no-hooks`, random name generation, and Git preparation."
---

`rift create` resolves the nearest managed ancestor from the current directory (or an explicit `from` path), copy-on-write clones that workspace into storage, registers the child in the SQLite registry with the immediate source as its parent, prepares Git state in the destination, and optionally runs `.rift.toml` postcreate hooks. On success the new workspace path is printed to stdout.

## Prerequisites

The source directory must belong to an initialized Rift workspace. Rift walks upward from `from` (default: current working directory) until it finds a `.rift` marker tied to a registry record.

<Warning>
If no marker is found, the CLI exits with `no initialized workspace found; run rift init from the root folder`. On Linux btrfs, an ordinary directory that was never converted to a subvolume fails with `this workspace must be initialized first; run rift init from its root folder`.
</Warning>

## Basic usage

```bash
cd ~/code/app
rift create
```

<RequestExample>

```bash
rift create --name parser-fix
```

</RequestExample>

<ResponseExample>

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

</ResponseExample>

The printed path is the canonical destination. Shell integration (`rift shell-init`) can consume it via the hidden `--shell-cwd` flag to `cd` into the new workspace automatically.

## CLI flags

| Flag | Default | Effect |
| --- | --- | --- |
| `from` (positional) | Current working directory | Starting path for upward `.rift` resolution |
| `--name` | Random `adjective-noun` name | Single path segment for the child directory |
| `--into` | Root workspace's `.rifts/<name>/` parent | Custom storage parent directory |
| `--copy-all` | Off (filtered copy) | Copy every path, including regenerable artifacts |
| `--no-hooks` | Off (hooks run) | Skip `.rift.toml` loading and postcreate execution |

<ParamField body="from" type="path">
Optional positional argument. Must resolve to a directory inside a managed workspace. Rift copies the workspace at the resolved marker path, not a subdirectory when `from` points deeper inside a managed tree.
</ParamField>

<ParamField body="--name" type="string">
Single path segment. Rejects empty names, `.`, `..`, and names containing path separators. When omitted, Rift generates a readable `adjective-noun` name (for example `amber-badger`) independent of the workspace ULID stored in `.rift`.
</ParamField>

<ParamField body="--into" type="absolute or relative path">
Parent directory for the new child. Rift creates the directory if needed, canonicalizes it, then places `<name>` inside. Must not lie inside the source tree. Custom storage must be on a filesystem that supports the active copy-on-write backend with the source; cross-filesystem `--into` paths fail with `copy-on-write cloning unavailable`.
</ParamField>

<ParamField body="--copy-all" type="boolean">
Selects `CopyMode::All`. Without this flag, Rift uses filtered copy and omits known heavyweight regenerable artifacts at any depth.
</ParamField>

<ParamField body="--no-hooks" type="boolean">
Selects `HookMode::Skip`. Skips reading `.rift.toml` entirely, so invalid config cannot block creation.
</ParamField>

## Default storage layout

When `--into` is omitted, Rift stores children beside the registered root workspace:

```text
~/code/app/                    source (registered root)
~/code/.rifts/app/parser-fix/  created child
```

Storage always anchors to the **root** workspace, even when `from` is itself a created child. Creating from `~/code/.rifts/app/first` still places siblings under `~/code/.rifts/app/`, and records `first` as the immediate parent in the registry.

<Info>
Created workspaces are never stored inside the directory being copied. An exact copy would recursively include existing children.
</Info>

## Copy modes

Filtered copy is the default. Rift preserves manifests, lockfiles, dirty files, staged changes, untracked files, and ignored paths that are not in the built-in exclusion set.

### Filtered (default)

Excluded path components (matched at any depth):

| Category | Excluded names |
| --- | --- |
| Node / JS | `node_modules`, `.pnpm-store`, `.yarn/cache`, `.yarn/unplugged`, `.yarn/install-state.gz`, `.yarn/build-state.yml` |
| Rust | `target` |
| Python | `.venv`, `venv`, `.tox`, `.nox`, `__pycache__`, `.pytest_cache`, `.mypy_cache`, `.ruff_cache` |
| Framework caches | `.next`, `.nuxt`, `.svelte-kit`, `.turbo`, `.vite`, `.parcel-cache`, `.cache` |
| Build output | `dist`, `build`, `coverage` |

Platform backends apply filtered mode differently:

| Platform | Filtered copy | Exact copy (`--copy-all`) |
| --- | --- | --- |
| Linux btrfs | Reflink import into a new subvolume (included paths only) | Writable btrfs subvolume snapshot |
| Linux reflink (XFS, etc.) | Per-file reflinks for included entries | Full tree reflink clone |
| macOS APFS | Per-entry `clonefile` | Directory `clonefile` clone |

<Note>
Full byte copying is not implemented. If no copy-on-write strategy succeeds, creation fails closed.
</Note>

### Exact (`--copy-all`)

```bash
rift create --copy-all --name full-copy
```

Preserves all paths including `node_modules`, `target`, virtualenvs, and build caches. Use when you need a bitwise-equivalent tree without reinstalling dependencies.

## Postcreate hooks

When hooks are enabled (default), Rift loads `.rift.toml` from the **source** workspace before copying. Invalid config (wrong `version`, empty `run`, unknown fields) fails before any filesystem copy begins.

After the workspace is copied, the `.rift` marker is written, Git preparation completes, and the child is registered, configured postcreate commands run sequentially in the **destination** root:

```toml
version = 1

[[hooks.postcreate]]
run = "pnpm install --frozen-lockfile"

[[hooks.postcreate]]
run = "pnpm run codegen"
```

Each hook inherits stdio and receives:

| Variable | Value |
| --- | --- |
| `RIFT_SOURCE` | Source workspace path |
| `RIFT_DESTINATION` | New workspace path |
| `RIFT_ID` | Child ULID |
| `RIFT_PARENT_ID` | Immediate parent ULID |

```bash
rift create --no-hooks
```

`--no-hooks` skips config loading and hook execution. An invalid `.rift.toml` on the source does not block creation.

<Warning>
If a hook fails, the workspace **remains registered and on disk**. `rift create` exits with a `postcreate hook failed` error naming the destination path and failing command. Later hooks in the sequence do not run.
</Warning>

## Git preparation

When the source contains a Git repository (`.git` is a directory), Rift prepares the destination after copying:

1. Adds `/.rift` to `.git/info/exclude` in the destination (and re-applies on the source) so the marker never appears in `git status`.
2. Detaches `HEAD` at the same commit as the source. Symbolic tag heads are peeled to commits. Repositories with no commits keep their unborn branch state unchanged.
3. Preserves index, working tree, staged, unstaged, untracked, and ignored state for all copied paths.

Creation is refused when the source Git state is unsafe:

| Condition | Error |
| --- | --- |
| Linked worktree (`.git` is a file) | `linked Git worktree sources are not supported` |
| Merge, cherry-pick, revert, or bisect in progress | `Git state in progress: <marker>` |
| `index.lock` or `HEAD.lock` present | `Git state in progress: <lock>` |

Rift does not create branches, commit changes, or replace normal Git workflows.

## Create lifecycle

```mermaid
sequenceDiagram
    participant CLI as rift CLI
    participant Mgr as Manager
    participant Reg as SQLite registry
    participant Strat as Strategy
    participant Git as git module
    participant Hook as hook runner

    CLI->>Mgr: create_with_options(from, name, into, options)
    Mgr->>Mgr: resolve source via upward .rift search
    Mgr->>Git: check_source(from)
    Mgr->>Mgr: resolve root for default storage
    Mgr->>Mgr: allocate ULID, validate name and destination
    alt HookMode::Run
        Mgr->>Mgr: load .rift.toml from source
    end
    Mgr->>Strat: copy_directory(from, dest, copy_mode)
    Strat-->>Mgr: CoW clone complete
    Mgr->>Mgr: write .rift marker (new ULID)
    alt is Git repository
        Mgr->>Git: hide_marker(dest), detach_destination(dest)
        Mgr->>Git: hide_marker(source)
    end
    Mgr->>Reg: insert_child(id, parent_id, path)
    Mgr->>Hook: run_postcreate(steps, source, dest)
    Mgr-->>CLI: destination PathBuf
    CLI-->>CLI: print path to stdout
```

On copy failure, Rift removes a partially created destination directory. On failure after copy but before registration completes, it also removes the destination.

## Parentage and identity

Each created workspace receives a new ULID written to `.rift`. The registry records `parent_id` as the immediate source workspace's id (not the root). Creating a child from another child forms a provenance chain traceable with `rift ancestors`.

```bash
rift create --name first      # parent: source root
cd "$(rift create --name second)"   # parent: first; storage still under root's .rifts/
rift ancestors                # first, then source root
```

## JavaScript API

The `rift-snapshot` package exposes the same semantics:

```ts
import { create } from "rift-snapshot";

const path = create({
  from: process.cwd(),
  name: "schema-work",
  into: "/fast/rifts",
  copyAll: false,
  hooks: true,
});
```

<ResponseField name="return" type="string">
Absolute path of the created workspace.
</ResponseField>

| Option | CLI equivalent | Default |
| --- | --- | --- |
| `from` | positional `from` | Required in FFI (no cwd default in core) |
| `name` | `--name` | Random `adjective-noun` |
| `into` | `--into` | Root's `.rifts/<name>/` parent |
| `copyAll` | `--copy-all` | `false` (filtered) |
| `hooks` | inverse of `--no-hooks` | `true` |

Failures throw `RiftError` with a `code` (for example `workspace_not_initialized`, `unsafe_git`, `hook_failed`, `cow_unavailable`) and optional `path`.

## Common errors

| Situation | CLI message / error code |
| --- | --- |
| No `.rift` ancestor | `no initialized workspace found; run rift init from the root folder` |
| btrfs dir not a subvolume | `this workspace must be initialized first; run rift init from its root folder` |
| Destination already exists | `rift directory already exists: <path>` |
| Destination inside source | `cannot copy a workspace into itself: <path>` |
| Invalid `--name` | `invalid rift name: <name>` |
| Cross-filesystem `--into` | `copy-on-write cloning unavailable: ...` |
| Unsafe Git state | `unsafe Git source: ...` |
| Invalid `.rift.toml` (hooks on) | `invalid rift config at <path>: ...` |
| Hook command failed | `postcreate hook failed at <path>: <command> ...` |

<Check>
**Verify success:** stdout is a single absolute path; the path contains a `.rift` file with a new ULID; `rift list` from the source includes the child; copied project files are present (filtered or exact per flags).
</Check>

## Related pages

<CardGroup>
<Card title="Quickstart" href="/quickstart">
Initialize a source workspace, create a filtered child, list it, and remove it with expected stdout signals.
</Card>
<Card title="Copy strategies and platforms" href="/copy-strategies">
Platform-specific copy-on-write backends, filtered vs exact modes, and unsupported platforms.
</Card>
<Card title="Storage layout" href="/storage-layout">
Default `.rifts` sibling storage, custom `--into` paths, and trash relocation naming.
</Card>
<Card title="Git integration" href="/git-integration">
Detached HEAD behavior, marker exclusion, unsafe source states, and preserved index semantics.
</Card>
<Card title="Postcreate hooks" href="/postcreate-hooks">
`.rift.toml` v1 schema, hook environment variables, sequential execution, and failure behavior.
</Card>
<Card title="CLI reference" href="/cli-reference">
All `rift` subcommands, flags, defaults, and exit codes.
</Card>
</CardGroup>

---

## 10. 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.

- Page Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/10-manage-and-remove-workspaces.md
- Generated: 2026-06-17T23:33:38.628Z

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

---

## 11. Shell integration

> Install Bash, Zsh, or Nushell wrappers via `rift shell-init` for automatic `cd` after init, create, and remove operations.

- Page Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/11-shell-integration.md
- Generated: 2026-06-17T23:34:59.024Z

### Source Files

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

---
title: "Shell integration"
description: "Install Bash, Zsh, or Nushell wrappers via `rift shell-init` for automatic `cd` after init, create, and remove operations."
---

`rift shell-init` prints a shell function that shadows the `rift` command, delegates `init`, `create`, and `remove` to the real binary with a hidden global `--shell-cwd` flag, and runs `cd` when the CLI prints a destination path on stdout. Shell integration is CLI-only; the native library and JavaScript FFI bindings do not expose this behavior.

## Supported shells

| Shell | `shell-init` argument | Install pattern |
| ----- | --------------------- | --------------- |
| Bash | `bash` | `eval` into `~/.bashrc` or a sourced file |
| Zsh | `zsh` | `eval` into `~/.zshrc` or a sourced file |
| Nushell | `nushell` | Save script to an autoload directory |

Fish, PowerShell, and other shells are not generated. Use the plain `rift` executable without a wrapper.

## Install the wrapper

<Steps>
<Step title="Generate the wrapper">

Run `rift shell-init` with your shell name. The command writes the function definition to **stdout** and resolves the executable path from `std::env::current_exe()` at generation time.

</Step>
<Step title="Load it in your shell">

<Tabs>
<Tab title="Bash or Zsh">

Add to your shell startup file:

```bash
eval "$(rift shell-init zsh)"   # or: bash
```

`eval` defines a `rift()` function (Bash/Zsh) that takes precedence over a same-named binary on `PATH` for the current session.

</Tab>
<Tab title="Nushell">

Save the script into the first user autoload directory:

```nushell
rift shell-init nushell | save -f (($nu.user-autoload-dirs | first) | path join "rift.nu")
```

Restart Nushell or reload autoload files so `rift` is defined as `def --env --wrapped rift`.

</Tab>
</Tabs>

</Step>
<Step title="Verify">

After reloading your shell, `type rift` (Bash/Zsh) or `which rift` (Nushell) should show a shell function, not only a filesystem path. Running `rift list` should behave identically to the unwrapped binary.

</Step>
</Steps>

## How the wrapper works

The generated function intercepts only three subcommands. All other subcommands (`list`, `ancestors`, `gc`, and future additions) pass through to the underlying executable unchanged.

```mermaid
flowchart TD
  A["rift &lt;subcommand&gt;"] --> B{subcommand?}
  B -->|init, create, remove| C["rift --shell-cwd &lt;args&gt;"]
  B -->|anything else| D["rift &lt;args&gt;"]
  C --> E{stdout path non-empty?}
  E -->|yes| F["cd to path"]
  E -->|no| G["stay in cwd"]
  D --> H["print CLI output as-is"]
```

### Bash and Zsh

Bash and Zsh share one POSIX function template:

```bash
rift() {
  case "${1-}" in
    init|create|remove)
      local __rift_cwd
      __rift_cwd="$('<executable>' --shell-cwd "$@")" || return $?
      if [ -n "$__rift_cwd" ]; then
        builtin cd -- "$__rift_cwd" || return $?
      fi
      ;;
    *)
      '<executable>' "$@"
      ;;
  esac
}
```

The wrapper propagates non-zero exit codes before attempting `cd`. The executable path is single-quoted with POSIX escaping for embedded apostrophes.

### Nushell

Nushell defines an environment-aware, wrapped command:

```nushell
def --env --wrapped rift [...rest] {
  match ($rest | get 0? | default "" | into string) {
    "init" | "create" | "remove" => {
      let cwd = (^<executable> --shell-cwd ...$rest | str trim)
      if ($cwd | is-not-empty) {
        cd $cwd
      }
    }
    _ => {
      ^<executable> ...$rest
    }
  }
}
```

The executable is embedded in a raw string (`r#'...'#`) with extra `#` characters when the path contains conflicting quote sequences.

## The `--shell-cwd` contract

`--shell-cwd` is a hidden, global CLI flag intended only for the shell wrapper. It changes how `init`, `create`, and `remove` split output between stdout and stderr.

| Stream | Role with `--shell-cwd` |
| ------ | ----------------------- |
| **stdout** | Destination path only. The wrapper reads this line and passes it to `cd`. |
| **stderr** | Human-readable status: progress, `created`, `removed`, `Unregistered`, and hints. |

Without `--shell-cwd`, `rift create` still prints the new workspace path on stdout, but status lines use stdout instead of stderr for some remove variants.

<Callout type="info">
The wrapper never parses stderr for paths. If stdout is empty after a successful command, the working directory stays unchanged.
</Callout>

## Directory changes by command

### `rift create`

Always emits the new workspace path on stdout when creation succeeds. With `--shell-cwd`, the status line `created <path>` goes to stderr instead.

The wrapper `cd`s into the newly created child workspace every time.

### `rift init`

Stdout carries a destination path only when **both** conditions hold:

1. Initialization performed a filesystem **conversion** (`InitOutcome::Converted`), such as a first-time btrfs subvolume import-and-swap.
2. The shell's current working directory is inside the initialized tree (`cwd.starts_with(at)`).

When those conditions are met, stdout is the initialized root path. Simple registration on APFS or non-converting Linux init prints `Ready <path>` to stderr and leaves stdout empty, so the wrapper does not change directory.

Other init outcomes also leave stdout empty:

| Outcome | stderr signal | Wrapper `cd` |
| ------- | ------------- | ------------ |
| Already initialized | `Already initialized <path>` | No |
| Marker restored | `Restored marker <path>` | No |
| Registered without conversion | `Ready <path>` | No |

### `rift remove`

Behavior depends on whether the current directory is inside the workspace being removed.

**Single remove** (`rift remove` without `--children`):

| Situation | stdout destination | stderr |
| --------- | ------------------ | ------ |
| Removing a child rift while cwd is inside it | Nearest parent workspace | `removed <path>` |
| Unregistering a source root (`-f`) while cwd is inside it | The source root path | `Unregistered <path>` |
| Removing a workspace cwd is not inside | *(empty)* | `Unregistered` or `removed` as applicable |

**Bulk remove** (`rift remove --children`):

When cwd is inside any removed descendant, stdout is the selected parent workspace path. Each removed child path is reported on stderr as `removed <path>`.

`cd` never runs when the command fails (Bash/Zsh exit before `cd`; an empty stdout suppresses `cd` in all shells).

## Commands that pass through

These subcommands are not intercepted and never trigger automatic `cd`, whether or not the wrapper is installed:

- `rift list`
- `rift ancestors`
- `rift gc`
- `rift shell-init`

Use the plain binary or the wrapper equivalently for these commands.

## Scope and limitations

- **CLI only.** `specs.md` documents shell integration as optional CLI ergonomics. The native `rift` library and Bun/Node FFI packages have no shell-wrapper or `--shell-cwd` API.
- **Three shells.** Only `bash`, `zsh`, and `nushell` are valid `shell-init` arguments (Clap `ValueEnum`).
- **Executable pinning.** The wrapper hard-codes the path of the binary that ran `shell-init`. Reinstalling or moving the binary requires regenerating and reloading the wrapper.
- **Init conversion edge case.** Auto-`cd` after `init` targets btrfs conversion scenarios where the directory at the same path is atomically replaced; registration-only platforms may not emit a stdout path.
- **Windows.** Workspace operations are not implemented on Windows; shell wrappers are not part of the supported Windows workflow.

## Troubleshooting

<Accordion title="Wrapper installed but directory never changes">
Confirm the subcommand is `init`, `create`, or `remove`. Check whether stdout is empty: `rift --shell-cwd create` should print exactly one path line on stdout and status text on stderr. For `init`, a path appears only after a converting initialization while cwd is inside the tree.
</Accordion>

<Accordion title="`rift` still runs the binary, not the function">
Reload the shell config (`source ~/.zshrc`, restart Nushell, etc.). Ensure `eval "$(rift shell-init …)"` runs after any `PATH` changes that might redefine `rift`.
</Accordion>

<Accordion title="Nushell `cd` runs after a failed command">
The Nushell wrapper trims stdout and `cd`s when non-empty; it does not mirror Bash/Zsh `|| return $?` on the external call. Check stderr and exit code manually, or call the bare `rift` binary when you need strict failure semantics.
</Accordion>

<Accordion title="Wrong binary invoked after upgrade">
Regenerate the wrapper so the embedded executable path matches the installed `rift`. Global npm/bun installs can change the resolved binary location between versions.
</Accordion>

## Related pages

<CardGroup cols={2}>
<Card title="Quickstart" href="/quickstart">
Initialize, create, list, and remove workspaces — the commands the shell wrapper augments.
</Card>
<Card title="CLI reference" href="/cli-reference">
Full subcommand inventory, hidden `--shell-cwd` and `--database` flags, and stdout/stderr conventions.
</Card>
<Card title="Initialize a workspace" href="/initialize-workspace">
When `rift init` converts vs registers, and which stderr signals mean no automatic `cd`.
</Card>
<Card title="Manage and remove workspaces" href="/manage-workspaces">
Remove semantics, `--children`, `-f` root unregistration, and trash behavior that determine post-remove destinations.
</Card>
</CardGroup>

---

## 12. Postcreate hooks

> Configure `.rift.toml` v1 postcreate hooks, hook environment variables, sequential execution, and failure behavior after workspace registration.

- Page Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/12-postcreate-hooks.md
- Generated: 2026-06-17T23:34:13.797Z

### Source Files

- `README.md`
- `specs.md`
- `crates/core/src/config.rs`
- `crates/core/src/hook.rs`
- `crates/core/src/lib.rs`

---
title: "Postcreate hooks"
description: "Configure `.rift.toml` v1 postcreate hooks, hook environment variables, sequential execution, and failure behavior after workspace registration."
---

Postcreate hooks are shell commands declared in the source workspace's `.rift.toml` and executed by `rift create` after the child workspace is copied, Git-prepared, and registered in the SQLite registry. Each `[[hooks.postcreate]]` entry runs sequentially in the new workspace root with injected `RIFT_*` environment variables; the first non-zero exit stops later hooks, but the created workspace remains on disk and registered.

## When hooks run

Hooks execute at the end of `create_with_options`, after these steps complete successfully:

1. Copy-on-write clone from the resolved source workspace (`from`)
2. `.rift` marker write on the destination
3. Git marker exclusion and detached `HEAD` preparation (when the workspace is a Git repository)
4. Registry insertion with parent provenance (`parent_id` → source rift `id`)

If any pre-hook step fails, the destination directory is removed and no hooks run. Hook failures occur only after registration succeeds.

```mermaid
sequenceDiagram
    participant CLI as rift create / FFI create
    participant Mgr as Manager::create_with_options
    participant Cfg as config::Config::load
    participant Strat as Strategy::copy_directory
    participant Reg as Registry::insert_child
    participant Hook as hook::run_postcreate

    CLI->>Mgr: Create + CreateOptions
    Mgr->>Cfg: load(source/.rift.toml) if HookMode::Run
    alt invalid config
        Cfg-->>CLI: InvalidConfig (no copy)
    end
    Mgr->>Strat: copy_directory(from → destination)
    Mgr->>Reg: insert_child(id, parent_id, destination)
    Mgr->>Hook: run_postcreate(steps, from, destination, id, parent_id)
    alt hook exits non-zero
        Hook-->>CLI: HookFailed (workspace stays registered)
    else all hooks succeed
        Hook-->>CLI: destination path
    end
```

<Note>
Config is loaded from the **source** workspace path (`from`), not re-parsed from the destination copy. The `.rift.toml` file is still copied into the child workspace as part of the tree clone.
</Note>

## Configuration

Place `.rift.toml` at the root of a managed source workspace. Rift supports schema version `1` only.

```toml
version = 1

[[hooks.postcreate]]
run = "pnpm install --frozen-lockfile"

[[hooks.postcreate]]
run = "pnpm run codegen"
```

### Schema

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `version` | `u32` | yes | Must be `1`; other values are rejected |
| `hooks.postcreate` | array | no | Ordered list of postcreate steps; defaults to empty |
| `hooks.postcreate[].run` | string | yes | Shell command executed in the destination root; trimmed; must be non-empty |

<ParamField body="version" type="integer" required>
Config schema version. Only `1` is accepted.
</ParamField>

<ParamField body="hooks.postcreate" type="array">
Ordered hook steps. Each `[[hooks.postcreate]]` table appends one command. Omitted or empty means no hooks run.
</ParamField>

<ParamField body="hooks.postcreate[].run" type="string" required>
Shell command string. Leading and trailing whitespace is trimmed. An empty string after trimming is rejected.
</ParamField>

### Validation rules

- **Unknown fields** are rejected (`deny_unknown_fields` on all config structs). For example, a `shell` key on a postcreate entry fails parsing.
- **Unsupported `version`** values produce `InvalidConfig`.
- **Missing `.rift.toml`** is valid: Rift treats the workspace as having no postcreate steps.
- **Invalid config is checked before copy** when hooks are enabled. A bad `.rift.toml` aborts `create` before any destination directory is created.

<Warning>
With hooks enabled (the default), fix `.rift.toml` validation errors before running `rift create`. With `--no-hooks` or `hooks: false`, invalid config is ignored because the file is never parsed.
</Warning>

## Execution environment

Each `run` command executes as a subprocess with:

| Property | Value |
| --- | --- |
| Working directory | Destination workspace root |
| Shell (Unix) | `sh -c "<run>"` |
| Shell (Windows) | `cmd /C "<run>"` |
| Stdio | Inherited from the calling process |
| Process environment | Inherited, plus injected `RIFT_*` variables below |

### Injected environment variables

| Variable | Value |
| --- | --- |
| `RIFT_SOURCE` | Canonical source workspace path (`from`) |
| `RIFT_DESTINATION` | Canonical destination workspace path |
| `RIFT_ID` | ULID of the newly created workspace |
| `RIFT_PARENT_ID` | ULID of the immediate source workspace |

Hook scripts can use these variables to reference provenance, write setup logs, or branch behavior based on parent identity.

### Sequential execution

Postcreate steps run in **array order**. Rift uses `try_for_each` over the parsed steps: the second hook does not start until the first exits successfully.

- A **non-zero exit code** stops remaining hooks and returns `HookFailed`.
- A **spawn failure** (command not found, permission error) also returns `HookFailed` with message `failed to start: …`.
- Successful hooks may mutate the destination filesystem; there is no automatic rollback of hook side effects.

## Skipping hooks

| Surface | Flag / option | Default |
| --- | --- | --- |
| CLI | `--no-hooks` | hooks run |
| JavaScript / Bun / Node FFI | `hooks: false` in `create()` | `hooks` defaults to `true` |
| Rust core | `CreateOptions::hook_mode(HookMode::Skip)` | `HookMode::Run` |

When hooks are skipped, Rift does not load `.rift.toml` at all—it uses an empty default config. This allows `create` to succeed even when the source contains an unsupported config version or malformed hooks, as long as the filesystem copy succeeds.

<CodeGroup>

```bash title="CLI"
rift create --no-hooks
rift create --name parser-fix --no-hooks
```

```ts title="JavaScript API"
import { create } from "rift-snapshot";

const workspace = create({
  from: process.cwd(),
  name: "schema-work",
  hooks: false,
});
```

</CodeGroup>

## Failure behavior

Postcreate hook failures differ from earlier `create` failures: the workspace **remains registered and on disk**.

| Failure phase | Destination on disk | Registry entry | `create` result |
| --- | --- | --- | --- |
| Invalid config (hooks enabled) | not created | none | `InvalidConfig` |
| Copy / marker / registry error | removed (best effort) | none | respective `RiftError` |
| Hook command fails | **kept** | **kept** | `HookFailed` |

On hook failure:

- Hooks that already completed keep their filesystem effects.
- Remaining hooks in the array are **not** executed.
- `rift list` on the source still includes the child path.
- CLI exits with code `1` and prints the error to stderr.
- FFI / JavaScript bindings surface `hook_failed` with the destination `path`.

<RequestExample>

```bash
rift create --name hook-failure
```

</RequestExample>

<ResponseExample>

```text
postcreate hook failed at /projects/.rifts/app/hook-failure: `exit 7` exited with exit status: 7
```

</ResponseExample>

Recovery after a hook failure:

1. Read the destination path from the stderr error message, or find it with `rift list` on the source workspace.
2. Fix the failing command or environment.
3. Re-run the setup manually, or remove the child with `rift remove` and retry `rift create`.

<Info>
Because the workspace is already registered, a retry with the same `--name` fails with `AlreadyExists` until the partial child is removed.
</Info>

## Error codes

| Code | When | `path` field |
| --- | --- | --- |
| `invalid_config` | `.rift.toml` parse or validation error while hooks are enabled | Path to `.rift.toml` |
| `hook_failed` | A postcreate command exited non-zero or failed to start | Destination workspace root |

CLI message format for hook failures:

```text
postcreate hook failed at {path}: `{command}` {message}
```

JavaScript consumers receive `RiftError` with `code: "hook_failed"` and optional `path`.

## Example workflows

<Steps>

<Step title="Add hooks to the source workspace">

Create `.rift.toml` at the registered source root (the workspace you copy **from**, not the `.rifts/` storage directory):

```toml
version = 1

[[hooks.postcreate]]
run = "npm ci"

[[hooks.postcreate]]
run = "npm run build"
```

</Step>

<Step title="Create a child workspace">

```bash
rift create --name feature-auth
```

Hooks run automatically after registration. Dependency install and build output appear on the inherited terminal streams.

</Step>

<Step title="Verify or recover">

On success, stdout prints the destination path. On hook failure, inspect the partial workspace, fix the command, or `rift remove` and recreate.

</Step>

</Steps>

### Using `RIFT_*` in a hook

```toml
version = 1

[[hooks.postcreate]]
run = 'echo "child $RIFT_ID from parent $RIFT_PARENT_ID" >> setup.log'
```

The log file is written in the destination root because that is the subprocess working directory.

## Related pages

<CardGroup>

<Card title="Create workspaces" href="/create-workspaces">
`rift create` flags, copy modes, and the full creation workflow that precedes hook execution.
</Card>

<Card title="Configuration reference" href="/configuration-reference">
Complete `.rift.toml` schema, validation rules, and skip-flag mapping across CLI and API surfaces.
</Card>

<Card title="Error codes" href="/error-codes">
`invalid_config` and `hook_failed` in the full `RiftError` catalog.
</Card>

<Card title="Troubleshooting" href="/troubleshooting">
Hook failures, partial workspaces, and recovery patterns.
</Card>

</CardGroup>

---

## 13. CLI reference

> All `rift` subcommands, flags, defaults, stdout/stderr behavior, hidden `--database` and `--shell-cwd` flags, and exit codes.

- Page Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/13-cli-reference.md
- Generated: 2026-06-17T23:35:11.744Z

### Source Files

- `crates/cli/src/main.rs`
- `README.md`
- `specs.md`
- `npm/rift-snapshot/bin/rift.js`

---
title: "CLI reference"
description: "All `rift` subcommands, flags, defaults, stdout/stderr behavior, hidden `--database` and `--shell-cwd` flags, and exit codes."
---

The `rift` binary is a thin Clap-driven wrapper around the `rift` core `Manager`. It parses eight subcommands, opens a SQLite registry (default or `--database`), delegates filesystem and registry work to the core library, and prints machine-readable paths on stdout while reserving human-oriented status for stderr. The npm `rift-snapshot` package forwards `process.argv` to a bundled prebuilt binary with inherited stdio.

## Command summary

| Subcommand | Positional argument | Default path | Opens registry |
| --- | --- | --- | --- |
| `shell-init <SHELL>` | — | — | No |
| `init [AT]` | `AT` workspace directory | Current working directory | Yes |
| `create [FROM]` | `FROM` source workspace | Current working directory | Yes |
| `remove [AT]` | `AT` workspace to remove | Current working directory | Yes |
| `list [OF]` | `OF` parent workspace | Current working directory | Yes |
| `ancestors [OF]` | `OF` child workspace | Current working directory | Yes |
| `gc` | — | — | Yes |

Workspace operations resolve paths by walking upward from the positional argument (or cwd) until a `.rift` marker is found, except `rift init` which applies Git-root or `--here` selection before calling core `init` on an exact path.

## Global and hidden flags

Two flags are defined on the root `Cli` parser and hidden from `--help`:

<ParamField body="--database" type="path">
Optional SQLite registry path. When omitted, the CLI opens `{data_local_dir}/rift/rift.sqlite` (typically `~/.local/share/rift/rift.sqlite` on Linux when `XDG_DATA_HOME` is unset). Must appear **before** the subcommand; it is not marked `global`.
</ParamField>

<ParamField body="--shell-cwd" type="boolean" default="false">
Global hidden flag consumed by shell wrappers from `rift shell-init`. When set, `init`, `create`, and `remove` emit a target directory on **stdout** for the wrapper to `cd` into, and move human-readable status lines to **stderr**. Other subcommands ignore it.
</ParamField>

## `rift shell-init`

```
rift shell-init <SHELL>
```

<ParamField body="SHELL" type="enum" required>
Shell integration target. Accepted values: `bash`, `zsh`, `nushell`.
</ParamField>

Prints a shell function definition to **stdout**. The function intercepts `init`, `create`, and `remove`, re-invokes the real `rift` binary with `--shell-cwd`, and `cd`s into the path printed on stdout when non-empty. All other subcommands pass through unchanged.

Does not open the registry. Always exits `0` on success.

<RequestExample>

```bash
eval "$(rift shell-init zsh)"
```

</RequestExample>

## `rift init`

```
rift init [OPTIONS] [AT]
```

Prepares and registers a source workspace for copy-on-write child creation.

<ParamField body="AT" type="path">
Directory to initialize. Defaults to the current working directory after canonicalization.
</ParamField>

<ParamField body="--here" type="boolean" default="false">
Initialize exactly `AT` (or cwd). Without this flag, the CLI selects the nearest existing managed ancestor, restores a missing marker on a known root, or falls back to the nearest Git root (directory containing `.git`).
</ParamField>

### Defaults and behavior

- Opens the registry before running.
- On Linux btrfs, first-time init of an ordinary directory may convert it into a btrfs subvolume with progress on stderr.
- On other supported platforms, init registers the directory in place after verifying copy-on-write support.
- If the workspace is already registered and the marker exists, reports `Already initialized` without conversion.
- If the marker was deleted but the registry entry remains, restores the marker.

### Stdout and stderr

| Outcome | Stdout | Stderr |
| --- | --- | --- |
| Successful conversion | Empty, unless `--shell-cwd` and cwd is inside the initialized tree → initialized path | Progress lines during btrfs conversion; final `Ready <path>` |
| Already initialized | Empty | `Already initialized <path>` |
| Marker restored | Empty | `Restored marker <path>` |
| Registered without conversion | Empty | `Ready <path>` |
| Converted, cwd inside tree, no `--shell-cwd` | Empty | `run cd <path> to enter the initialized workspace` |
| Converted, cwd inside tree, `--shell-cwd` | `<path>` | Progress and `Ready` lines |

Btrfs conversion progress on stderr includes `Initializing`, `First-time setup can take a moment.`, `New rifts will be instant.`, `Creating BTRFS subvolume...`, and `Importing workspace...` as applicable.

## `rift create`

```
rift create [OPTIONS] [FROM]
```

Creates a copy-on-write child workspace from a managed source.

<ParamField body="FROM" type="path">
Source workspace. Defaults to cwd. Resolved upward to the nearest `.rift` marker.
</ParamField>

<ParamField body="--name" type="string">
Child directory name. When omitted, generates a random `adjective-noun` name (for example `amber-brook`).
</ParamField>

<ParamField body="--into" type="path">
Custom storage parent directory. When omitted, uses `<parent-of-root>/.rifts/<root-basename>/`.
</ParamField>

<ParamField body="--copy-all" type="boolean" default="false">
Use exact copy mode (`CopyMode::All`). Default is filtered copy (`CopyMode::Filtered`), which omits heavyweight regenerable artifacts such as `node_modules`, `target`, `.venv`, `.next`, `dist`, `build`, and `coverage`.
</ParamField>

<ParamField body="--no-hooks" type="boolean" default="false">
Skip `.rift.toml` postcreate hooks (`HookMode::Skip`). Default runs configured hooks after registration (`HookMode::Run`).
</ParamField>

### Stdout and stderr

| Mode | Stdout | Stderr |
| --- | --- | --- |
| Default | New workspace path (one line) | Empty |
| `--shell-cwd` | New workspace path (one line) | `created <path>` |

On Git repositories, the child receives detached `HEAD` at the source commit with preserved index and working-tree state. If a postcreate hook fails, the workspace remains registered and the command exits `1` with a `postcreate hook failed at <path>: \`<command>\` <message>` error on stderr.

## `rift remove`

```
rift remove [OPTIONS] [AT]
```

Logically deletes a created rift (moves to `.trash`) or unregisters a source root.

<ParamField body="AT" type="path">
Workspace to remove. Defaults to cwd, resolved upward via `.rift`.
</ParamField>

<ParamField body="--children" type="boolean" default="false">
Trash all managed descendants of `AT` but preserve `AT` itself. Maps to core `remove_all`.
</ParamField>

<ParamField body="-f, --force" type="boolean" default="false">
Required when removing a registered **source root** (workspace with no parent). Without `-f`, the CLI exits `1` before touching the filesystem.
</ParamField>

### Stdout and stderr

**Single remove** (default, not `--children`):

| Case | Stdout | Stderr |
| --- | --- | --- |
| Remove created rift, no `--shell-cwd` | Empty | Empty |
| Remove created rift, `--shell-cwd`, cwd inside removed tree | Parent workspace path (if applicable) | `removed <path>` |
| Unregister source root (`-f`) | Empty (unless `--shell-cwd` emits destination) | `Unregistered <path>`; with `--shell-cwd`, may also print destination |

**`--children` mode:**

| Mode | Per removed descendant | If cwd inside a removed path |
| --- | --- | --- |
| Default | One path per line on **stdout** | — |
| `--shell-cwd` | `removed <path>` on **stderr** | `AT` path on **stdout** |

<Warning>
Unregistering a source root with `-f` deletes the `.rift` marker, trashes all registered descendants, and removes the active registry tree. The source directory itself remains on disk.
</Warning>

Force-required error on stderr:

```text
This is the root workspace.

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

## `rift list`

```
rift list [OF]
```

<ParamField body="OF" type="path">
Parent workspace. Defaults to cwd.
</ParamField>

Prints one direct active child workspace path per line on **stdout**, in registry order. Empty output when there are no children. No stderr on success.

## `rift ancestors`

```
rift ancestors [OF]
```

<ParamField body="OF" type="path">
Child workspace. Defaults to cwd.
</ParamField>

Prints ancestor workspace paths on **stdout**, nearest parent first, up to the registered source root. One path per line.

## `rift gc`

```
rift gc
```

Physically deletes directories previously moved to Rift-owned `.trash` storage and prunes registry entries for active workspaces whose directories were removed outside Rift (when safe). Prints each deleted or pruned path on **stdout**, one per line.

## Exit codes

| Code | Source | When |
| --- | --- | --- |
| `0` | Rust CLI | Successful command completion |
| `1` | Rust CLI | Any runtime error (`rift::Error`, I/O, or `ForceRequired`); message on stderr |
| `2` | Clap | Parse errors: unknown subcommand, missing required arguments, invalid flag usage |
| `1` | npm `rift.js` | Unsupported platform/arch, missing prebuilt binary, or spawn failure |
| Child status | npm `rift.js` | Forwards the bundled binary exit code; treats `null` as `1` |

The Rust `main` function always prints errors to **stderr** via `eprintln!` and calls `std::process::exit(1)`. Clap handles its own usage errors before `run()` executes.

### CLI error message remapping

Three core errors are rewritten for CLI users; all other `rift::Error` variants use `Display` as-is on stderr:

| Core error | CLI stderr message |
| --- | --- |
| `InitializationRequired` | `this workspace must be initialized first; run \`rift init\` from its root folder` |
| `WorkspaceNotInitialized` | `no initialized workspace found; run \`rift init\` from the root folder` |
| `MissingMarker` | `this workspace is missing its \`.rift\` marker; run \`rift init\` to restore it` |

Common unmapped errors include `copy-on-write cloning unavailable: …`, `unsafe Git source: …`, `postcreate hook failed at …`, `cannot remove subtree while a recorded rift path is missing: …`, and `rift directory already exists: …`.

## npm launcher (`rift-snapshot`)

The `rift-snapshot` npm package installs `bin/rift.js`, which:

1. Resolves `prebuilds/<platform>-<arch>/rift` (or `rift.exe` on Windows).
2. Spawns the binary with `process.argv.slice(2)` and `stdio: "inherit"`.
3. Exits with the child process status.

Unsupported platform or missing binary messages go to stderr with exit `1`. All CLI flags and subcommands are identical to the native binary; hidden flags are not exposed in `--help` but are accepted when passed explicitly.

## `--shell-cwd` protocol

Shell wrappers generated by `shell-init` depend on a strict stdout/stderr split:

```text
rift --shell-cwd <subcommand> [args]
  │
  ├─ stdout: single path line to cd into (may be empty)
  └─ stderr: human status (created/removed/ready messages)
```

| Subcommand | stdout when `--shell-cwd` | stderr when `--shell-cwd` |
| --- | --- | --- |
| `init` (converted, cwd inside) | Initialized workspace path | Progress and `Ready` lines |
| `create` | New child path | `created <path>` |
| `remove` (created rift) | Parent path if cwd was inside removed tree | `removed <path>` |
| `remove --children` | `AT` if cwd inside any removed descendant | `removed <path>` per descendant |
| `remove -f` (root) | Destination if cwd inside removed tree | `Unregistered <path>` |

The wrapper runs `cd` only when stdout is non-empty after trimming.

## Related pages

<CardGroup>
<Card title="Quickstart" href="/quickstart">
Initialize a workspace, create a child, list it, and remove it with expected stdout signals.
</Card>
<Card title="Shell integration" href="/shell-integration">
Install Bash, Zsh, or Nushell wrappers via `rift shell-init`.
</Card>
<Card title="Error codes" href="/error-codes">
Complete `RiftError` catalog with FFI codes and path-bearing variants.
</Card>
<Card title="Installation" href="/installation">
Install via npm global package or Cargo build script.
</Card>
</CardGroup>

---

## 14. JavaScript API reference

> `rift-snapshot` exports, Bun vs Node conditional bindings, FFI request protocol, function signatures, and Node.js 26.1+ FFI requirements.

- Page Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/14-javascript-api-reference.md
- Generated: 2026-06-17T23:35:00.024Z

### Source Files

- `npm/rift-snapshot/index.d.ts`
- `npm/rift-snapshot/bun/index.js`
- `npm/rift-snapshot/node/index.js`
- `npm/rift-snapshot/package.json`
- `crates/ffi/src/lib.rs`
- `README.md`

---
title: "JavaScript API reference"
description: "`rift-snapshot` exports, Bun vs Node conditional bindings, FFI request protocol, function signatures, and Node.js 26.1+ FFI requirements."
---

The `rift-snapshot` npm package exposes six workspace operations (`init`, `create`, `remove`, `list`, `ancestors`, `gc`) and a `RiftError` class through conditional exports that load platform-specific prebuilt shared libraries from `prebuilds/<platform>-<arch>/`. Each JavaScript call serializes a JSON request, invokes the Rust `rift_ffi_call` symbol, parses the JSON response, and either returns a typed value or throws `RiftError`.

## Package surface

| Export | Type | Role |
| --- | --- | --- |
| `init` | function | Register or restore a managed workspace at an exact path |
| `create` | function | Create a copy-on-write child workspace |
| `remove` | function | Trash a workspace subtree, or unregister a source root |
| `list` | function | List direct child workspaces |
| `ancestors` | function | List parent workspaces, nearest first |
| `gc` | function | Physically delete trashed workspaces and prune stale registry entries |
| `RiftError` | class | Typed error with `code`, `message`, and optional `path` |

The package also ships a CLI shim at `bin/rift.js` that spawns the bundled `rift` executable. The JavaScript API does not use that binary; it calls the FFI shared library directly.

<Note>
Install the package globally or as a project dependency. See [Installation](/installation) for platform matrices and prebuild layout.
</Note>

## Conditional exports

`package.json` routes imports by runtime:

```json
"exports": {
  ".": {
    "types": "./index.d.ts",
    "bun": "./bun/index.js",
    "node": "./node/index.js",
    "default": "./node/index.js"
  }
}
```

<Tabs>
<Tab title="Bun">

The Bun binding (`bun/index.js`) uses `bun:ffi` with `dlopen`, `ptr`, and `CString`. It resolves the native library through a dynamic `import` of the prebuild asset with `{ with: { type: "file" } }`, then passes null-terminated UTF-8 request bytes to `rift_ffi_call`.

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

No extra runtime flags are required beyond a supported Bun version with FFI support.

</Tab>
<Tab title="Node.js">

The Node binding (`node/index.js`) uses the experimental `node:ffi` module. It resolves the library path with `fileURLToPath` and `fs.existsSync`, then passes the request as a JavaScript string to `rift_ffi_call`.

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

Node.js **26.1 or later** is required. Start the process with the experimental FFI flag:

```bash
node --experimental-ffi app.mjs
```

<Warning>
When Node's permission model is enabled, also pass `--allow-ffi` or the `dlopen` call fails at startup.
</Warning>

</Tab>
</Tabs>

### Supported platforms

The package declares `os: ["darwin", "linux", "win32"]` and `cpu: ["arm64", "x64"]`. Both bindings map `os.platform()` and `os.arch()` to prebuild directories:

| Platform key | Library filename |
| --- | --- |
| `linux-x64` | `librift_ffi.so` |
| `darwin-x64` | `librift_ffi.dylib` |
| `darwin-arm64` | `librift_ffi.dylib` |
| `windows-x64` | `rift_ffi.dll` |

Unsupported platform/architecture pairs throw at module load time. A missing prebuild file on Node throws `Unable to locate the Rift Node library for <platform>-<arch>. Reinstall rift-snapshot.`

:::files
npm/rift-snapshot/
├── index.d.ts          # Shared TypeScript declarations
├── bun/index.js        # Bun FFI binding
├── node/index.js       # Node FFI binding
├── bin/rift.js         # CLI launcher (separate from API)
└── prebuilds/
    ├── linux-x64/      # rift + librift_ffi.so
    ├── darwin-x64/     # rift + librift_ffi.dylib
    ├── darwin-arm64/   # rift + librift_ffi.dylib
    └── windows-x64/    # rift.exe + rift_ffi.dll
:::

## FFI request protocol

JavaScript bindings and the Rust `rift-ffi` crate communicate through a single JSON request/response envelope. Two C symbols are exported:

| Symbol | Contract |
| --- | --- |
| `rift_ffi_call(input)` | Accept a null-terminated request buffer; return an allocated response buffer |
| `rift_ffi_free(output)` | Free a buffer previously returned by `rift_ffi_call` |

```mermaid
sequenceDiagram
    participant JS as bun/index.js or node/index.js
    participant FFI as librift_ffi (rift_ffi_call)
    participant Core as Manager (rift core)

    JS->>FFI: JSON request (+ optional database path)
    FFI->>Core: deserialize Command, open Manager
    Core-->>FFI: Result<Value, Failure>
    FFI-->>JS: JSON response pointer
    JS->>FFI: rift_ffi_free(pointer)
    alt status = ok
        JS-->>JS: return response.value
    else status = error
        JS-->>JS: throw new RiftError(response.error)
    end
```

### Request shape

Every request is a JSON object with an optional `database` field and a `command` discriminator:

<ParamField body="database" type="string">
Optional absolute path to the SQLite registry file. When omitted, the core opens the default database at `<user-data-dir>/rift/rift.sqlite`.
</ParamField>

Commands use `snake_case` names in the wire format. The JavaScript bindings translate camelCase option names where needed.

| `command` | Fields | Maps to |
| --- | --- | --- |
| `init` | `at` | `Manager::init` |
| `create` | `from`, `name`, `into`, `copyAll`, `hooks` | `Manager::create_with_options` |
| `remove` | `at`, `all` | `Manager::remove` or `Manager::remove_all` |
| `list` | `of` | `Manager::list` |
| `ancestors` | `of` | `Manager::ancestors` |
| `gc` | _(none)_ | `Manager::gc` |

<RequestExample>

```json
{
  "command": "create",
  "from": "/home/dev/app",
  "name": "schema-work",
  "copyAll": false,
  "hooks": true
}
```

</RequestExample>

### Response shape

Responses are tagged by `status`:

<ResponseField name="status" type="string">
Either `ok` or `error`.
</ResponseField>

<ResponseField name="value" type="null | string | string[]">
Present when `status` is `ok`. Shape depends on the command (see return values below).
</ResponseField>

<ResponseField name="error" type="object">
Present when `status` is `error`. Contains `code`, `message`, and optional `path`.
</ResponseField>

<ResponseExample>

```json
{
  "status": "ok",
  "value": "/home/dev/.rifts/app/schema-work"
}
```

</ResponseExample>

<ResponseExample>

```json
{
  "status": "error",
  "error": {
    "code": "workspace_not_initialized",
    "message": "workspace is not initialized: /tmp/app",
    "path": "/tmp/app"
  }
}
```

</ResponseExample>

The FFI layer also returns protocol-level errors not produced by workspace logic:

| Code | When |
| --- | --- |
| `invalid_request` | Malformed JSON, invalid UTF-8, or null input pointer |
| `panic` | Unwound Rust panic inside `rift_ffi_call` |
| `serialization` | Response JSON could not be serialized, or contained an interior null byte |

## Function reference

TypeScript declarations live in `index.d.ts`. Runtime defaults are applied in the Bun and Node binding implementations.

### `init(options?)`

<ParamField body="at" type="string" default="process.cwd()">
Directory to initialize exactly. Unlike the CLI, there is no Git-root selection or `--here` resolution — pass the precise path you want registered.
</ParamField>

<ParamField body="database" type="string">
Optional registry database path.
</ParamField>

**Returns:** `null`

Registers the directory in the SQLite registry, writes or restores the `.rift` marker, and runs platform-specific initialization (btrfs subvolume conversion, reflink verification, or APFS registration). Idempotent when the workspace is already registered.

### `create(options?)`

<ParamField body="from" type="string" default="process.cwd()">
Source managed workspace. Resolved upward through `.rift` markers.
</ParamField>

<ParamField body="name" type="string">
Child workspace name. When omitted, the core generates a readable random name.
</ParamField>

<ParamField body="into" type="string">
Custom storage parent directory. When omitted, storage defaults to the sibling `.rifts` layout described in [Storage layout](/storage-layout).
</ParamField>

<ParamField body="copyAll" type="boolean" default="false">
`false` uses filtered copy (omits regenerable artifacts such as `node_modules` and `target`). `true` preserves the full tree.
</ParamField>

<ParamField body="hooks" type="boolean" default="true">
`true` runs `.rift.toml` postcreate hooks after registration. `false` skips hooks entirely.
</ParamField>

<ParamField body="database" type="string">
Optional registry database path.
</ParamField>

**Returns:** `string` — absolute path to the new workspace.

Filtered vs exact copy behavior and platform backends are described in [Copy strategies and platforms](/copy-strategies). Hook configuration is in [Configuration reference](/configuration-reference).

### `remove(options?)`

<ParamField body="at" type="string" default="process.cwd()">
Workspace to remove. Resolved upward through `.rift` markers.
</ParamField>

<ParamField body="all" type="boolean" default="false">
`false` trashes the selected workspace (or unregisters a source root). `true` trashes all descendants while preserving the selected workspace — equivalent to CLI `rift remove --children`.
</ParamField>

<ParamField body="database" type="string">
Optional registry database path.
</ParamField>

**Returns:**

| `all` | Return type | Behavior |
| --- | --- | --- |
| `false` | `void` | Trash the workspace, or unregister a source root and trash its descendants |
| `true` | `string[]` | Trash descendant paths; selected workspace remains active |

<Warning>
Removing a source root through the JavaScript API does not require a force flag. The CLI blocks root unregistration unless `-f` is passed; the FFI `remove` command calls `Manager::remove` directly, which unregisters roots without that guard.
</Warning>

### `list(options?)`

<ParamField body="of" type="string" default="process.cwd()">
Workspace whose direct children to list.
</ParamField>

<ParamField body="database" type="string">
Optional registry database path.
</ParamField>

**Returns:** `string[]` — absolute paths of direct active child workspaces.

### `ancestors(options?)`

<ParamField body="of" type="string" default="process.cwd()">
Workspace whose ancestry to trace.
</ParamField>

<ParamField body="database" type="string">
Optional registry database path.
</ParamField>

**Returns:** `string[]` — parent workspace paths, nearest parent first.

### `gc(options?)`

<ParamField body="database" type="string">
Optional registry database path.
</ParamField>

**Returns:** `string[]` — absolute paths physically deleted from trash storage.

## Error handling

Operation failures throw `RiftError`, a subclass of `Error` with:

<ResponseField name="code" type="RiftErrorCode">
Machine-readable error identifier.
</ResponseField>

<ResponseField name="message" type="string">
Human-readable description from the Rust error.
</ResponseField>

<ResponseField name="path" type="string">
Optional filesystem path associated with the error (workspace roots, markers, hook targets).
</ResponseField>

```ts
import { create, RiftError } from "rift-snapshot";

try {
  create({ from: "/uninitialized/project" });
} catch (error) {
  if (error instanceof RiftError) {
    console.error(error.code);    // e.g. "workspace_not_initialized"
    console.error(error.path);    // e.g. "/uninitialized/project"
  }
}
```

The complete code catalog, including path-bearing variants and CLI message mappings, is in [Error codes](/error-codes).

Non-`RiftError` exceptions can occur before the FFI boundary:

- Unsupported platform at module load
- Missing prebuild library file (Node)
- `Rift native library returned no response` when `rift_ffi_call` returns a null pointer

## API vs CLI

The JavaScript API and the `rift` CLI share the same Rust `Manager` implementation but differ in ergonomics:

| Behavior | JavaScript API | CLI |
| --- | --- | --- |
| `init` path selection | Exact `at` path only | Git-root or existing Rift root unless `--here` |
| `create` output | Return value | Prints path to stdout |
| `remove` on source root | Always permitted | Requires `-f` |
| `remove` descendants | `remove({ all: true })` | `rift remove --children` |
| Progress reporting | None | stderr progress during btrfs conversion |
| Shell `cd` after operations | Not available | `rift shell-init` wrapper |
| Registry override | `database` option on every call | Hidden global `--database` flag |

For stdout signals, exit codes, and the full flag inventory, see [CLI reference](/cli-reference).

## Example workflow

<CodeGroup>

```ts title="workspace.mjs"
import { init, create, list, remove, gc } from "rift-snapshot";

// Initialize exactly this directory (not Git-root selection)
init({ at: "/home/dev/my-app" });

// Create a filtered child workspace with hooks enabled (default)
const child = create({
  from: "/home/dev/my-app",
  name: "feature-x",
});

console.log(list({ of: "/home/dev/my-app" }));
// → ["/home/dev/.rifts/my-app/feature-x"]

remove({ at: child });
gc();
```

```bash title="Run on Node.js 26.1+"
node --experimental-ffi workspace.mjs
```

```bash title="Run on Bun"
bun workspace.mjs
```

</CodeGroup>

<Check>
After `create`, the returned string is the canonical absolute path. Use it as `at` in subsequent `remove` calls rather than reconstructing storage paths manually.
</Check>

## Related pages

<CardGroup>
<Card title="Installation" href="/installation">
Install `rift-snapshot` and understand the prebuild binary layout bundled with the package.
</Card>
<Card title="Quickstart" href="/quickstart">
End-to-end init → create → list → remove workflow with expected outputs.
</Card>
<Card title="CLI reference" href="/cli-reference">
Subcommands, flags, and behaviors that differ from the JavaScript API.
</Card>
<Card title="Error codes" href="/error-codes">
Complete `RiftError` code catalog from FFI and TypeScript bindings.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
`.rift.toml` postcreate hooks controlled by the `hooks` create option.
</Card>
</CardGroup>

---

## 15. Configuration reference

> `.rift.toml` schema: `version`, `hooks.postcreate` array, validation rules, and CLI/API hook skip flags.

- Page Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/15-configuration-reference.md
- Generated: 2026-06-17T23:35:15.883Z

### Source Files

- `crates/core/src/config.rs`
- `specs.md`
- `README.md`
- `crates/core/src/hook.rs`

---
title: "Configuration reference"
description: "`.rift.toml` schema: `version`, `hooks.postcreate` array, validation rules, and CLI/API hook skip flags."
---

Rift reads an optional `.rift.toml` file from the **source workspace root** during `rift create`. When hooks are enabled (the default), the file is parsed and validated before any copy starts; configured `hooks.postcreate` commands run sequentially in the new workspace after copy, Git preparation, and registry insertion. Use `--no-hooks` on the CLI or `hooks: false` in the JavaScript API to skip config loading and hook execution entirely.

## File location

| Property | Value |
| --- | --- |
| Filename | `.rift.toml` |
| Directory | Root of the source workspace resolved by `rift create` (the managed workspace whose `.rift` marker was found by upward search) |
| Required | No — a missing file yields an empty hook list |
| Copied to child | Yes — `.rift.toml` is not in the filtered-copy exclusion set and is copied into every new workspace |

<Info>
Config is read from the **source** (`from`), not from the destination. The child workspace receives a copy of the file; hooks execute against the new path.
</Info>

## Schema (version 1)

Rift supports exactly one configuration version. The on-disk format is TOML.

```toml
version = 1

[[hooks.postcreate]]
run = "pnpm install --frozen-lockfile"

[[hooks.postcreate]]
run = "pnpm run codegen"
```

### Top-level fields

<ParamField body="version" type="integer" required>
Must be exactly `1`. Any other value returns `invalid_config` with message `unsupported config version {n}`.
</ParamField>

<ParamField body="hooks" type="object">
Optional. Defaults to an empty object. Only `hooks.postcreate` is recognized under `hooks`.
</ParamField>

### `hooks.postcreate` entries

Each `[[hooks.postcreate]]` table defines one shell command. Tables are executed in file order.

<ParamField body="run" type="string" required>
Shell command string. Leading and trailing whitespace is trimmed. An empty string after trimming is rejected with `postcreate run cannot be empty`.
</ParamField>

<Warning>
Unknown keys at any level are rejected. For example, a `shell` field on a postcreate entry fails parsing with `invalid_config`.
</Warning>

## Validation rules

Validation runs only when `HookMode::Run` is active (hooks not skipped). A failing validation aborts `create` **before** the workspace copy begins.

| Rule | Error when violated |
| --- | --- |
| File must parse as valid TOML | `invalid_config` — serde/TOML parse message |
| `version` must be present and equal to `1` | `invalid_config` — missing field or `unsupported config version {n}` |
| No unknown top-level or nested keys (`deny_unknown_fields`) | `invalid_config` — serde unknown-field message |
| Each `run` must be non-empty after trim | `invalid_config` — `postcreate run cannot be empty` |
| Empty `hooks.postcreate` array | Valid — no hooks run |
| Missing `.rift.toml` file | Valid — treated as zero hooks |

```text
create (hooks enabled)
  │
  ├─ load + validate .rift.toml at source
  │     └─ InvalidConfig → abort (no destination created)
  │
  ├─ copy workspace → write .rift marker → Git prep → registry insert
  │
  └─ run hooks.postcreate[] sequentially in destination
        └─ HookFailed → workspace remains registered; create returns error
```

## Hook execution

Postcreate hooks run after the destination workspace exists on disk and is registered in the SQLite registry.

| Behavior | Detail |
| --- | --- |
| Working directory | Destination workspace root |
| Stdio | Inherited from the parent `rift create` process |
| Shell (Unix) | `sh -c "<run>"` |
| Shell (Windows) | `cmd /C "<run>"` |
| Order | Sequential; first failure stops remaining hooks |
| On failure | Destination directory and registry record are **kept**; `create` returns `hook_failed` |

### Hook environment variables

Each `run` command receives the process environment plus:

| Variable | Value |
| --- | --- |
| `RIFT_SOURCE` | Absolute path of the source workspace |
| `RIFT_DESTINATION` | Absolute path of the new workspace |
| `RIFT_ID` | ULID of the new workspace |
| `RIFT_PARENT_ID` | ULID of the immediate parent workspace |

<Note>
Environment variables are injected per hook step. They are not available during config parsing — only at command execution time.
</Note>

## Skipping hooks

Hook execution is controlled by `HookMode` in the core library. Both the CLI and JavaScript bindings map user-facing flags to this mode.

| Surface | Skip flag | Default | Effect when skipped |
| --- | --- | --- | --- |
| CLI | `--no-hooks` | hooks run | `HookMode::Skip` — no config load, no validation, no hook execution |
| JavaScript / FFI | `hooks: false` | `hooks: true` (omitted) | Same as CLI |
| Core `CreateOptions` | `hook_mode(HookMode::Skip)` | `HookMode::Run` | Same as above |

<RequestExample>

```bash
rift create --name parser-fix --no-hooks
```

</RequestExample>

<RequestExample>

```ts
import { create } from "rift-snapshot";

const workspace = create({
  from: process.cwd(),
  name: "parser-fix",
  hooks: false,
});
```

</RequestExample>

When hooks are skipped:

- `.rift.toml` is **not** read or validated, so an invalid config file does not block creation.
- The file is still copied into the child workspace as part of the normal copy operation.
- No `RIFT_*` environment variables are set because no commands run.

<Tip>
Use `--no-hooks` / `hooks: false` for fast workspace spin-up when you plan to run setup commands manually, or when the source config is temporarily invalid but you still need a copy.
</Tip>

## Error codes

Config and hook failures surface through the standard Rift error types.

### `invalid_config`

Returned when `.rift.toml` fails parsing or semantic validation while hooks are enabled.

| Field | Content |
| --- | --- |
| `code` (FFI/JS) | `invalid_config` |
| `path` | Absolute path to `.rift.toml` (e.g. `/projects/app/.rift.toml`) |
| `message` | Human-readable detail (TOML error, version mismatch, empty `run`, etc.) |

CLI message format: `invalid rift config at {path}: {message}`

### `hook_failed`

Returned when a postcreate command exits non-zero or cannot be started.

| Field | Content |
| --- | --- |
| `code` (FFI/JS) | `hook_failed` |
| `path` | Destination workspace path |
| `message` | Includes the failing `run` value and exit detail (e.g. `` `exit 7` exited with exit status: 7 ``) |

CLI message format: `postcreate hook failed at {path}: \`{command}\` {message}`

<Warning>
A hook failure does not roll back the created workspace. The child remains on disk and in the registry. Remove it with `rift remove` if the partial setup is unusable.
</Warning>

## Complete example

This configuration installs dependencies and runs codegen in every new workspace created from the source root:

```toml
version = 1

[[hooks.postcreate]]
run = "pnpm install --frozen-lockfile"

[[hooks.postcreate]]
run = "pnpm run codegen"
```

```bash
cd ~/code/app
rift create --name feature-x
```

Expected flow:

1. Rift validates `.rift.toml` at `~/code/app/.rift.toml`.
2. A filtered copy-on-write child is created under default storage (e.g. `~/code/.rifts/app/feature-x/`).
3. `.rift` marker is written, Git `HEAD` is detached if applicable, registry record is inserted.
4. `pnpm install --frozen-lockfile` runs in the child with `RIFT_DESTINATION` set to the child path.
5. On success, `pnpm run codegen` runs.
6. The child path is printed to stdout.

## Related pages

<CardGroup>
  <Card title="Postcreate hooks" href="/postcreate-hooks">
    Hook lifecycle, environment variables, failure behavior, and practical setup patterns.
  </Card>
  <Card title="Create workspaces" href="/create-workspaces">
    `rift create` flags including `--no-hooks`, copy modes, and storage options.
  </Card>
  <Card title="JavaScript API reference" href="/javascript-api">
    `create({ hooks })` signature, FFI protocol, and `RiftError` codes.
  </Card>
  <Card title="Error codes" href="/error-codes">
    Full `invalid_config` and `hook_failed` catalog with path-bearing variants.
  </Card>
</CardGroup>

---

## 16. Error codes

> Complete `RiftError` code catalog from FFI and TypeScript bindings, CLI user-facing messages, and path-bearing error variants.

- Page Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/16-error-codes.md
- Generated: 2026-06-17T23:36:08.962Z

### Source Files

- `npm/rift-snapshot/index.d.ts`
- `crates/ffi/src/lib.rs`
- `crates/core/src/lib.rs`
- `crates/cli/src/main.rs`

---
title: "Error codes"
description: "Complete `RiftError` code catalog from FFI and TypeScript bindings, CLI user-facing messages, and path-bearing error variants."
---

Rift surfaces failures through a single snake_case `code` string. The Rust core defines `rift::Error`; the FFI layer maps each variant to a code and optional `path`; the `rift-snapshot` package throws `RiftError` with the same fields. The CLI prints customized guidance for three workspace-state codes and a separate `ForceRequired` guard that never appears in FFI or JavaScript.

```mermaid
sequenceDiagram
  participant App as Application
  participant JS as rift-snapshot
  participant FFI as rift_ffi_call
  participant Core as rift::Error

  App->>JS: init / create / remove / ...
  JS->>FFI: JSON request
  FFI->>Core: Manager operation
  alt success
    Core-->>FFI: Ok(value)
    FFI-->>JS: {"status":"ok","value":...}
    JS-->>App: result
  else core failure
    Core-->>FFI: Err(Error)
    FFI-->>JS: {"status":"error","error":{code,message,path?}}
    JS-->>App: throw RiftError
  else protocol failure
    FFI-->>JS: {"status":"error","error":{code,message}}
    JS-->>App: throw RiftError
  end
```

## Error object shape

FFI error responses use this JSON structure:

```json
{
  "status": "error",
  "error": {
    "code": "workspace_not_initialized",
    "message": "workspace is not initialized: /tmp/app",
    "path": "/tmp/app"
  }
}
```

The `path` field is omitted when the variant carries no filesystem location. Protocol-layer failures (`invalid_request`, `panic`, `serialization`) never include `path`.

<TypeScript bindings>

```ts
export class RiftError extends Error {
  code: RiftErrorCode
  path?: string
  constructor(input: { code: RiftErrorCode; message: string; path?: string })
}
```

Both Node and Bun bindings parse the FFI JSON response and throw `new RiftError(response.error)` when `status === "error"`. The `message` string always comes from the Rust `Display` implementation (or FFI protocol text); `hook_failed` and `invalid_config` embed extra detail in `message` only.

</TypeScript bindings>

<Note>
Binding load failures are ordinary JavaScript `Error` objects, not `RiftError`: unsupported platform, missing prebuilt library, or a null FFI response.
</Note>

## Complete code catalog

### Core domain codes

These codes originate in `rift::Error` and are shared across CLI, FFI, and JavaScript.

| Code | Core variant | `path` in FFI/JS | Default `message` template |
|------|--------------|------------------|----------------------------|
| `io` | `Io` | — | Underlying `std::io::Error` text |
| `database` | `Database` | — | Underlying `rusqlite::Error` text |
| `walk` | `Walk` | — | Underlying `walkdir::Error` text |
| `invalid_path` | `Path` | — | `invalid path: {detail}` |
| `cow_unavailable` | `CowUnavailable` | — | `copy-on-write cloning unavailable: {detail}` |
| `initialization_required` | `InitializationRequired` | source path | `workspace requires initialization: {path}` |
| `workspace_not_initialized` | `WorkspaceNotInitialized` | queried path | `workspace is not initialized: {path}` |
| `missing_marker` | `MissingMarker` | directory path | `rift marker is missing: {path}` |
| `unsupported_entry` | `UnsupportedEntry` | entry path | `unsupported filesystem entry: {path}` |
| `unsafe_git` | `UnsafeGit` | — | `unsafe Git source: {detail}` |
| `not_managed` | `NotManaged` | path | `directory is not managed by rift: {path}` |
| `marker_mismatch` | `MarkerMismatch` | directory path | `rift marker does not match the registry at: {path}` |
| `unknown_marker` | `UnknownMarker` | directory path | `rift marker belongs to an unknown registry entry at: {path}` |
| `already_exists` | `AlreadyExists` | destination path | `rift directory already exists: {path}` |
| `missing_rift` | `MissingRift` | expected path | `cannot remove subtree while a recorded rift path is missing: {path}` |
| `inside_source` | `InsideSource` | destination path | `cannot copy a workspace into itself: {path}` |
| `invalid_config` | `InvalidConfig` | config file path | `invalid rift config at {path}: {message}` |
| `hook_failed` | `HookFailed` | destination workspace | `postcreate hook failed at {path}: \`{command}\` {message}` |

### FFI protocol codes

These codes are produced only inside the FFI crate and do not have a `rift::Error` counterpart.

| Code | When raised | Example `message` |
|------|-------------|-------------------|
| `invalid_request` | Null input pointer, invalid UTF-8, or JSON deserialization failure | `rift_ffi_call received a null request` |
| `panic` | Unwind inside `rift_ffi_call` | `rift FFI call panicked` |
| `serialization` | Response JSON serialization failure, or response contains an interior null byte | `failed to serialize response` / `response contained an interior null byte` |

## Path-bearing variants

Twelve core codes attach a `path` field in FFI and JavaScript. Use `error.path` for programmatic handling; read `error.message` for the full human-readable string.

| Code | What `path` identifies |
|------|------------------------|
| `initialization_required` | Source workspace that must be converted (btrfs subvolume init) before `create` |
| `workspace_not_initialized` | Directory where no `.rift` marker chain resolves to a registry record |
| `missing_marker` | Directory registered in SQLite but missing its `.rift` file |
| `unsupported_entry` | Non-file, non-directory, non-symlink entry encountered during copy |
| `not_managed` | Path referenced in ancestry walk with no registry parent |
| `marker_mismatch` | Directory whose `.rift` ID does not match the registry record at that path |
| `unknown_marker` | Directory with a `.rift` ID absent from the registry |
| `already_exists` | Destination rift path or trash relocation target that already exists |
| `missing_rift` | Registry-active child path missing on disk during remove |
| `inside_source` | Proposed child destination that falls inside the source tree |
| `invalid_config` | `.rift.toml` file path (typically `{workspace}/.rift.toml`) |
| `hook_failed` | Newly created child workspace where the hook ran (`RIFT_DESTINATION`) |

<Warning>
`hook_failed` and `invalid_config` expose the failing command and validation detail only in `message`, not as separate JSON fields. Parse `message` for display; branch on `code` and `path` in application logic.
</Warning>

## Common `message` details by code

### `invalid_path`

| Trigger | `message` detail |
|---------|------------------|
| Path is not a directory | `not a directory: {path}` |
| Invalid `--name` / `name` option | `invalid rift name: {name}` |
| Default database location unavailable | `user data directory is unavailable` |
| Relative `--into` resolution | `workspace has no parent`, `workspace has no name`, `rift has no parent`, `rift has no name` |
| Trash relocation | `trash path has no parent` |
| Null bytes in paths | `path contains a null byte: {path}` |
| btrfs ioctl name validation | `invalid btrfs subvolume name: {path}` |
| Registry UTF-8 constraint | `path is not valid UTF-8: {path}` |

### `cow_unavailable`

| Platform / context | Typical `message` detail |
|--------------------|--------------------------|
| Windows and other unsupported OS | `no copy-on-write strategy has been implemented for this platform` |
| Linux btrfs, wrong filesystem | `Linux snapshot creation requires btrfs; {path} is on another filesystem` |
| Linux btrfs `init` on non-btrfs | `{path} is not on a btrfs filesystem` |
| Linux reflink, cross-device | `Linux reflinks require source and destination on the same filesystem: {path}` |
| Linux reflink probe failure | `{path} does not support Linux copy-on-write reflinks: ...` |
| APFS `clonefile` failure | `failed to clone {path}: {os_error}` |
| Linux per-file reflink failure | `failed to reflink {path}: {os_error}` |
| btrfs snapshot/subvolume ioctl failure | `failed to {action} {path}: {os_error}` |
| btrfs init activation rollback | `failed to activate initialized workspace; restored the original workspace: ...` |

### `unsafe_git`

Raised by `git::check_source` before `init` or `create` when the source has a `.git` entry that is not a directory (linked worktree) or when any of these in-progress states exist:

`MERGE_HEAD`, `CHERRY_PICK_HEAD`, `REVERT_HEAD`, `BISECT_LOG`, `rebase-merge`, `rebase-apply`, `index.lock`, `HEAD.lock`

| Condition | `message` detail |
|-----------|------------------|
| Linked worktree `.git` file | `linked Git worktree sources are not supported` |
| In-progress Git operation | `Git state in progress: {state}` |

### `invalid_config`

Raised when `.rift.toml` exists but fails validation:

- TOML parse errors (serde message appended)
- `version` other than `1` → `unsupported config version {n}`
- Empty `hooks.postcreate[].run` → `postcreate run cannot be empty`
- Unknown fields (denied by schema) → serde unknown-field error

### `hook_failed`

Raised after the child workspace is registered when a postcreate hook exits non-zero or fails to start:

| Condition | `message` detail |
|-----------|------------------|
| Command cannot spawn | `` `{command}` failed to start: {error} `` |
| Non-zero exit | `` `{command}` exited with {status} `` |

Hook environment variables: `RIFT_SOURCE`, `RIFT_DESTINATION`, `RIFT_ID`, `RIFT_PARENT_ID`.

## CLI behavior

On failure the CLI writes the error message to **stderr** and exits with code **1**. Most codes print the core `Display` string unchanged.

Three workspace-state codes are rewritten for operator guidance:

| Code | CLI stderr message |
|------|-------------------|
| `initialization_required` | `this workspace must be initialized first; run \`rift init\` from its root folder` |
| `workspace_not_initialized` | `no initialized workspace found; run \`rift init\` from the root folder` |
| `missing_marker` | `this workspace is missing its \`.rift\` marker; run \`rift init\` to restore it` |

### CLI-only error (not a `RiftError` code)

`rift remove` on a root workspace without `-f` raises `CliError::ForceRequired` before calling the core library:

```
This is the root workspace.

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

This error has no `code` field and is not thrown by the JavaScript API.

## Errors by operation

| Operation | Codes commonly encountered |
|-----------|---------------------------|
| `init` | `cow_unavailable` (btrfs non-btrfs), `unsafe_git`, `marker_mismatch`, `io`, `walk` |
| `create` | `initialization_required` (btrfs unconverted source), `workspace_not_initialized`, `unsafe_git`, `inside_source`, `already_exists`, `cow_unavailable`, `unsupported_entry`, `invalid_config`, `hook_failed`, `invalid_path` |
| `remove` / `remove -f` | `missing_rift`, `marker_mismatch`, `workspace_not_initialized`, CLI `ForceRequired` |
| `remove --children` | Same as remove, plus `missing_rift` if a child path vanished |
| `list` / `ancestors` | `workspace_not_initialized`, `not_managed`, `marker_mismatch` |
| `gc` | `io`, `database`, `cow_unavailable` (btrfs subvolume deletion) |
| JavaScript `call()` | Any core code above, plus `invalid_request`, `panic`, `serialization` |

## Handling errors in JavaScript

<RequestExample>

```ts
import { create, RiftError } from "rift-snapshot"

try {
  const child = create({ from: "/path/to/source", name: "feature" })
} catch (error) {
  if (error instanceof RiftError) {
    switch (error.code) {
      case "initialization_required":
        // error.path is the source workspace
        break
      case "hook_failed":
        // error.path is the child workspace; message contains the command
        break
      default:
        console.error(error.code, error.message)
    }
  }
}
```

</RequestExample>

<Info>
Use an optional `database` option in API calls to point at a non-default SQLite registry—the same hidden `--database` flag the CLI accepts. Database open failures surface as `database` or `io` codes.
</Info>

## Related pages

<CardGroup>
  <Card title="Troubleshooting" href="/troubleshooting">
    Common failure modes and recovery steps for workspace, CoW, Git, and hook errors.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    Subcommands, flags, stderr behavior, and exit codes.
  </Card>
  <Card title="JavaScript API reference" href="/javascript-api">
    FFI request protocol, function signatures, and `RiftError` usage.
  </Card>
  <Card title="Postcreate hooks" href="/postcreate-hooks">
    Hook configuration, environment variables, and `hook_failed` failure behavior.
  </Card>
  <Card title="Copy strategies and platforms" href="/copy-strategies">
    Platform backends that produce `cow_unavailable` and `initialization_required`.
  </Card>
  <Card title="Git integration" href="/page-git-integration">
    Git detection rules and `unsafe_git` source-state checks.
  </Card>
</CardGroup>

---

## 17. Troubleshooting

> Common failure modes: uninitialized workspaces, missing markers, CoW unavailable, unsafe Git state, hook failures, and platform limitations.

- Page Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/17-troubleshooting.md
- Generated: 2026-06-17T23:36:19.684Z

### Source Files

- `crates/cli/src/main.rs`
- `crates/core/src/lib.rs`
- `npm/rift-snapshot/index.d.ts`
- `README.md`
- `specs.md`
- `crates/core/src/git.rs`

---
title: "Troubleshooting"
description: "Common failure modes: uninitialized workspaces, missing markers, CoW unavailable, unsafe Git state, hook failures, and platform limitations."
---

Rift surfaces failures through stderr messages on the CLI (exit code `1`), `RiftError` objects from the JavaScript FFI bindings (`code`, `message`, optional `path`), and the `RiftError` enum in the Rust core. Most errors print the core message verbatim; three workspace-resolution errors get tailored CLI guidance. Use the symptom tables below to map output to cause and remediation.

## How failures are reported

| Surface | Success signal | Failure signal |
| --- | --- | --- |
| CLI | Exit code `0`; paths on stdout for `create`, `list`, `ancestors`, `gc` | Exit code `1`; message on stderr |
| JavaScript (`rift-snapshot`) | Return value (`string`, `string[]`, or `null`) | Thrown `RiftError` with `code` and optional `path` |
| FFI protocol | `{ "status": "ok", "value": ... }` | `{ "status": "error", "error": { "code", "message", "path?" } }` |

The CLI remaps three workspace errors to actionable guidance:

| Core error | CLI stderr message |
| --- | --- |
| `WorkspaceNotInitialized` | `no initialized workspace found; run rift init from the root folder` |
| `MissingMarker` | `this workspace is missing its .rift marker; run rift init to restore it` |
| `InitializationRequired` | `this workspace must be initialized first; run rift init from its root folder` |

All other errors use the core `Display` string (for example `copy-on-write cloning unavailable: ...` or `unsafe Git source: ...`).

<Note>
The JavaScript `init()` function initializes exactly the `at` path. Git-root selection and `--here` are CLI-only behaviors. If `init()` fails from a nested directory, pass the workspace root explicitly.
</Note>

## Workspace resolution failures

Rift locates managed workspaces by walking upward from the requested path. It reads `.rift` marker files and cross-checks them against the central SQLite registry.

```text
requested path
    │
    ▼ walk ancestors
┌─────────────────────────────────────┐
│ .rift marker present?               │
│   yes → verify ID in registry       │
│   no  → registry row at path?       │
│         yes → MissingMarker         │
│         no  → continue upward       │
└─────────────────────────────────────┘
    │
    ▼ no match found
WorkspaceNotInitialized
```

### No initialized workspace (`workspace_not_initialized`)

**Symptom:** `rift create`, `rift list`, `rift ancestors`, or `rift remove` from a directory with no `.rift` marker anywhere above it.

**Cause:** The path is not under a registered Rift workspace.

**Fix:**

```bash
cd /path/to/project-root
rift init
```

The CLI `rift init` (without `--here`) selects the nearest existing managed ancestor or the nearest Git root before calling core `init`. Use `rift init --here` to initialize exactly the current directory.

### Missing `.rift` marker (`missing_marker`)

**Symptom:** Registry has an active row for a directory, but `.rift` was deleted or never written.

**Cause:** Manual deletion of `.rift`, interrupted `init`, or filesystem damage.

**Fix:**

```bash
rift init
```

If the directory is already registered, `rift init` restores the marker from the existing registry identity without re-converting the filesystem.

### Workspace requires initialization (`initialization_required`)

**Symptom:** On Linux btrfs, `rift create` fails even though a `.rift` marker exists.

**Cause:** The source is an ordinary btrfs directory, not a subvolume. Btrfs snapshots require `rift init` to convert the workspace first.

**Fix:**

```bash
rift init    # converts ordinary btrfs directory → subvolume on first run
rift create
```

### Unknown or mismatched markers

| Error code | Meaning | Typical cause |
| --- | --- | --- |
| `unknown_marker` | `.rift` contains an ID not in the registry | Corrupt marker, wrong database, manual edit |
| `marker_mismatch` | Marker ID exists but path does not match registry | Marker copied to wrong directory, registry/path drift |

**Fix:** Restore the correct ULID from the registry into `.rift`, or re-run `rift init` on a registered root to restore its marker. If the registry and filesystem are irreconcilable, unregister with `rift remove -f` on the source root (after backing up) and re-initialize.

## Copy-on-write unavailable (`cow_unavailable`)

Rift has no byte-copy fallback. If no platform strategy can clone the tree, `create` and `init` fail with `copy-on-write cloning unavailable`.

### Platform matrix

| Platform | Supported backends | Failure when |
| --- | --- | --- |
| Linux x64 (btrfs) | Writable subvolume snapshots; reflink import for filtered copies | Path not on btrfs; ordinary directory used for `create` without prior `init` |
| Linux x64 (other, e.g. XFS) | Per-file `FICLONE` reflinks | Filesystem lacks reflink support; probe file creation fails |
| macOS arm64 / x64 | APFS `clonefile` | `clonefile` syscall fails (non-APFS volume, permissions) |
| Windows x64 | None implemented | Any `create` or `init` that requires cloning |

<Warning>
On Windows, the `rift-snapshot` package is published but workspace creation is not implemented. `create` returns `cow_unavailable` with message `no copy-on-write strategy has been implemented for this platform`.
</Warning>

### Linux btrfs-specific failures

| Message fragment | Cause | Fix |
| --- | --- | --- |
| `is not on a btrfs filesystem` | `rift init` on ext4, NFS, etc. | Move workspace to btrfs, or use a reflink-capable filesystem (XFS with `FICLONE`) |
| `Linux snapshot creation requires btrfs` | `create` on btrfs path that is not a subvolume | Run `rift init` first |
| `failed to activate initialized workspace` | Subvolume swap failed mid-conversion | Check permissions and disk space; original may be restored or left in `.rift-init-original-*` staging |

### Cross-filesystem storage

On reflink-capable Linux filesystems, source and destination must share a device:

```
Linux reflinks require source and destination on the same filesystem: /path/to/dest
```

**Fix:** Use default storage (sibling `.rifts/<workspace-name>/`) or pass `--into` on the same mount as the source. This commonly affects workspaces at filesystem mount roots where the default sibling path crosses devices.

### Unsupported file types (`unsupported_entry`)

**Symptom:** `unsupported filesystem entry: /path/to/fifo` (or socket, device node).

**Cause:** Filtered and exact copy walks encounter non-regular entries (FIFOs, sockets) that cannot be reflinked or cloned.

**Fix:** Remove or relocate special files before `rift create`. Partial copy failures roll back: the destination directory is removed and no registry row is inserted.

## Unsafe Git state (`unsafe_git`)

`rift init` and `rift create` call `check_source` on Git repositories. Creation is refused when the copy would be ambiguous or unsafe.

### Rejected conditions

| Condition | Error detail |
| --- | --- |
| Linked Git worktree | `linked Git worktree sources are not supported` (`.git` is a file, not a directory) |
| Merge in progress | `Git state in progress: MERGE_HEAD` |
| Cherry-pick in progress | `Git state in progress: CHERRY_PICK_HEAD` |
| Revert in progress | `Git state in progress: REVERT_HEAD` |
| Bisect in progress | `Git state in progress: BISECT_LOG` |
| Rebase in progress | `Git state in progress: rebase-merge` or `rebase-apply` |
| Index or HEAD locked | `Git state in progress: index.lock` or `HEAD.lock` |

**Fix:** Finish or abort the in-progress Git operation, then retry. For linked worktrees, run Rift from the main worktree whose `.git` is a directory, not from a linked checkout.

<Note>
Repositories with no commits yet are allowed. `HEAD` stays in unborn-branch state because there is no commit to detach to.
</Note>

## Postcreate hook failures (`hook_failed`)

Hooks run **after** the workspace is copied, registered, and Git-prepared. A hook failure does **not** roll back the workspace.

**Symptom:**

```
postcreate hook failed at /path/to/child: `pnpm install` exited with exit status: 1
```

**Behavior:**

- Hooks execute sequentially; the first failure stops later hooks.
- The created workspace remains on disk and in the registry.
- Environment variables available: `RIFT_SOURCE`, `RIFT_DESTINATION`, `RIFT_ID`, `RIFT_PARENT_ID`.
- Stdio is inherited from the parent process.

**Fix options:**

1. Fix the failing command and run it manually in the new workspace.
2. Remove the broken rift: `rift remove` then `rift create --no-hooks` to skip hooks.
3. Correct `.rift.toml` before the next create.

### Invalid configuration (`invalid_config`)

Config is validated **before** copying when hooks are enabled (default). Invalid `.rift.toml` prevents workspace creation entirely.

| Validation rule | Example failure |
| --- | --- |
| `version` must be `1` | `unsupported config version 2` |
| `run` must be non-empty | `postcreate run cannot be empty` |
| Unknown fields rejected | `shell` field in `[[hooks.postcreate]]` |

**Fix:** Correct `.rift.toml` per the configuration reference. Use `rift create --no-hooks` to bypass config loading and hook execution.

## Path, storage, and naming errors

| Error code | Cause | Fix |
| --- | --- | --- |
| `invalid_path` | Not a directory; empty name; `parent/child` name; null byte in path | Pass a canonical directory; use a single path segment for `--name` |
| `already_exists` | Destination or trash target already exists | Pick a different `--name`; run `rift gc` if trash is stale |
| `inside_source` | `--into` or destination falls inside the source tree | Store rifts outside the copied tree (default `.rifts/` sibling or external `--into`) |
| `not_managed` | Ancestor chain broken in registry | Registry corruption or manual DB edit; inspect registry |
| `missing_rift` | Registered path missing on disk during `remove` | Restore moved/deleted directory, or prune with `rift gc` after manual cleanup |

### Root unregistration requires force

Unregistering a source root is CLI-gated:

```
This is the root workspace.

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

Use `rift remove -f` only when you intend to drop Rift metadata while keeping the source directory. Descendants move to `.trash/`.

## JavaScript and FFI troubleshooting

### Node.js FFI requirements

Node bindings require the experimental FFI API:

```bash
node --experimental-ffi app.mjs
```

With Node's permission model, also pass `--allow-ffi`.

### Missing prebuilt binaries

If `prebuilds/<platform>-<arch>/` is absent after install:

```
Unable to locate the Rift binary for linux-x64. Reinstall rift-snapshot.
```

Reinstall the global package or verify the release archive matches your platform (`darwin-arm64`, `darwin-x64`, `linux-x64`, `windows-x64`).

### FFI protocol errors

| Code | Cause |
| --- | --- |
| `invalid_request` | Malformed JSON request to `rift_ffi_call` |
| `panic` | Unhandled panic inside the native library |
| `serialization` | Response encoding failure |

Catch `RiftError` by `code` for programmatic handling:

```ts
import { create, RiftError } from "rift-snapshot";

try {
  create({ from: process.cwd(), name: "task-a" });
} catch (error) {
  if (error instanceof RiftError) {
    console.error(error.code, error.path, error.message);
  }
}
```

## Diagnostic workflow

<Steps>
<Step title="Reproduce with verbose context">

Note the command, working directory, platform, and filesystem type (btrfs, XFS, APFS). Check whether `.rift` exists and what it contains.

</Step>
<Step title="Classify the error family">

Match stderr or `RiftError.code` to workspace resolution, CoW, Git, hooks, or path/storage categories using the tables above.

</Step>
<Step title="Apply the targeted fix">

Run `rift init` for marker or btrfs subvolume issues. Resolve Git state before `create`. Use `--into` for cross-filesystem storage. Use `--no-hooks` to isolate hook problems.

</Step>
<Step title="Verify recovery">

```bash
rift list          # confirms registry sees children
rift ancestors     # confirms parent chain
rift gc            # cleans stale trash after manual fixes
```

</Step>
</Steps>

## Quick reference: error codes

| Code | Category |
| --- | --- |
| `workspace_not_initialized` | No `.rift` ancestry |
| `missing_marker` | Registry row without `.rift` |
| `initialization_required` | Btrfs source not yet a subvolume |
| `unknown_marker` / `marker_mismatch` | Marker/registry inconsistency |
| `cow_unavailable` | Platform or filesystem unsupported |
| `unsupported_entry` | FIFO, socket, or special file in tree |
| `unsafe_git` | Linked worktree or in-progress Git operation |
| `hook_failed` | Postcreate command failed (workspace kept) |
| `invalid_config` | `.rift.toml` validation failed |
| `already_exists` / `inside_source` / `invalid_path` | Destination or naming problem |
| `missing_rift` / `not_managed` | Filesystem/registry drift on removal |

For the full catalog including path-bearing variants and FFI message shapes, see the error codes reference.

## Related pages

<CardGroup>
<Card title="Initialize a workspace" href="/initialize-workspace">
Run `rift init`, restore missing markers, and convert btrfs directories to subvolumes.
</Card>
<Card title="Copy strategies and platforms" href="/copy-strategies">
Platform backends, reflink requirements, and why byte-copy is not a fallback.
</Card>
<Card title="Git integration" href="/git-integration">
Detached HEAD behavior, marker exclusion, and unsafe source detection.
</Card>
<Card title="Postcreate hooks" href="/postcreate-hooks">
Hook configuration, environment variables, and failure semantics.
</Card>
<Card title="Error codes" href="/error-codes">
Complete `RiftError` catalog for CLI, FFI, and TypeScript bindings.
</Card>
</CardGroup>

---

## 18. Development

> Build and test the Rust workspace, install the CLI locally, CI test matrix, and filesystem integration test environment variables.

- Page Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/18-development.md
- Generated: 2026-06-17T23:36:20.566Z

### Source Files

- `README.md`
- `scripts/install.sh`
- `Cargo.toml`
- `.github/workflows/ci.yml`
- `crates/cli/tests/filesystem_e2e.rs`
- `scripts/ci/linux-fs.sh`

---
title: "Development"
description: "Build and test the Rust workspace, install the CLI locally, CI test matrix, and filesystem integration test environment variables."
---

The Rift repository is a Cargo workspace (`crates/core`, `crates/cli`, `crates/ffi`) versioned at `0.0.10` with edition 2024. Day-to-day development runs `cargo test --workspace --locked` for portable unit and manager tests, and `./scripts/install.sh` to place an optimized `rift` binary at `${CARGO_HOME:-$HOME/.cargo}/bin/rift`. Copy-on-write filesystem integration tests are opt-in: they early-return unless a `RIFT_REQUIRE_*` environment variable is set, and CI mounts synthetic filesystem fixtures before running them.

## Workspace layout

```text
rift/                          Cargo workspace root
├── crates/core/               `rift` library — Manager, strategies, registry
├── crates/cli/                `rift-cli` — `rift` binary and CLI e2e tests
├── crates/ffi/                `rift-ffi` — cdylib for JavaScript bindings
├── scripts/install.sh         Local CLI install
└── scripts/ci/linux-fs.sh     CI filesystem fixture runner
```

| Crate | Package name | Artifact |
| --- | --- | --- |
| `crates/core` | `rift` | Library + `create` / `compare` benches |
| `crates/cli` | `rift-cli` | `rift` executable |
| `crates/ffi` | `rift-ffi` | `cdylib` for npm FFI bindings |

<Note>
Always pass `--locked` when building or testing. CI and release workflows use the lockfile to pin dependency versions.
</Note>

## Build and test

### Default test run

The standard workspace test command exercises unit tests, manager logic with `TestStrategy`, CLI argument parsing, and FFI protocol serialization. It does **not** execute real copy-on-write operations unless the checkout filesystem happens to satisfy a strategy probe and the corresponding `RIFT_REQUIRE_*` variable is set.

```bash
cargo test --workspace --locked
```

### Per-crate testing

| Command | Scope |
| --- | --- |
| `cargo test --package rift --locked` | Core library, strategy modules, `linux_filesystem_tests` |
| `cargo test --package rift-cli --locked` | CLI unit tests + `filesystem_e2e` integration tests |
| `cargo test --package rift-ffi --locked` | FFI request/response protocol tests |

### Release build

```bash
cargo build --release --package rift-cli --locked
```

The release workflow builds `rift` for `x86_64-unknown-linux-gnu`, `x86_64-apple-darwin`, and `aarch64-apple-darwin` after the same `cargo test --workspace --locked` gate.

## Install the CLI locally

`scripts/install.sh` installs an optimized binary from the workspace checkout:

```bash
./scripts/install.sh
```

The script runs:

```bash
cargo install --path crates/cli --root "${CARGO_HOME:-$HOME/.cargo}" --force --locked
```

Verify with `rift --help`. The installed binary is the same `rift` target defined in `crates/cli/Cargo.toml`.

<Tip>
Use the locally installed binary when exercising shell integration (`rift shell-init`) or manual quickstart flows against real directories.
</Tip>

## Test layers

Rift separates tests by filesystem dependency. Understanding which layer a test belongs to explains why `cargo test --workspace` passes on a laptop without btrfs or APFS fixtures.

```text
┌─────────────────────────────────────────────────────────────┐
│  Layer 1 — Always runs (any OS, any filesystem)             │
│  core/tests.rs (TestStrategy), cli/main.rs unit tests,      │
│  ffi protocol tests, config/registry/git/filter tests       │
└─────────────────────────────────────────────────────────────┘
                              │
┌─────────────────────────────────────────────────────────────┐
│  Layer 2 — Strategy probes (auto-skip when probe fails)     │
│  btrfs/reflink/apfs module tests using tempdir_in(cwd)    │
└─────────────────────────────────────────────────────────────┘
                              │
┌─────────────────────────────────────────────────────────────┐
│  Layer 3 — Gated integration (requires RIFT_REQUIRE_* env)  │
│  linux_filesystem_tests, filesystem_e2e, CI env assertions  │
└─────────────────────────────────────────────────────────────┘
```

**Layer 1** uses `TestStrategy` and `FailureStrategy` mocks so manager, registry, hook, and Git semantics are validated without touching the production CoW backend.

**Layer 2** creates fixtures under `std::env::current_dir()` so temp directories land on the same mounted device as the checkout. Tests call `reflink_temp()` or `btrfs_temp()` and return early when the probe fails.

**Layer 3** checks `RIFT_REQUIRE_*` at the start of each test and returns immediately when unset. CI sets exactly one variable per matrix row before invoking tests on a prepared filesystem.

## CI test matrix

GitHub Actions workflow `.github/workflows/ci.yml` defines three job families.

### Unit test job

| Field | Value |
| --- | --- |
| Runner | `ubuntu-latest` |
| Command | `cargo test --workspace --locked` |
| Trigger | Push to `dev`, all pull requests |

### Linux filesystem matrix

| Filesystem fixture | APT packages | Environment variable | Expected behavior |
| --- | --- | --- | --- |
| `btrfs` | `btrfs-progs` | `RIFT_REQUIRE_BTRFS_TESTS=1` | Writable btrfs subvolumes, subvolume snapshots |
| `xfs-reflink` | `xfsprogs` | `RIFT_REQUIRE_REFLINK_TESTS=1` | Native per-file reflinks (`mkfs.xfs -m reflink=1`) |
| `zfs` | `zfsutils-linux` | `RIFT_REQUIRE_REFLINK_TESTS=1` | ZFS reflink clone support |
| `xfs-no-reflink` | `xfsprogs` | `RIFT_REQUIRE_UNSUPPORTED_LINUX_TESTS=1` | Reflink probe fails; init/create reject with `CowUnavailable` |
| `ext4` | `e2fsprogs` | `RIFT_REQUIRE_UNSUPPORTED_LINUX_TESTS=1` | No CoW; fail-closed |
| `tmpfs` | _(none)_ | `RIFT_REQUIRE_UNSUPPORTED_LINUX_TESTS=1` | No CoW; fail-closed |

Each matrix row runs:

```bash
bash scripts/ci/linux-fs.sh <filesystem> -- \
  env <required_env>=1 cargo test --package rift --package rift-cli --locked
```

`fail-fast: false` keeps independent filesystem rows running when one fails.

### APFS integration job

| Field | Value |
| --- | --- |
| Runner | `macos-14` |
| Environment | `RIFT_REQUIRE_APFS_TESTS=1` |
| Command | `cargo test --package rift --locked` |

## Filesystem integration environment variables

Four environment variables gate Layer 3 tests. When unset, gated tests return immediately without failure.

<ParamField body="RIFT_REQUIRE_BTRFS_TESTS" type="flag">
Set to any value (CI uses `1`). Enables btrfs round-trip tests in `linux_filesystem_tests`, CLI `filesystem_e2e`, and asserts initialized paths are btrfs subvolumes. The btrfs strategy integration test fails the run if the checkout is not on btrfs when this variable is set.
</ParamField>

<ParamField body="RIFT_REQUIRE_REFLINK_TESTS" type="flag">
Set to any value. Enables native reflink round-trip tests on non-btrfs Linux filesystems (XFS with reflink, ZFS). Asserts shared extents when reliable and verifies copy divergence after mutation. The reflink strategy integration test fails if the checkout lacks reflink support when this variable is set.
</ParamField>

<ParamField body="RIFT_REQUIRE_UNSUPPORTED_LINUX_TESTS" type="flag">
Set to any value. Enables fail-closed tests: `init` returns `CowUnavailable`, no `.rift` marker is written, `create` returns `WorkspaceNotInitialized`, and reflink probe artifacts (`.rift-reflink-probe*`) are cleaned up.
</ParamField>

<ParamField body="RIFT_REQUIRE_APFS_TESTS" type="flag">
Set to any value on macOS. Enables the APFS CI integration assertion that `ApfsStrategy::copy_directory` succeeds on the runner filesystem.
</ParamField>

### Variable-to-test mapping

| Variable | Core tests | CLI e2e tests | Strategy assertions |
| --- | --- | --- | --- |
| `RIFT_REQUIRE_BTRFS_TESTS` | `production_supported_linux_filesystem_*` | `supported_filesystem_cli_round_trip` | `btrfs_integration_environment_is_available` |
| `RIFT_REQUIRE_REFLINK_TESTS` | `production_supported_linux_filesystem_*` | `supported_filesystem_cli_round_trip` | `linux_reflink_integration_environment_is_available` |
| `RIFT_REQUIRE_UNSUPPORTED_LINUX_TESTS` | `production_unsupported_linux_filesystem_rejects_management` | `unsupported_filesystem_cli_fails_closed` | Reflink probe preflight in `linux-fs.sh` |
| `RIFT_REQUIRE_APFS_TESTS` | — | — | `integration_environment_is_required_by_ci` |

<Warning>
`RIFT_REQUIRE_BTRFS_TESTS` and `RIFT_REQUIRE_REFLINK_TESTS` both enable the **supported** round-trip suites. CI sets only one per matrix row. Do not set both locally unless the checkout is btrfs (btrfs takes precedence in strategy selection).
</Warning>

## `linux-fs.sh` fixture runner

`scripts/ci/linux-fs.sh` prepares an isolated filesystem mount, copies the repository checkout onto it, runs capability preflights, and executes the supplied test command from the mounted copy.

```text
GITHUB_WORKSPACE ──copy──► /mnt/rift-<fs>/rift/  ──cd──► cargo test ...
         │                         │
         │                  loopback image or tmpfs
         │                  (1 GiB for block-backed FS)
         └─ required env var
```

### Usage

```bash
bash scripts/ci/linux-fs.sh <btrfs|xfs-reflink|xfs-no-reflink|ext4|tmpfs|zfs> [-- <command> ...]
```

### Required and optional host variables

| Variable | Required | Purpose |
| --- | --- | --- |
| `GITHUB_WORKSPACE` | Yes | Source checkout path to copy onto the fixture mount |
| `RUNNER_TEMP` | No | Directory for loopback images; defaults to `mktemp -d` |
| `GITHUB_RUN_ID` | No | ZFS pool name suffix for collision avoidance |

### Preflight checks

Before running tests, the script verifies:

- Mount fstype matches the requested fixture (`btrfs`, `xfs`, `ext4`, `tmpfs`, or `zfs`)
- Checkout is writable by the current user
- Registry temp paths reside on the same device as the checkout
- Capability probes pass or fail as expected:
  - **btrfs**: `btrfs subvolume create` / `delete`
  - **xfs-reflink, zfs**: `cp --reflink=always` succeeds
  - **xfs-no-reflink, ext4, tmpfs**: reflink probe fails

Cleanup unmounts loop devices, destroys ZFS pools, and removes mount points on exit.

### Run filesystem tests locally

Reproduce a CI matrix row on a Linux machine with `sudo` access:

```bash
export GITHUB_WORKSPACE="$(pwd)"

bash scripts/ci/linux-fs.sh btrfs -- \
  env RIFT_REQUIRE_BTRFS_TESTS=1 cargo test --package rift --package rift-cli --locked
```

For unsupported-filesystem fail-closed behavior:

```bash
bash scripts/ci/linux-fs.sh ext4 -- \
  env RIFT_REQUIRE_UNSUPPORTED_LINUX_TESTS=1 cargo test --package rift --package rift-cli --locked
```

On macOS with APFS, run the APFS job equivalent directly:

```bash
RIFT_REQUIRE_APFS_TESTS=1 cargo test --package rift --locked
```

<Note>
Filesystem fixtures create temp directories under `current_dir()` (the mounted checkout). Run `linux-fs.sh` from a clean working tree so probe and e2e artifacts do not pollute your primary checkout.
</Note>

## CLI e2e test isolation

`crates/cli/tests/filesystem_e2e.rs` exercises the real `rift` binary through `CliFixture`. Each invocation passes a dedicated registry via the hidden `--database` flag and isolates state with:

- `HOME` → fixture-local home directory
- `XDG_DATA_HOME` → fixture-local data directory (default DB path: `$XDG_DATA_HOME/rift/rift.sqlite`)

Supported round-trip e2e covers `init --here`, `create`, `--into` custom storage, `list`, `ancestors`, cross-filesystem `--into` failure, `remove`, and `gc`. Unsupported e2e verifies stderr contains `copy-on-write cloning unavailable` or `no initialized workspace found` and that no registry rows are written.

## Benchmarks

Performance measurement lives in `crates/core/benches/`. See the benchmarking page for `create` and `compare` bench usage, sample counts, and JSON output schema.

## Related pages

<CardGroup>
  <Card title="Installation" href="/installation">
    Install via npm, bun, or Cargo build script; prebuilt binary layout.
  </Card>
  <Card title="Copy strategies and platforms" href="/copy-strategies">
    btrfs, reflink, and APFS backends exercised by the filesystem test matrix.
  </Card>
  <Card title="Benchmarking" href="/benchmarking">
    Measure and compare `rift create` performance with Cargo benches.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    Hidden `--database` and `--shell-cwd` flags used by e2e fixtures.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Common failure modes surfaced by integration tests: CoW unavailable, missing markers, unsafe Git state.
  </Card>
</CardGroup>

---

## 19. Benchmarking

> Measure `rift create` performance with `create` and `compare` Cargo benches, sample counts, JSON output schema, and candidate comparison workflow.

- Page Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/pages/19-benchmarking.md
- Generated: 2026-06-17T23:36:03.677Z

### Source Files

- `README.md`
- `crates/core/benches/create.rs`
- `crates/core/benches/compare.rs`
- `crates/core/benches/AUTORESEARCH.md`
- `crates/core/Cargo.toml`

---
title: "Benchmarking"
description: "Measure `rift create` performance with `create` and `compare` Cargo benches, sample counts, JSON output schema, and candidate comparison workflow."
---

The `rift` crate (`crates/core`) ships two standalone Cargo bench binaries—`create` and `compare`—that time the production `Manager::create_with_options` path against a real workspace directory. Both benches use `harness = false`, so they run as ordinary binaries invoked through `cargo bench` rather than through Criterion. The `create` bench records per-sample elapsed milliseconds; `compare` orchestrates that bench across multiple candidate checkouts and ranks results by median time.

## What gets measured

The `create` bench exercises the same code path the CLI uses for workspace creation, with two deliberate isolations from unrelated overhead:

| Phase | Included in timing? | Notes |
| --- | --- | --- |
| `Manager::init` on the workload | No | Runs once before the sample loop |
| `Manager::create_with_options` | Yes | Production copy strategy and registry registration |
| Postcreate hooks | No | `HookMode::Skip` disables `.rift.toml` hook execution |
| `Manager::remove` + `Manager::gc` | No | Runs after each sample to reset state for the next run |

Each timed sample creates a uniquely named child workspace (`benchmark-{pid}-{run_id}-{sample}`), then removes it and runs garbage collection so later samples start from a clean registry state.

<Note>
The benchmark uses the production filesystem strategy selected at runtime. On macOS that means APFS `clonefile`; on Linux btrfs that means subvolume snapshots; on reflink-capable Linux filesystems that means per-file reflinks. Results are not comparable across platforms.
</Note>

## Bench targets

Both targets are declared in `crates/core/Cargo.toml`:

```toml
[[bench]]
name = "create"
harness = false

[[bench]]
name = "compare"
harness = false
```

Run them from the repository root or from `crates/core`. `cargo bench` compiles with release optimizations by default.

## `create` benchmark

### Command

```bash
cargo bench --bench create -- /path/to/workspace [--samples N] [--output /path/to/result.json]
```

<ParamField body="workspace" type="path" required>
  Directory to initialize and use as the create source. Must exist. The bench canonicalizes this path before timing.
</ParamField>

<ParamField body="--samples" type="integer" default="1">
  Number of independent create operations to time. Must be greater than zero.
</ParamField>

<ParamField body="--output" type="path">
  Optional file path. When set, writes a JSON result document after all samples complete.
</ParamField>

### Stdout

Each sample prints a tab-separated progress line:

```text
create	1/10	37.842 ms	/path/to/.rifts/benchmark-12345-...
```

When `--samples` is greater than 1, a summary line follows:

```text
median	37.800 ms	min 37.600 ms	max 38.100 ms
```

On failure the bench prints `benchmark failed: {error}` to stderr and exits with code `1`.

### Establish a baseline

Measure multiple independent creations and persist machine-readable output outside the workload directory so result files do not alter future measurements:

```bash
mkdir -p /path/to/results
cargo bench --bench create -- /path/to/linux \
  --samples 10 \
  --output /path/to/results/baseline.json
```

Use `median_ms` as the primary comparison metric. Keep `samples_ms` to detect noisy runs.

## `create` JSON output schema

When `--output` is set, the bench writes a single JSON object:

<ResponseField name="benchmark" type="string">
  Always `"create"`.
</ResponseField>

<ResponseField name="timestamp_ms" type="integer">
  Wall-clock milliseconds since Unix epoch at write time.
</ResponseField>

<ResponseField name="platform" type="string">
  `std::env::consts::OS` value (for example `"linux"`, `"macos"`).
</ResponseField>

<ResponseField name="source" type="string">
  Canonicalized workload directory path.
</ResponseField>

<ResponseField name="samples_ms" type="number[]">
  Elapsed milliseconds for each sample, in run order.
</ResponseField>

<ResponseField name="median_ms" type="number">
  Median of `samples_ms`. For even sample counts, the average of the two middle values.
</ResponseField>

<ResponseField name="min_ms" type="number">
  Minimum value in `samples_ms`.
</ResponseField>

<ResponseField name="max_ms" type="number">
  Maximum value in `samples_ms`.
</ResponseField>

<ResponseField name="cleanup_passed" type="boolean">
  Always `true` when the bench exits successfully. The `compare` bench rejects candidates where this is `false`.
</ResponseField>

<RequestExample>

```json
{
  "benchmark": "create",
  "timestamp_ms": 1718640000123,
  "platform": "macos",
  "source": "/path/to/linux",
  "samples_ms": [37.8, 38.1, 37.6, 37.9, 38.0],
  "median_ms": 37.8,
  "min_ms": 37.6,
  "max_ms": 38.1,
  "cleanup_passed": true
}
```

</RequestExample>

## `compare` benchmark

The `compare` bench runs each candidate's `create` bench against the same workload, collects per-candidate JSON results, and writes a ranked summary.

### Command

```bash
cargo bench --bench compare -- /path/to/workload \
  --candidate /path/to/rift \
  [--candidate /path/to/rift-other] \
  --output /path/to/results \
  [--samples N]
```

<ParamField body="workload" type="path" required>
  Workload directory passed through to every candidate's `create` bench. Canonicalized before execution.
</ParamField>

<ParamField body="--candidate" type="path" required>
  Repeatable. Each path must be a Cargo workspace root containing `Cargo.toml` and the `create` bench target. At least one candidate is required.
</ParamField>

<ParamField body="--output" type="path" required>
  Output directory. Created if missing, then canonicalized. Receives per-candidate JSON files and `summary.json`.
</ParamField>

<ParamField body="--samples" type="integer" default="10">
  Sample count forwarded to each candidate's `create` bench.
</ParamField>

### How candidates are invoked

For each `--candidate` path, `compare` runs:

```bash
cargo bench --locked --bench create -- <workload> --samples <N> --output <output>/candidate-NN.json
```

with `current_dir` set to the candidate checkout. Candidates are numbered in invocation order (`candidate-01.json`, `candidate-02.json`, …). If any candidate's `cargo bench` exits non-zero, or `cleanup_passed` is not `true` in its result file, the comparison fails.

### Stdout

Per candidate:

```text
running	/path/to/rift-git
```

After all candidates complete, ranked results:

```text
1	34.900 ms	+0.00%	/path/to/rift-registry
2	39.400 ms	+12.92%	/path/to/rift
summary	/path/to/results/round-01/summary.json
```

Ranking uses `median_ms`. `difference_from_fastest_percent` is `(median_ms / fastest_median - 1) * 100`.

### Output directory layout

```text
/path/to/results/round-01/
  candidate-01.json
  candidate-02.json
  candidate-03.json
  summary.json
```

## `compare` JSON output schema

### Per-candidate files (`candidate-NN.json`)

Same schema as the `create` bench output.

### Summary file (`summary.json`)

<ResponseField name="benchmark" type="string">
  Always `"create"`.
</ResponseField>

<ResponseField name="timestamp_ms" type="integer">
  Wall-clock milliseconds since Unix epoch at summary write time.
</ResponseField>

<ResponseField name="platform" type="string">
  OS of the machine running `compare` (not per-candidate).
</ResponseField>

<ResponseField name="source" type="string">
  Canonicalized workload path.
</ResponseField>

<ResponseField name="samples" type="integer">
  Sample count used for every candidate in this round.
</ResponseField>

<ResponseField name="candidates" type="object[]">
  Ranked list sorted by ascending `median_ms`.
</ResponseField>

Each entry in `candidates`:

<ResponseField name="rank" type="integer">
  1-based rank by `median_ms`.
</ResponseField>

<ResponseField name="candidate" type="string">
  Canonicalized path to the candidate checkout.
</ResponseField>

<ResponseField name="result" type="string">
  Path to the corresponding `candidate-NN.json` file.
</ResponseField>

<ResponseField name="median_ms" type="number">
  Median create time for this candidate.
</ResponseField>

<ResponseField name="min_ms" type="number">
  Minimum sample time.
</ResponseField>

<ResponseField name="max_ms" type="number">
  Maximum sample time.
</ResponseField>

<ResponseField name="difference_from_fastest_percent" type="number">
  Percent slower than the fastest candidate's `median_ms`. The fastest candidate is `0.0`.
</ResponseField>

<RequestExample>

```json
{
  "benchmark": "create",
  "timestamp_ms": 1718640000456,
  "platform": "linux",
  "source": "/path/to/linux",
  "samples": 10,
  "candidates": [
    {
      "rank": 1,
      "candidate": "/path/to/rift-registry",
      "result": "/path/to/results/round-01/candidate-03.json",
      "median_ms": 34.9,
      "min_ms": 34.1,
      "max_ms": 36.0,
      "difference_from_fastest_percent": 0.0
    },
    {
      "rank": 2,
      "candidate": "/path/to/rift",
      "result": "/path/to/results/round-01/candidate-01.json",
      "median_ms": 39.4,
      "min_ms": 38.8,
      "max_ms": 40.6,
      "difference_from_fastest_percent": 12.9
    }
  ]
}
```

</RequestExample>

## Candidate comparison workflow

Use the `compare` bench to evaluate implementation changes across isolated checkouts. A typical layout keeps one unchanged baseline, several experimental siblings, one shared workload, and results stored outside all checkouts:

```text
/path/to/rift              unchanged baseline and comparison runner
/path/to/rift-git          one experimental hypothesis
/path/to/rift-registry     another experimental hypothesis
/path/to/linux             shared workload checkout
/path/to/results           benchmark evidence
```

<Steps>
<Step title="Preserve the benchmark framework">
Commit or otherwise preserve the benchmark targets in the base `rift` checkout before creating candidates. Every candidate must contain the `create` bench and its dependencies.
</Step>

<Step title="Create experimental candidates">
Create sibling checkouts from the same Git revision. Rift itself can provision them:

```bash
cd /path/to/rift
rift init --here .
rift create --name rift-git --into /path/to
rift create --name rift-registry --into /path/to
```

Use the paths printed by `rift create` if storage layout differs from the example.
</Step>

<Step title="Verify correctness in each changed candidate">
Before benchmarking, run tests in every candidate that received code changes:

```bash
cargo test --workspace --locked
```

Exclude candidates whose tests fail from the comparison invocation.
</Step>

<Step title="Run the comparison from the baseline checkout">
Keep the base `rift` checkout unchanged and include it as a `--candidate` for baseline ranking:

```bash
cd /path/to/rift
cargo bench --bench compare -- /path/to/linux \
  --candidate /path/to/rift \
  --candidate /path/to/rift-git \
  --candidate /path/to/rift-registry \
  --samples 10 \
  --output /path/to/results/round-01
```
</Step>

<Step title="Inspect results and decide next steps">
Read `summary.json` and per-candidate `samples_ms` arrays. Apply the decision rules below before promoting a winner or starting a follow-up round.
</Step>
</Steps>

For follow-up rounds among finalists, increase sample count rather than re-measuring clearly unsuccessful candidates:

```bash
cargo bench --bench compare -- /path/to/linux \
  --candidate /path/to/rift \
  --candidate /path/to/rift-registry \
  --candidate /path/to/rift-combined \
  --samples 30 \
  --output /path/to/results/round-02-finalists
```

<Warning>
On Linux, the workload must reside on supported btrfs or native reflink-capable storage for production copy-on-write behavior. On the first benchmark run against an ordinary btrfs directory, `init` may convert it into a subvolume before any timed samples—that conversion is outside the measured interval but affects what subsequent runs measure.
</Warning>

## Interpreting results

| Rule | Action |
| --- | --- |
| Tests fail in a candidate | Exclude the candidate; do not benchmark it |
| `cleanup_passed` is not `true` | Reject the candidate's benchmark result |
| Lower `median_ms` than baseline | Candidate is a timing improvement candidate |
| Raw `samples_ms` overlap substantially | Treat small median differences as inconclusive; rerun finalists with more `--samples` |
| Multiple independent winners | Combine changes in a fresh `rift-*` candidate from the baseline, then benchmark the combination against individual winners |
| Round complete | Keep the results directory unchanged as experiment evidence |

<Tip>
Assign one clearly stated hypothesis per experimental `rift-*` candidate so measured differences can be attributed to specific code changes.
</Tip>

## Limitations

The benchmark framework handles measurement, JSON persistence, and candidate ranking. It does not automate hypothesis selection, parallel code changes, test execution, candidate exclusion, or promotion decisions—those remain manual or agent-driven steps in the auto-research workflow documented in `crates/core/benches/AUTORESEARCH.md`.

Postcreate hooks are skipped during timing (`HookMode::Skip`). Benchmark results do not reflect hook execution cost; see the postcreate hooks page if hook latency matters for your workload.

## Related pages

<CardGroup>
<Card title="Create workspaces" href="/create-workspaces">
CLI flags and behavior for `rift create`, including filtered copy, `--copy-all`, and hook modes.
</Card>
<Card title="Copy strategies and platforms" href="/copy-strategies">
Platform-specific copy-on-write backends that determine what the benchmark actually measures.
</Card>
<Card title="Development" href="/development">
Build and test the Rust workspace with `cargo test --workspace --locked` before benchmarking candidates.
</Card>
<Card title="Postcreate hooks" href="/postcreate-hooks">
Hook configuration and execution—the benchmark skips hooks via `HookMode::Skip`.
</Card>
</CardGroup>

---
