# Shell integration

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

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

## Source Files

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

---

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