GenerateSaaS

Terminal Sessions

Any user can open their own coding CLI already wired to your product - your capabilities on its MCP surface, your prompt in its context - from their own terminal or the desktop app.

A terminal session turns your product into a place a user can work from their own CLI. On a machine whose companion is paired and has a terminal-capable CLI connected (<binary> connect claude-code), they run one command:

<binary> terminal --url https://your-app.com/api

and their own Claude Code or Codex opens with your product already wired in: your capabilities on the CLI's MCP surface, your composed instructions in the model's context, and a working folder. No key to paste, nothing to configure - the session is composed by your backend at open time.

This ships with the companion. It needs config.companion.enabled and nothing else: the route the session is composed from (POST /companion/terminal-spec) mounts with the rest of the /companion/* transport. Your users get it whether you point at a shared companion or publish your own.

What your backend composes

Your app contributes only what it alone knows; the daemon owns the machine and everything on it.

Your backend suppliesThe daemon supplies
The tool manifest - your capabilities, at the same surface your in-app assistant runs atThe loopback MCP server the tools are served over (127.0.0.1, its own path token)
The instructions - your composed system prompt (AI prompts)The CLI (the user's own, with the user's own subscription) and its argv
A session-scoped wire tokenThe folder the session runs in, and the local MCP servers it gets

The CLI's tool calls come back to POST /companion/tool-call with the session id as their run id, and resolve exactly like a chat turn's: scoped to that user (and their active organization in org-billing mode), under the capability set the manifest advertised, and audited. The tools you declare are exactly what the CLI can call - a session is composed, never opened up.

A session runs at the chat capability surface, so it gets the same full set your in-app assistant does. A capability you exclude from chat (via surfaces) is excluded from terminal sessions too.

Name capabilities and integrations like identifiers. A tool name leaves your server: the daemon joins the manifest's names into the single comma-separated allowlist it hands the CLI, which reads it as a list of permission rules. A capability named list_users,Bash would, absent the daemon's own guard, pre-approve unprompted shell execution on your user's machine. It never gets that far: the daemon refuses the session outright, so what a name like that actually does is silently break every terminal session your users open. The framework refuses one at the declaration site besides - see Capabilities and AI integrations.

What your users get on their side

Two things belong to the user, and your app can neither set nor read them. Both are worth telling users about, because they are what makes a terminal session useful on real work.

Their own MCP servers

A user can wire their tools into the session beside yours - their issue tracker, their notes, their database:

<binary> mcp add linear --url https://your-app.com/api --http https://mcp.linear.app/mcp
<binary> mcp add notes --url https://your-app.com/api --command npx --arg -y --arg @acme/notes-mcp --env NOTES_TOKEN=...
<binary> mcp list

These are local. Your backend can never add, read, or list one, and any MCP server it tries to push into a session is dropped by the daemon. Credentials passed with --env are encrypted at rest on the user's machine and handed to the CLI through its environment, never on a command line.

Their own folders

A session runs in the product's confined work folder by default. To work on their own project, a user grants that folder once, at their machine:

<binary> policy grant-folder add ~/code/my-project --url https://your-app.com/api
<binary> terminal --url https://your-app.com/api --cwd ~/code/my-project

A --cwd outside every granted folder is refused. Your app can never add, name, or widen a grant: a session reports no path back to it, and the spec your backend composes cannot carry one. Grants and revokes are recorded in the user's local audit log, like every policy change.

A folder is where a session starts, not a sandbox around it. A terminal session is the user's own CLI, on their own machine, with their own terminal attached, and what keeps it in that folder is the CLI's own approval prompts - which the user's ceiling decides the fate of: under full the CLI is started with its own guardrails bypassed and can reach anything the user can, while read-only and auto-edit withhold that bypass and leave its native prompts on. A dispatched run is different: it is driven headlessly and stays confined regardless.

In the desktop app

If your product ships the desktop app, its Terminal side-panel tab is this same session, in an embedded terminal: the app spawns the daemon's terminal command in a real pty, and its folder picker grants the folder it picked (the durable, per-app grant above) before the session opens. See Terminal and side panel.

Version skew

The daemon and your backend ship on different schedules, and both directions are handled honestly rather than silently:

  • A daemon older than the feature has no terminal command at all. The desktop app detects that from the daemon's reported version and tells the user to update their companion instead of offering controls that cannot work. On a vendored companion, that update is a release you publish; on a shared one, its maintainer's.
  • A backend older than the daemon answers 404 at /companion/terminal-spec. The daemon names exactly that, and says the app has to ship the route - dispatched runs, schedules, and chat keep working meanwhile.

On this page