cabane
The Cabane Companion runs your Cabane agents on a device you choose. It receives their work from Cabane, starts a harness on the device and streams replies back. The harness calls its configured model provider; agents use the local files, tools and credentials available to their environment.
It uses Claude Code, Codex or OpenCode. The package includes Claude Code and Codex execution binaries, but setup does not run a vendor CLI installer or sign you in. OpenCode needs its own running server.
Start with Cabane Companion, then Mac setup, Windows setup or Server and VPS setup. FAQ and Help covers sign-in and troubleshooting. This README is the device-side reference: commands, configuration and local state.
Pairing supplies a device token, not your Cabane password or a full-account token. The device token is for this device's assignments and liveness; assigned agents receive workspace-bound credentials. The trust guide explains the layers, records and access.
Install
On the device, in a terminal. One of:
curl -fsSL https://cabane.ai/install.sh | bash
The script brings its own Node (checksum-checked, into ~/.cabane) and needs no sudo. Or, with Node 22 or newer already there (the only route on Windows):
npm i -g @cabane/companion && cabane setup
cabane setup on an unpaired device prints a code and a link:
Pair this device
Enter this code in Cabane, or press Return to open the page:
WXYZ-2345
https://app.cabane.ai/pair/enter-code
⠋ Waiting for you to confirm in Cabane… (the code is good for 15 minutes)
Enter the code in Cabane, signed in as yourself: from your own Settings → Devices → Add a device, from a workspace's Settings → Devices → Set up a new device, or on the page the link opens. Cabane shows which device asked; confirming pairs it to your account and nothing else. A workspace's dialog then adds the device to that workspace; from anywhere else, add it to a workspace from that workspace's Settings → Devices. Pairing from a workspace you're setting up places its default agent on the device once a harness there is usable.
Without a terminal, setup prints the same lines once and waits, so a container's logs carry the code. An expired code is replaced by a fresh one, up to three; Ctrl-C stops cleanly. The link is on the server the device pairs with: pass --server <url> for your own Cabane.
From an interactive terminal, setup then starts the Companion in the background, waits for its first check-in and reports the device's state. Without an interactive terminal, setup stays attached to the running Companion.
If no harness is usable, interactive setup asks which one you want to set up. Follow the complete CLI installation and sign-in steps for Mac setup, Windows setup or Server and VPS setup, then check cabane status.
✓ Paired as "maya-mbp".
Cabane is running in the background. This device is ready — checked in 2s ago.
Harnesses on this device
✓ Claude Code connected — signed in (2.1.289)
Codex needs sign-in — run codex login
OpenCode off — not running
Status cabane status Stop cabane stop Log cabane logs
Start at login and keep Cabane updating itself: cabane service install
In Cabane: https://app.cabane.ai/settings/devices
Setup never installs the service. On macOS or Linux, run cabane service install when you want the Companion to start at login and keep itself updated; the closing block names it. Native Windows uses the background process and has no service installer.
If cabane isn't found afterwards, open a new terminal, or run the source line setup printed.
The server guide provides a Dockerfile and Compose example to build locally. Its Dockerfile installs the Companion and all three command-line tools; you sign in inside the container. Follow that guide for pairing and starting the Docker environment.
Commands
| Command | What it does |
|---|---|
setup | Unpaired: print a code to enter in Cabane, wait for the confirm, then detect harnesses and start. Paired: a re-runnable checklist that reports harnesses and starts what isn't running. |
start [--foreground] | Start the Companion without prompts. From an interactive terminal, run in the background; noninteractive use may stay attached. Unpaired, it names setup. --foreground stays attached for your supervisor. |
stop | Stop the Companion cleanly. Ends any turn in progress. |
status | Version and install, the paired device by its Cabane name, its state in Cabane's words and when it last checked in, each harness's state, where agents work, declared secret names, and local overrides. |
update [--auto on|off] | Install the version Cabane names now; with --auto, set whether the Companion keeps itself up to date. |
service install|uninstall|status | Run as a systemd user service or a LaunchAgent that starts at login and restarts if it stops; remove it; show its state. |
logs [-n N] [-f] | Recent log lines, or follow. |
transcript [file] [--last] [-f] | Read back a full agent turn, tool calls and results included. |
logout [--purge] | Remove the device token and cached agent credentials. Doesn't remove the device from Cabane. |
Anything else about a device is done in Cabane, on the device's page.
Keeping it running
For a direct installation, a reboot or a crash leaves the Companion stopped until the next cabane start unless you have installed its service. Under the service it restarts if it stops and starts with its user session (at login on macOS; enable linger on Linux for operation after logout), and follows the version Cabane names: it installs the new version under ~/.cabane/versions, checks it starts, and swaps to it, which can restart the Companion during a turn. A version that can't start is rolled back by the boot guard. cabane update --auto off stops updates; without the service, cabane update installs and the next cabane start runs it.
On native Windows, check the background process with cabane status and run cabane start after a Windows restart. If you installed OpenCode outside Docker, keep its server running separately; the Companion's service does not supervise it.
For the Docker example, keep automatic Companion updates off and rebuild the local image to update the Companion and harness tools. Follow the server guide's update steps.
Other supervisors: run cabane start --foreground with CABANE_COMPANION_SUPERVISED=1 set, using the absolute paths ~/.cabane/node/bin/node and ~/.cabane/current/dist/bin.js, which don't move on an update.
For a direct installation, after Rotate token in Cabane, apply the replacement on the device:
- Run
cabane stop. - In
~/.cabane/config.json, replacedeviceTokenwith the replacement token shown by Cabane. - Run
cabane start.
For Docker, follow Rotate a Docker device's token.
What's on this device
All of it in ~/.cabane/, mode 600, no database. Cabane is the source of truth for which agents run and how.
~/.cabane/
├── config.json # pairing, local overrides
├── work/ # a directory per agent and conversation, for agents with no folder of their own
├── credentials.json # per-agent workspace-bound credentials, delivered once
├── secrets.json # { NAME: "value" } for ${NAME} placeholders
├── transcripts/ # one JSONL per turn, newest ~200 kept, local only
├── outbox/ # a reply survives a transient API outage
└── companion.log
Secrets
An MCP server configured in Cabane names its credential as ${NAME}; the value never leaves the device. The Companion resolves it from secrets.json at dispatch, never from your shell environment, and a name that isn't declared fails the turn loudly. It reports the declared names (never values) on its heartbeat, so Cabane can warn before a turn runs.
A device with no keychain or browser, such as a server or a container, can sign its harness in from secrets.json too: CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY for Claude Code, CODEX_API_KEY for Codex. Only the turn's own harness gets its sign-in. GH_TOKEN there reaches every turn, for gh and git over HTTPS.
Local overrides
config.json can carry, per agent (keyed by id, @username or workspace-slug/username) or for the whole device:
cwd: wins over the agent's Starts in folder set in Cabane.prepareHook: a command that resolves the working directory before each turn; wins over both. A device-level hook applies to every agent that doesn't name its own. The hook getsCABANE_DEFAULT_CWD, the directory below, for a hook that only adds to it.root: where agents work when nothing else says. Every agent gets a directory per conversation under it,<root>/<workspace>/<agent>/<conversation>/, made on the first turn and never removed by the Companion. Default~/.cabane/work; absolute, or~/…. An agent with no folder in Cabane, nocwdand no hook stands there; a folder orcwdthat isn't an existing directory is passed over with a line in the log.harnesses: an allowlist such as["claude-code"]; absent means all three.claudeCode.autoMemory: Claude Code's local memory directory is off by default in the Companion, because notes belong in the workspace where others can read them.truehands it back.
A malformed block fails cabane start with the field named. A runners block, a row's sandbox, or a prepareHook's validateCached or failureReporter from an older version of the Companion is ignored, with one line in the log naming it.
Every turn's shell gets CABANE_CONVERSATION_ID and CABANE_AGENT_ID, so a script an agent runs can tell which conversation and agent it's working for.
Running each turn somewhere else
runner.command puts a command in front of every turn: "runner": { "command": ["/usr/local/bin/enter-sandbox", "--fresh"] } starts each turn as enter-sandbox --fresh <the Companion's own run-turn command>. Use it to run each turn in a container or VM of your own while the Companion stays outside. Your command must run the rest of its arguments, with the environment and stdin it was given.
A wrapped turn gets the device's secrets on its stdin rather than reading secrets.json, inherits no sign-in from the Companion's own environment, and is never signalled by process group from outside: your command ends whatever the turn leaves behind.
What your command can rely on, and can set:
| Variable | |
|---|---|
CABANE_DISPATCH | Read: the dispatch as JSON, including conversationId and agentId. |
CABANE_RUNNER_SPEC | Read: the turn's settings as JSON, including agentUsername. |
CABANE_TURN_ID | Read: the turn's id. |
CABANE_COMPANION_HOME | Read, when the Companion was started with one: the home its .cabane directory is under. |
CABANE_WORK_ROOT | Set: the root the turn's working directory goes under, in place of root. |
CABANE_TRANSCRIPT_DIR | Set: where the turn writes its transcript. A directory one level under ~/.cabane/transcripts/ lists in cabane transcript. |
CABANE_WORK_ROOT_DEDICATED | Set to 1 when the root is already this one conversation and agent's: the turn stands in it directly, with no per-conversation directory under it. |
These names are a contract: changing one would break your command, so none changes without a note in the release.
Keeping provider credentials outside a sandbox
An operator-managed sandbox can use the host-side @cabane/companion/credential-broker module. This is an embedding API for a trusted host adapter, not a proxy that becomes safe by listening on localhost. The adapter must authenticate the accepted socket as one live sandbox instance and revoke its grants when that instance is stopped. A shared UID, a copied placeholder, a socket filename or an HTTP header is not an instance identity. An unresolved, ambiguous or changed identity must return null from resolveIdentity(socket).
Run the broker and its credential files under a host identity outside every sandbox UID mapping. Hold an exclusive process lock for the entire service lifetime, including shutdown: there must be exactly one renewal owner for each account file. The parent of an account file must be owned by that identity with mode 0700; the file must be a regular, single-link 0600 file. Stop the service before importing or changing login state. fileCredentialStore(path) and openBrokerCredentials(store) read that state; share the resulting owner between every grant using the account. Renewal writes and fsyncs a replacement before releasing it. A failed persistence write refuses further use until the operator recovers the stored account.
The account format is { mode, accessToken, refreshToken?, idToken?, expiresAt?, accountId? }. mode is claude-setup, claude-oauth, codex-oauth or github. OAuth modes require a refresh token and an expiry in epoch milliseconds. A setup token never renews. Normal Claude OAuth renews inside the broker before expiry, or on one upstream 401; Codex renewal returns fresh placeholders to its client. After a failed exchange the owner refuses renewal for a backoff window, 30 seconds doubling to 15 minutes, rather than retrying upstream on every request; a successful renewal clears it. Never place the real account file, a credential-store bind, or an alternate harness login inside the sandbox. Real OAuth compatibility must be verified with the installed harness and login mode; a successful setup-token turn proves nothing about OAuth renewal.
Create the broker with createCredentialBroker({ resolveIdentity, audit? }). Its server handles model and git routes; bind it only to host loopback. Git's HTTP proxy requests are accepted only for the exact http://github.com/ authority, then forwarded to fixed HTTPS GitHub upstreams. Arbitrary proxy destinations and CONNECT tunnels are refused. Its githubServer handles gh's http_unix_socket transport; authenticate that socket's peer too. Neither exposes a management endpoint. Call grant({ identity, credentials, expiresAt }) only from the trusted host adapter. credentials maps claude, codex and github to their corresponding credential owners. The returned token is disposable; revoke() aborts active requests and removes its aliases. Teardown calls revokeIdentity(identity); service shutdown calls revokeAll() and closes both servers. A restart creates fresh grants, so old placeholders stop working.
Scope the GitHub token on GitHub's side; the broker imposes no repository, branch, PR-action or GraphQL-shape policy.
Set runner.brokeredCredentials: true beside runner.command in the Companion config. This removes provider and GitHub credential variables from the launcher's environment, secret-store payload and device-grant payload before the wrapper receives them. The wrapper supplies brokerClientConfiguration({ baseUrl, placeholder, githubSocket, codexConfig })'s environment and generated configuration to the isolated home, and writes await grant.codexAuth() as its placeholder-only Codex auth.json. The helper preserves the supplied Codex settings while selecting the broker provider and HTTP Responses transport. Include its git configuration in the sandbox's global config and use its gh configuration there. For a GitHub-enabled box, provide githubSocket. The helper sets GH_TOKEN and preserves github.com in git's expanded remote so gh can resolve a tracked branch, without setting GH_REPO. The generated git config rewrites HTTPS URLs to an HTTP GitHub URL carrying only a placeholder, and sends that origin through the loopback broker using Git's per-origin http.proxy; the broker's upstream hop remains HTTPS. Install the entire generated config, replacing the previous generated file, rather than retaining an older rewrite. Do not bypass this proxy with NO_PROXY or a per-remote proxy override. For a model-only box, omit githubSocket or pass null; no GitHub environment is emitted and both git/gh config strings are empty, so an adapter can clear any previous generated routing. The wrapper must remove old credential mounts and alternate authentication helpers; this switch cannot inspect or secure an operator's filesystem mounts.
The fixed upstream hosts are api.anthropic.com, chatgpt.com, api.github.com and github.com. Paths and request bodies pass through without a workflow policy, except for known credential-minting, credential-registering or disclosure endpoints: Claude CLI API-key creation and OAuth token exchange, ChatGPT session/token disclosure, and GitHub app-manifest conversion, installation/application token endpoints, legacy authorizations, SSH user/deploy-key registration, runner registration/removal/JIT credentials and OAuth/device token endpoints. These return new secrets the echo filter cannot know in advance, or register box-chosen credentials that would outlive the grant. This deny-list must be revisited when providers add credential-returning or credential-registering routes; it is not a guarantee against unknown endpoints or arbitrary encodings. Redirects are refused and responses are filtered for known credential echoes. Credential-free injection is restricted to Codex's designated /backend-api/ps/mcp route. Each upstream exchange, streams included, is capped at ten minutes by default (requestTimeoutMs on createCredentialBroker), and request bodies at 64 MiB.
Arbitrary room secrets and other MCP credentials still follow their existing delivery contract. This API does not protect SSH keys or offer general secret injection into arbitrary HTTP destinations. Claude's feature flags fall back to defaults when their direct requests use placeholders. Before enabling a sandbox deployment, verify actual socket ownership, PID/namespace reuse, service UID separation, grant revocation, concurrent renewal and restart, required client operations, and a canary extraction attempt from inside the real sandbox. A successful unit test or package installation does not establish that host boundary.
Harness permissions
The Companion approves Claude Code's tools through a hook and approves OpenCode permission requests; no turn waits for a tool-approval dialog. For Codex it disables approvals and, when your configuration sets a top-level sandbox_mode, passes no sandbox flag; without that setting it uses danger-full-access. Choose where agents run using Agent access, and configure the tools through Harness sandboxes.
Debugging a turn
Cabane shows the conversation; the device also keeps a detailed local record. Every turn writes its full stream to ~/.cabane/transcripts/: the tool list, every tool call and its result, the text, the outcome. A failed turn's log line names its transcript.
cabane transcript --last # render the most recent turn
cabane transcript # list recent turns
cabane transcript -f # live-watch turns as they land
cabane logs -f # follow the log
cabane start --log-level debug or "logLevel": "debug" in config.json for more; --log-format json for machine-readable lines.
Not yet
Native Windows has no service install and no self-update. There is no per-tool permissioning in Cabane. Control agents' access through their environment and the harness's own settings.