npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cabane/companion

v0.6.147

Published

The Cabane Companion (headless): connect a coding agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.

Readme

cabane-companion

Connect a coding agent on your machine to your Cabane 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, Codex, or opencode — and you expose the ones you have. Install and sign in to a harness yourself; the Companion drives it. See 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 step-by-step guides — Pair a device, Create an agent, Give an agent host access — are in Cabane's docs, with screenshots: in Cabane, open the Account menu and choose Docs. This README is the reference for running the Companion on the machine.

The model in one paragraph

The Companion runs on a device you pair, running agents you assign to it. cabane-companion start pairs the device with a short code the first time, then runs. 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 (host access, MCP servers, model), is owned by Cabane and delivered per turn. The machine only holds the bits that must be local: the device token, your secrets, and any overrides you set for this machine.

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. Renaming, deactivating and removing a device, rotating its token, and assigning agents to it all live in Cabane; 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 has the full per-harness wiring; the short version:

Claude Code — install it globally and log in:

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.

opencode — install it, authenticate a provider through its own flow, and start its server and leave it running (opencode serve --port 4096). The Companion finds a server on that port by itself; for one anywhere else, connect it with cabane-companion connect opencode --url <url>.

You also need a Cabane account. A device belongs to your account, not to one workspace: once paired, it is available in every workspace the account is in.

Install

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

One command sets the machine up:

cabane-companion start

It names each coding agent it finds and asks whether to make it available in Cabane. Press Enter for yes, or type n to leave one out. Then it prints a pairing code and a link, and waits. The code is good for 15 minutes.

  We found Claude Code on this machine (2.1.287). Make it available in Cabane? [Y/n]
  ✓ Claude Code — available once this device is paired.

  Now let's pair this machine. Enter this code in Cabane:

      4HSP-FPWB

  or, if Cabane isn’t open:

      https://app.cabane.ai/pair/enter-code?code=4HSP-FPWB

  Waiting for you to confirm in Cabane… (the code is good for 15 minutes)

In Cabane, open the Account menu at the bottom left and choose Settings, then Devices, then Pair a device and Enter pairing code. Type the eight characters and choose Pair device. Or open the link from the terminal, which fills the code in. Any browser signed in to the account will do, so a headless machine needs no browser of its own. The device token is delivered straight to the waiting CLI. Nothing is copied or pasted.

The terminal finishes by itself:

  ✓ Paired to Maya Finch's account.
  ✓ Claude Code connected.
  ✓ Cabane companion is running in the background.
    Stop:  cabane-companion stop        Logs:  cabane-companion logs   (~/.cabane/companion.log)

  ! It won't restart on its own — after a reboot, or if it ever stops,
    just run cabane-companion start again.

  All set on this side. Finish up in Cabane.

Back in Cabane, Name this device asks for a Device name, which starts as the machine's hostname and can change later. Choose Save and continue. What Runs on This Device lists each coding agent on the device as a connector; choose Continue. The device is then listed under Settings → Devices as Online.

A freshly paired device runs nothing yet: pairing doesn't put any agent on it. In a workspace, open Settings, then Agents, choose an agent, and under Assignment choose Assign a device, pick this device, and choose Assign device. Then, under Execution, choose its Connector and model and choose Save execution settings. Until those are saved, the agent's row in the list still says "Not configured". Only the device's own owner can assign an agent to it. The Create an agent guide in Cabane's docs walks these screens. Assign agents in as many workspaces as the account is in: one running Companion serves them all.

Commands

| Command | What it does | | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | pair [--server <url>] | Pair this device with a short code entered in Cabane, without starting the Companion. start does this by itself when the device isn't paired, so pair is rarely 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. |

Everything else about a device is done in Cabane, not the CLI: rename, deactivate, rotate token and remove are on the device's page under the account's Settings → Devices, and agents are assigned on each workspace's Settings → Agents.

Managing the device

