# cabane-companion

Connect a **coding agent on your machine** to your [Cabane](https://cabane.ai) workspaces as a **responder** — it replies to messages inside Cabane while running as a full local AI client, so you can drive work against your own codebase, files, and MCP servers without putting any of it in Cabane. The model and tools run on your box; only the reply crosses back.

The Companion runs your agents through a **harness** on your machine — **[Claude Code](https://www.npmjs.com/package/@anthropic-ai/claude-code)**, **[Codex](https://developers.openai.com/codex/cli)**, or **[opencode](https://opencode.ai)** — and you expose the ones you have. Install and sign in to a harness yourself; the Companion drives it. See [Harnesses](#harnesses).

This is **v0**. It's a small CLI — install it from npm with one command, pair the machine once, and run. It talks to Cabane only over Cabane's public API.

**The full story** — why you'd want the Companion, how it works, the privacy and tool-policy model, the diagram — lives at **[app.cabane.ai/docs/reference/connectors](https://app.cabane.ai/docs/reference/connectors)**. This README is the reference for _running_ the Companion.

## The model in one paragraph

The Companion runs on a **device you pair**, running **agents you assign to it**. Pair the device once with a short code, then `start` it. From then on it **pulls its assignments from Cabane at runtime** — every agent you've pointed at this device, across all your workspaces — and runs each one locally. Which agents to run, and how they're configured (mode, MCP servers, model), is owned by Cabane and delivered per turn. The machine only holds the bits that _must_ be local: the device token, each agent's working directory, and your secrets.

> **No account password or full-account token ever touches your machine.** The short-code pairing flow delivers a **device token** (`cabdev_…`) directly to the waiting CLI — good only for pulling this device's assignments and reporting liveness. Each agent the Companion runs gets its own **workspace-bound agent token**, delivered once when the agent is assigned and scoped to that one agent in that one workspace. Deactivating and removing a device — and assigning agents to it — all live in the Cabane app; the CLI only pairs the machine and runs the agents locally.

## Prerequisites

You need **Node 22+** (the Companion itself runs on Node — check with `node --version`) and **at least one harness installed and signed in**. The Companion installs no harness and drives no login for you — bring your own, and set it up **before** starting the Companion. It will still `start` with nothing connected — that is the ordinary first-run state, and `start` is what offers you the harnesses it found — but no turn can be routed to it until you connect one.

Set up whichever you already use — one is enough, and a machine can expose several. [Harnesses](#harnesses) has the full per-harness wiring; the short version:

**Claude Code** — install it globally _and_ log in:

```sh
npm i -g @anthropic-ai/claude-code
claude   # complete the login, then quit
```

Both halves are load-bearing, for different reasons:

- **The install** puts `claude` on your PATH (`which claude` should resolve). That is what makes the Companion _offer_ to connect Claude Code, and what `cabane-companion status` reports a version from. It is not what runs your turns, and on its own it exposes nothing — that takes connecting it _and_ the bundled binary (below).
- **The login** writes the credential the turn actually uses. The Companion does _not_ run your global `claude` to answer a turn — inference goes through the bundled Claude Agent SDK, which spawns its own native binary — but the SDK reads the credential your Claude Code login left on disk. So a machine with `claude` installed and never logged in looks fine and then fails every turn with **"Sign-in needed"**.

**Connect it.** Claude Code is an opt-in like the others: accept the offer from `cabane-companion start`, run `cabane-companion connect claude-code`, or add a `claudeCode` block to `~/.cabane/config.json`. A device exposes it only when you have connected it **and** the Agent SDK's bundled binary is installed — that binary arrives as an optional npm dependency, so a companion installed with optional dependencies omitted has none, and will tell you so with the command that fixes it.

**Codex** — log in (`codex login`, or set `CODEX_API_KEY` in Codex's own environment — Cabane never sees the key), then opt in the same way: accept the offer from `cabane-companion start`, run `cabane-companion connect codex`, or add a one-line `codex` block in `~/.cabane/config.json`. As with Claude Code, the turn runs the SDK's own vendored `codex` binary rather than the CLI on your PATH. → [full setup](https://app.cabane.ai/docs/reference/connectors/chatgpt)

**opencode** — install it, authenticate a provider through its own flow, and start its server (`opencode serve`); the Companion addresses it at the URL you give it. → [full setup](https://app.cabane.ai/docs/reference/connectors/opencode)

You also need **a Cabane account.** A device is registered under your user account; the agents it runs can live in any workspace you have access to.

## Install

```sh
npm i -g @cabane/companion
```

That puts `cabane-companion` on your PATH. Confirm it with `cabane-companion --version`. To **update** later: `npm i -g @cabane/companion@latest`.

## First run

On your machine, start the short-code pairing flow:

```sh
cabane-companion pair
# Enter this code at https://app.cabane.ai/settings/companions
#
#     ABCD-EFGH
#
# Waiting for you to confirm it in cabane…
```

Open the printed URL, enter the code in **Settings → Connectors**, give the device a label, and confirm. The waiting command receives its device token directly and finishes:

```sh
# ✓ Paired this device "MacBook" with https://app.cabane.ai.
# Run `cabane-companion start` — it will pull the agents assigned to this device and run them.

cabane-companion start
#   ✓ Cabane companion is running in the background.
#     Stop:  cabane-companion stop        Logs:  cabane-companion logs   (~/.cabane/companion.log)
```

A headless machine needs no browser of its own: run `cabane-companion pair` on it, then open **Settings → Connectors** in any browser you're signed in to and enter the short code it printed. The device token is delivered straight to the waiting CLI over the pairing channel — nothing is ever copied or pasted.

A freshly paired device runs nothing yet — it has no agents. In **Workspace settings → Agents**, set an agent's **executor** to this device. The running Companion picks the assignment up within a minute (no restart), and the next time you address that agent — put it on a message's **To** line — the reply comes from your own machine. Assign agents in as many of your workspaces as you like — one running Companion serves them all.

## Commands

| Command                                  | What it does                                                                                                                                                                                                                                                   |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pair [--server <url>]`                  | Pair this device using a short code confirmed in Settings → Connectors. `start` does this automatically when needed.                                                                                                                                           |
| `start [--foreground] [--daemon]`        | Pair if needed, offer detected coding harnesses, and run in the background. `--daemon` is an accepted compatibility alias for the default. `--foreground` stays attached for a terminal or external supervisor. A non-interactive `start` also stays attached. |
| `connect <claude-code\|codex\|opencode>` | Connect a detected coding harness now or update the running Companion over its control socket.                                                                                                                                                                 |
| `stop`                                   | Stop a foreground or detached Companion cleanly. Idempotent — "nothing running" is a success.                                                                                                                                                                  |
| `status`                                 | Print the paired device, log/transcript paths, process mode, pid and uptime, declared secret names, and local overrides.                                                                                                                                       |
| `logs [-n N] [-f]`                       | Show recent log lines or follow new ones.                                                                                                                                                                                                                      |
| `transcript [file] [--last] [-f]`        | Show a recent full agent transcript. No args lists recent turns; `--last` renders the newest; `--follow` live-watches new turns.                                                                                                                               |
| `logout [-y] [--purge]`                  | Remove the device token and cached agent credentials. `--purge` removes the whole local config. Neither removes the device from Cabane.                                                                                                                        |
| `--version` / `-V`                       | Print the Companion version.                                                                                                                                                                                                                                   |

Lifecycle — **register / deactivate / remove** a device, and **assign agents** to it — lives in the Cabane app (Settings → Devices, and each workspace's Agents page), not the CLI.

## Background and supervised operation

In an interactive terminal, `cabane-companion start` backgrounds itself, returns the prompt, and keeps running after the terminal closes:

```sh
cabane-companion start
#   ✓ Cabane companion is running in the background.
#     Stop:  cabane-companion stop        Logs:  cabane-companion logs   (~/.cabane/companion.log)
```

`start --daemon` takes the same path for compatibility. This detached process does not survive logout or reboot and Cabane does not install, enable, or update a login service. Use `cabane-companion status` to check it and `cabane-companion stop` to stop it.

If you want restart or login persistence, configure your own supervisor to run `cabane-companion start --foreground`. The supervisor owns when the process starts and restarts; the Companion never writes these files for you.

### Find the executable paths

The examples below need absolute paths because service managers do not load your interactive shell setup:

```sh
command -v node
printf '%s\n' "$(npm root -g)/@cabane/companion/dist/cli.js"
```

Confirm both paths exist, then substitute them for `/absolute/path/to/node` and `/absolute/path/to/cli.js`. NVM and global npm paths are often versioned and can move after a Node or package upgrade. After every upgrade, run these commands again, update both paths in your supervisor config, and reload/restart it.

### macOS LaunchAgent

Save this as `~/Library/LaunchAgents/ai.cabane.companion.plist`, replacing both paths:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>ai.cabane.companion</string>
  <key>ProgramArguments</key>
  <array>
    <string>/absolute/path/to/node</string>
    <string>/absolute/path/to/cli.js</string>
    <string>start</string>
    <string>--foreground</string>
  </array>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
  <key>StandardOutPath</key><string>/tmp/cabane-companion.out.log</string>
  <key>StandardErrorPath</key><string>/tmp/cabane-companion.err.log</string>
</dict>
</plist>
```

Load it with `launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.cabane.companion.plist`. Adding a login-persistent LaunchAgent is security-sensitive behavior and may be reported by macOS or endpoint-security tooling. Cabane cannot prevent or hide that report.

To update moved executable paths, unload it, edit the two paths, then load it again:

```sh
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/ai.cabane.companion.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.cabane.companion.plist
```

To remove it completely:

```sh
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/ai.cabane.companion.plist
rm ~/Library/LaunchAgents/ai.cabane.companion.plist
```

### Linux systemd user unit

Save this as `~/.config/systemd/user/cabane-companion.service`, replacing both paths:

```ini
[Unit]
Description=Cabane Companion
After=network-online.target

[Service]
Type=simple
ExecStart=/absolute/path/to/node /absolute/path/to/cli.js start --foreground
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target
```

Load it with:

```sh
systemctl --user daemon-reload
systemctl --user enable --now cabane-companion.service
```

A user service normally stops when its login session ends. If this machine must keep running it after logout, an administrator can enable lingering with `sudo loginctl enable-linger "$USER"`; that is a separate operating-system persistence decision.

After an NVM/npm upgrade, stop the unit, rediscover and replace both executable paths, then reload and restart it:

```sh
systemctl --user stop cabane-companion.service
systemctl --user daemon-reload
systemctl --user start cabane-companion.service
```

To remove it completely:

```sh
systemctl --user disable --now cabane-companion.service
rm ~/.config/systemd/user/cabane-companion.service
systemctl --user daemon-reload
```

If you enabled lingering only for this Companion, undo it separately with `sudo loginctl disable-linger "$USER"`.

### Other supervisors

Run `cabane-companion start --foreground`. Configure the supervisor itself for restart and login/boot behavior, and use absolute executable paths when it does not load your interactive shell. Removing the supervisor configuration removes the persistence; no Cabane-managed service remains.

## Configuring an agent: what's in Cabane vs. on this machine

There's a deliberate split. **What the agent is** — its mode, MCP servers, model, reasoning, and charter — is configured in **Cabane** (Workspace settings → Agents) and delivered to the Companion per turn, so an edit in Cabane lands on the next turn with **no restart**. The Companion holds only the bits that can't travel from a server: the **working directory** an agent runs in (a path on _this_ machine) and your **secrets**.

### Agent modes (set in Cabane)

`mode` names the agent by what it _is_, not by how locked-down its tools are. You pick it on the workspace Agents page:

| `mode`      | What the agent is                                                                                                                                        |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `assistant` | A Cabane assistant — Cabane tools only (read/write workspace files, post messages). Local shell/filesystem tools are off. The cloud-safe default.        |
| `coding`    | A full coding agent on your machine: all tools available, **with no permission prompts**, in the working directory you set below. **Companion-only.** ⚠️ |
| `custom`    | An explicit **allowlist** — Cabane's tools plus exactly the tools you list, and nothing else.                                                            |

> ⚠️ **`coding` mode is yolo — there is no permissioning yet.** It runs a coding agent on **your machine, with no approval prompts** — it can run any shell command and edit any file in its working directory. Only assign it where you trust it to operate, and treat it like Claude Code with permissions disabled. A future release adds real per-tool gating. Until then, `assistant` is the safe default.

### Working directory + prepare-hook (set on this machine)

A `coding` agent needs a directory to run in — a path that only exists on your machine, so it lives in `~/.cabane/config.json`, not in Cabane. Add an `agents` map keyed by the agent's id, its `@username`, or `workspace-slug/username` (all three show up in `cabane-companion status`, copy-pasteable):

```jsonc
{
  // …device identity, managed by `pair`; don't hand-edit those fields…

  // Per-agent machine-local overrides. Only `cwd` and `prepareHook` live here —
  // everything else about the agent (mode, MCP servers, model) is set in Cabane.
  "agents": {
    "my-coding-agent": {
      "cwd": "/Users/you/code/project", // working directory the agent runs in
      "prepareHook": { "command": "/Users/you/bin/prepare-turn" }, // optional: resolves the cwd before each turn
    },
  },
}
```

A malformed `agents` block fails `cabane-companion start` immediately with a message pointing at the exact field — it won't silently fall back.

**A hook that is the same for every agent goes at the top level instead.** If your hook works out the directory itself — from the conversation and agent ids the Companion hands it — then listing every agent by name is a roster you have to remember to update, and an agent you forget gets no hook at all and starts its turn wherever the Companion happens to be. So `prepareHook` can sit beside `agents` rather than inside it:

```jsonc
{
  "prepareHook": { "command": "/Users/you/bin/prepare-turn" }, // every agent on this device
  "agents": {
    "special-case": { "prepareHook": { "command": "/Users/you/bin/other-hook" } }, // wins for this one
  },
}
```

The agent's own entry wins when it names a hook; otherwise the device-level one applies. It survives a re-pair along with the rest of your local settings, and `cabane-companion status` prints it. A hook that fails still fails the turn loudly either way.

### Auto-memory (off by default)

Claude Code's **auto-memory** — where the agent writes notes to a local `~/.claude` memory directory and recalls them on later turns — is **off by default** on every Companion. The stance is that memory-shaped things belong in your Cabane workspace (a shared, versioned `ABOUT.md`-style file or doc), not a private local directory that no one else can see. If you run your own Companion and want your normal Claude Code memory workflow back, set `claudeCode.autoMemory: true` on the agent — the Companion then stops overriding it and your own `~/.claude/settings.json` governs auto-memory as usual (this bites in `coding` mode, where your project settings are read):

```jsonc
{
  "agents": {
    "my-coding-agent": {
      "cwd": "/Users/you/code/project",
      "claudeCode": { "autoMemory": true }, // opt back into local Claude Code auto-memory
    },
  },
}
```

### Secrets (set on this machine)

When an agent's MCP servers in Cabane need a credential, that credential is written in Cabane as a `${PLACEHOLDER}` reference — the real value never leaves your machine. The Companion resolves placeholders at dispatch time from an explicit, operator-declared store at `~/.cabane/secrets.json`, a flat `{ NAME: "value" }` map (mode 600):

```jsonc
// ~/.cabane/secrets.json
{
  "GITHUB_TOKEN": "ghp_…",
  "LINEAR_API_KEY": "lin_api_…",
}
```

A `${VAR}` the store doesn't declare fails the turn loudly — it is **never** read from your shell environment, so a config can't smuggle out an ambient credential. The Companion reports the declared **names** (never values) to Cabane on its heartbeat, so the settings UI can warn "this agent needs `${GITHUB_TOKEN}`, this device doesn't expose it" before a turn ever runs.

## Harnesses

A **harness** is the local tool that runs a model — **[Claude Code](https://www.npmjs.com/package/@anthropic-ai/claude-code)**, **[Codex](https://developers.openai.com/codex/cli)**, or **[opencode](https://opencode.ai)** — together with the credential behind it. A harness connected to Cabane on this device is a **Connector**, and one device runs as many Connectors as it has harnesses. None is privileged: a device advertises exactly the harnesses it actually has.

Which harness a turn uses follows the agent's **model**, set in Cabane (Workspace settings → Agents): a Claude model runs on Claude Code, an OpenAI model on Codex, an opencode model on opencode. A device only offers a harness's models — and only accepts its turns — once that harness is exposed, so set it up first, then pick a matching model for the agent. A turn is never routed to a harness the machine can't run.

What "exposed" means differs per harness, because each is discoverable in a different way. Bring your own install and your own login in all three cases — the Companion never installs a binary and never drives a login:

| Harness         | How the device exposes it                                                                          | Setup                                                                      |
| --------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| **Claude Code** | A `claudeCode` block in `~/.cabane/config.json`, **and** the Agent SDK's bundled binary installed. | [Claude](https://app.cabane.ai/docs/reference/connectors/claude)           |
| **Codex**       | A `codex` block in `~/.cabane/config.json`, **and** the Codex SDK's vendored binary installed.     | [ChatGPT (Codex)](https://app.cabane.ai/docs/reference/connectors/chatgpt) |
| **opencode**    | An `opencode.serverUrl` in `~/.cabane/config.json` pointing at a reachable `opencode serve`.       | [opencode](https://app.cabane.ai/docs/reference/connectors/opencode)       |

Claude Code and Codex each run a native binary their SDK ships, installed alongside the Companion as an optional npm dependency. Those are what a turn launches — not the `claude` / `codex` CLIs on your PATH, which are for setup and login. If one didn't install, the Companion says so on startup and on the device's page in Cabane, with the command that repairs it: `npm i -g @cabane/companion --include=optional`.

Each harness has a full setup page in the docs — install, login, enable, verify — linked above; this section is just the shape of it and the config the Companion reads.

**You usually don't hand-edit the config.** Interactive `cabane-companion start` offers each detected harness, and `cabane-companion connect codex` (or `claude-code` / `opencode`) can connect one later. Editing `~/.cabane/config.json` yourself is the fallback and what a headless device wants:

```jsonc
{
  // …device identity, managed by `pair`; don't hand-edit those fields…
  "claudeCode": { "enabled": true },
  "codex": { "enabled": true },
  "opencode": { "serverUrl": "http://127.0.0.1:4096" },
}
```

Every block is optional and independent — set only the harnesses you have. Claude Code and Codex each take a flag and no URL, because the SDK bundled with the Companion spawns its own native binary per turn; opencode is a long-lived server addressed by URL, so run one `opencode serve` per Companion process. A hand-edit needs a restart (`cabane-companion stop && cabane-companion start`); the `connect` command updates a running Companion over its control socket.

Everything else about a Companion-run agent is the same whichever harness runs it — same assignment, same working directory, same secrets. A harness is a way to execute a turn, not a different way to run the Companion.

## Files on disk

```
~/.cabane/
├── config.json            # device identity: baseUrl + device token (cabdev_…) + device id/label,
│                          #   plus optional per-agent `agents` overrides (cwd / prepareHook), the
│                          #   optional harness blocks (`claudeCode` / `codex` enabled,
│                          #   `opencode` serverUrl), and
│                          #   legacy local prefs. mode 600.
│                          #   No account password, no full-account token.
├── credentials.json       # agentId → per-agent workspace-bound token, cached on first assignment
│                          #   pull (delivered once, then never re-sent). mode 600.
├── secrets.json           # operator-declared { NAME: "value" } store for ${PLACEHOLDER} resolution. mode 600.
├── cursors/<workspace_id> # last-seen SSE event id per workspace
├── outbox/<workspace_id>/ # durable per-agent commit queue — a reply survives a transient API outage
├── runtime.json           # written while `start` runs: control socket + pid (swept on exit)
├── transcripts/           # one JSONL file per dispatch — the full agent turn (see below)
└── companion.log          # readable event log (JSON opt-in)
```

No SQLite, no embedded DB. Cabane is the source of truth for which agents to run and how they're configured.

## Logs

`cabane-companion logs` shows the last 50 lines. Use `-n 20` for twenty lines or
`-f` to follow new lines until Ctrl-C. The initial read is capped at 1 MiB.
The file is `~/.cabane/companion.log` and is human-readable by default: local
`HH:MM:SS`, a level word for warnings and errors, then a sentence. Turns include
the agent's name and an eight-character conversation id. Failed turns include
a reason, the next step and the transcript path.

- `info` (default): startup, connection, assigned agents, and turn outcomes.
- `warn`: degraded operation, such as reconnecting or updates waiting to arrive.
- `debug`: internal diagnostics for investigating a problem. Errors are always shown.

Choose with `cabane-companion start --log-level debug` or `"logLevel": "debug"`
in `~/.cabane/config.json`. Config changes apply on reload; a start flag wins.
Use `--log-format json` or `"logFormat": "json"` for machine-readable JSON with
full ids. The default is `human`; the terminal always stays human-readable.
Log files append across starts. Transcripts remain JSONL in either mode.

## Debugging a turn: transcripts

Cabane shows the agent's final reply, but not _how_ it got there. When a turn misbehaves — an agent that "couldn't read the file", a tool that errored, MCP tools that didn't load, a missing secret — the full picture is on disk. Every dispatch — on whichever harness ran it — writes the complete turn stream (the system init with its tool list, every tool call **and its result**, the assistant text, the outcome) to a JSONL file under `~/.cabane/transcripts/`. On a failed turn, the Companion log also prints the exact path.

Read one back in a readable form:

```sh
cabane-companion transcript --last     # render the most recent turn
cabane-companion transcript            # list recent turns (newest first)
cabane-companion transcript <file>     # render a specific one (filename or a substring)
cabane-companion transcript --follow   # live-watch turns as they land (-f; Ctrl-C to stop)
```

`--follow` (`-f`) is a built-in live watch: it renders the current turn as its lines append and rolls to the next turn when a new dispatch starts — handy for demos or watching the agent work in real time.

It renders the conversation step by step — `🔧 tool(input) → ok/ERROR: result` — so you can see exactly where a turn went wrong. The raw JSONL stays on disk for `jq`. Transcripts can contain whatever the agent read (file contents in tool results), so they're kept local-only in a `700` dir; the newest ~200 are retained.

## What v0 doesn't do yet

There's no OS-service install — an interactive `start` runs detached but doesn't survive logout/reboot or auto-restart on crash. A non-interactive `start` stays attached for a supervisor or provisioner. There's no per-tool permissioning for `coding` mode yet. And there's no self-update; re-run `npm i -g @cabane/companion@latest` to pick up a new version.
