# Postcreate hooks

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

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

## Source Files

- `README.md`
- `specs.md`
- `crates/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>