Each of these starts in Cabane, on the device's page, and three of them need a command on the device afterwards.

  • After a reboot, or whenever the Companion has stopped, run cabane-companion start. Running it while the Companion is already running changes nothing. In Cabane, a device whose Companion has stopped turns to Offline about two minutes later, and back to Online once it is up.
  • Deactivate turns the device off without removing it. The Companion on the device exits. To bring it back, choose Reactivate in Cabane, then run cabane-companion start on the device. The Companion's log says to log out and pair again; that isn't needed.
  • Rotate token shows a new device token (cabdev_…) once, and the old one stops working at once. On the device, run cabane-companion stop, replace the value of deviceToken in ~/.cabane/config.json with the new token, then run cabane-companion start. Stop first: a Companion that is still running keeps the old token, and start does nothing while one is running.
  • Remove device can't be undone. The Companion on the device exits. To pair the same machine again, run cabane-companion logout, then cabane-companion start.

If cabane-companion start says the Companion is running but the device stays Offline, the Companion may have exited straight after starting, which happens when Cabane no longer accepts the device. cabane-companion status and cabane-companion logs show which.

Background and supervised operation

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

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:

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

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:

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:

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

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:

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

To remove it completely:

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 host access, working directory, MCP servers, connector, model and charter — is configured in Cabane (a workspace's Settings → Agents) and delivered to the Companion per turn, so an edit in Cabane lands on the agent's next reply with no restart. One exception: a conversation given Custom execution settings keeps its own connector, model and tuning when the agent's defaults change. The Companion holds only the bits that can't travel from a server: your secrets, and machine-local overrides such as a prepare-hook.

An agent can reach MCP servers two ways, and they don't overlap. The agent's own MCP servers are part of what the agent is: set on its Agents page, they go with it into every room. Connections belong to a person instead: you add one under a workspace's Settings → Connections (an endpoint and a pasted key, stored encrypted in Cabane), then add it to a room from the room's menu and pick which agents there may use it. On each turn in that room, Cabane attaches the room's connections for that agent and sends the key with the turn, into the harness's environment and never onto its command line. Nothing is configured on this machine. Replacing, disconnecting or deleting a key takes effect on the agent's next turn, and nothing is revoked at the service itself.

Host access (set in Cabane)

Host access is a setting in the agent's Environment, on its page under Settings → Agents. Only the owner of the agent's device can change it.

| Host access | What the agent can do | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | No | Work in the workspace: read and write the workspace's files and take part in conversations. No shell or file tools on the device. The default. | | Yes | All of that, plus run shell commands and read and write files on the device, starting in its working directory. ⚠️ |

⚠️ An agent with host access acts on the device as the user account the Companion runs under, with no approval prompts. With Claude Code as the connector, it runs every command and writes every file it is asked to, and it isn't restricted to its working directory. Turn it on for an agent whose work needs the device, on a device where that reach is acceptable. The sandbox below limits what those tools can touch.

MCP servers in the agent's Environment don't depend on host access: a stdio server listed there runs on the device and gives the agent its tools either way.

Working directory + prepare-hook

An agent with host access starts in its Working directory, set in Cabane in the same Environment dialog as host access. Left empty, or naming a folder that isn't on the device, the agent starts in the folder the Companion was started from; a missing folder is reported only in the Companion's log.

The machine can override it. A cwd in ~/.cabane/config.json wins over the directory set in Cabane, and a prepare-hook wins over both. 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):

{
  // …device identity, written when the device is paired. The one field to edit by
  // hand is `deviceToken`, after rotating the token in Cabane…

  // Per-agent machine-local overrides. Only `cwd`, `prepareHook`, `claudeCode` and
  // `sandbox` live here — everything else about the agent (host access, MCP
  // servers, model) is set in Cabane.
  "agents": {
    "my-coding-agent": {
      "cwd": "/Users/you/code/project", // overrides the working directory set in Cabane
      "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:

{
  "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 matters for an agent with host access, where your project settings are read):

{
  "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):

// ~/.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.

Sandbox (set on this machine)

With "runners": { "enabled": true, "sandbox": "os" }, each turn's harness — the model's process and every shell it starts — runs inside an OS sandbox (bubblewrap on Linux, Seatbelt on macOS) whose network goes through a local proxy. It's a guardrail on your own machine: it stops a turn from reading the secrets named below or reaching hosts you didn't allow. It runs as your user on your kernel, so it isn't isolation. Host access on the agent in Cabane decides whether the agent gets shell and file tools at all; the sandbox decides what those tools can touch. The two are independent.

What the sandbox allows is runners.policy in ~/.cabane/config.json. Every key is optional, and with no policy block you get the defaults:

{
  "runners": {
    "enabled": true,
    "sandbox": "os",
    "policy": {
      "hosts": { "allow": ["gitlab.example.com"], "deny": [], "defaults": true },
      "filesystem": { "mode": "blocklist", "denyRead": [], "allowRead": [], "allowWrite": [] },
      "credentials": {
        "env": { "GH_TOKEN": { "secret": "${GITHUB_TOKEN}", "hosts": ["api.github.com"] } },
        "git": [{ "host": "github.com", "secret": "${GITHUB_TOKEN}" }],
      },
    },
  },
}

| Setting | Default | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Hosts, always reachable | Cabane, and the model provider of the turn's harness (Anthropic for Claude Code, OpenAI for Codex). allow and defaults never remove these; a deny entry naming one does, and blocks the turn from reaching it. | | Hosts, defaults | github.com, *.github.com, *.githubusercontent.com, registry.npmjs.org, pypi.org, files.pythonhosted.org, crates.io, static.crates.io, index.crates.io. allow adds to them, deny removes a host (and wins), defaults: false drops the list. | | Reads, blocklist mode (the default) | Everything is readable except every companion's store (~/.cabane, and any .cabane under your home or beside this companion's home; see below), the .claude and .codex of the other companion homes those stores sit in, ~/.ssh, ~/.aws, ~/.gnupg, ~/.config/gh, ~/.git-credentials, ~/.netrc, ~/.npmrc, ~/.pypirc, ~/.docker/config.json, ~/.kube, ~/.config/gcloud, ~/.azure, ~/.config/op, ~/.password-store, and your denyRead paths. allowRead reopens a path inside a denied one. | | Writes | The working directory, the temp dir, ~/.claude, ~/.claude.json, ~/.codex, plus allowWrite. |

One agent can be excepted. The sandbox is set per device, and one agent's row in agents can say otherwise — for example an ops agent whose job is the machine itself, on a device whose other agents stay sandboxed:

{
  "runners": {
    "enabled": true,
    "sandbox": "os",
    "policy": {
      /* unchanged */
    },
  },
  "agents": {
    "014d18a7-6395-48e3-a2f2-313d5baa2a5e": { "sandbox": "none" }, // the ops agent, by its id: it runs with the machine; everyone else stays sandboxed
  },
}

It works the other way too: "agents": { "<agent id>": { "sandbox": "os" } } sandboxes one agent on a device that otherwise isn't. A sandbox row is keyed by the agent's id, the last part of the agent's settings page URL in Cabane (/<workspace>/settings/agents/<agent id>). A username, or workspace-slug/username, can match more than one agent: slugs are unique only per workspace owner, so two workspaces this device serves can share one. A sandbox row keyed any other way is refused at start, with the field named. cwd and prepareHook rows still take all three keys. The agent's row wins, then runners.sandbox, then no sandbox. The policy stays the device's. Under the docker provider the container is the sandbox, so a row's sandbox is ignored, and the log says so at start. The exception is set only here: Cabane shows it on the device page and can't change it.

Paths may start with ~/ and may be globs. An invalid policy doesn't stop the Companion from starting. Instead, every sandboxed turn is refused and the conversation names the field that's wrong, for example runners.policy.filesystem.mode.

Other companions on the same machine. A second companion (another account, a test device) keeps its own secrets in its own .cabane, so a turn can't read any companion's store, not only this device's. Every turn denies this companion's store and ~/.cabane (both, when the Companion runs from a home set with CABANE_COMPANION_HOME), plus any .cabane up to two levels under your home or beside this companion's home, where companion homes conventionally sit. Stores anywhere deeper are found by a search of your home with find, along with the .claude and .codex beside each one, which hold that companion's harness logins. That search takes seconds over a large home, so the Companion runs it in the background: when it starts, and again whenever a turn starts more than 5 minutes after the last search finished. Only the first turn after a start waits for it. A store created deeper than two levels is denied within about 5 minutes plus one search, not instantly. If a search can't finish (find is missing, runs past 60 seconds, or hits a directory it can't list), the stores it did find are still denied, and the log says so. The search runs in strict mode too, because strict mode reopens the working directory, and a store inside it must stay hidden.

.env files are readable. Beyond the credential locations above, nothing guesses which of your files hold secrets, so .env files, in the working directory or any other project, are readable unless you deny them. To hide them, list the roots that hold them, for example "denyRead": ["~/work/**/.env", "~/work/**/.env.*"]. Each glob is expanded when the turn starts, which costs a walk of that tree on every turn and misses files created during the turn, so keep the roots narrow. When other projects' secrets really matter, strict mode is the better answer, because it hides them all without a walk.

Strict mode. "filesystem": { "mode": "strict" } hides your whole home directory except the working directory, the harness state (~/.claude, ~/.codex), what the Companion and its harnesses run from, and the paths you list in allowRead. Toolchains live all over a home directory (~/.nvm, ~/.local/share/pnpm, ~/.cargo, ~/.pyenv, mise or asdf shims), so list yours, or builds will fail with "No such file or directory" on a path you didn't expect. On Linux a hidden path looks missing rather than denied, so the Companion log prints the readable list at the start of each strict turn. Compare it against the path the build couldn't find.

Credentials, without the harness ever holding them. A credentials.env entry sets that variable in the sandbox to a placeholder like fake_value_…, and the proxy swaps in the real value from ~/.cabane/secrets.json only on requests to the hosts the entry names in hosts. hosts is required, and it's not a formality: the entry's hosts are the only places the real value goes, so a GitHub token should name api.github.com and nothing else. A credentials.git entry does the same for git over HTTPS. git base64-encodes its credentials, so the placeholder can't be found inside them, and the Companion instead has git send a ready-made basic-auth header for that host (username defaults to x-access-token). If a name isn't in secrets.json, the turn is refused with the name in the conversation. It is never left unset without telling you. With sandbox: "none", or on a host that can't run the sandbox, credentials aren't given to the harness at all. If the sandbox fails to start for a turn that holds credentials, the turn is refused rather than run unsandboxed. Masking keeps the harness from reading a credential, not from using it. The proxy also swaps the value in request bodies, so a turn can still put the real token into something it sends to one of the entry's hosts, such as a pull request comment. Give a token only the permissions its agents need.

Recipe: GitHub with gh and git push.

  1. Create a fine-grained personal access token scoped to your repositories, with Contents and Pull requests set to read and write.
  2. Put it in ~/.cabane/secrets.json as "GITHUB_TOKEN": "github_pat_…".
  3. Add both credential entries from the example above: env.GH_TOKEN with "hosts": ["api.github.com"] for gh, and a git entry for github.com.
  4. Use HTTPS remotes, because ~/.ssh is unreadable in the sandbox. Rewrite existing SSH remotes once on the machine: git config --global url."https://github.com/".insteadOf [email protected]:.

A gh auth login session on the machine isn't used: ~/.config/gh is unreadable in the sandbox. Only the token in secrets.json is.

cabane-companion status prints the effective posture: the filesystem mode, extra hosts, denied hosts and reads, the names of masked credentials, never their values, and each agent excepted on its row. The Companion also logs it once at start.

Harnesses

A harness is the local tool that runs a model — Claude Code, Codex, or opencode — 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 is the agent's Connector, chosen in Cabane under Execution on the agent's page (Settings → Agents), together with a model from that connector. The list holds the connectors on the agent's device, so set the harness up first, then choose it for the agent.

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 | | --------------- | -------------------------------------------------------------------------------------------------- | | Claude Code | A claudeCode block in ~/.cabane/config.json, and the Agent SDK's bundled binary installed. | | Codex | A codex block in ~/.cabane/config.json, and the Codex SDK's vendored binary installed. | | opencode | An opencode.serverUrl in ~/.cabane/config.json pointing at a reachable opencode serve. |

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.

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:

{
  // …device identity, written when the device is paired…
  "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:

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 an agent with host access yet. And there's no self-update; re-run npm i -g @cabane/companion@latest to pick up a new version.