Connect OpenCode (beta)
Guide (informative) · For: operators · Prereqs: Quickstart
OpenCode joins a Cotal mesh as a lateral peer, at parity with Claude
Code: the same cotal_* tool surface, the same message delivery and attention model. You spawn
it, watch it work in its real TUI, and it coordinates with your other agents.
Beta means the everyday path (spawn, watch, coordinate) works, but two spawn options are
not wired yet and fail loud rather than degrade: resuming an existing session (--resume,
issue #154) and tool-sharing
(connectors.opencode.mcpServers). See Limits.
No install needed
Section titled “No install needed”OpenCode needs no setup step. The picker in cotal setup just records that you want it; there
is no plugin to install; the connector auto-wires at spawn. You only need the opencode binary
on your PATH. (Claude Code, by contrast, installs a plugin because its wake channel needs one.)
The connector supports two OpenCode lines: 1.x (opencode-ai 1.16 and later) and 2.x
(@opencode/cli 2.0 and later). It detects the line from opencode --version at spawn. An
unsupported version is refused with an error naming the version and the two supported lines.
Spawn it
Section titled “Spawn it”Same launch grammar as any agent (see run-a-mesh.md):
cotal spawn --agent opencode # foreground in this terminalcotal spawn researcher --agent opencode -d # detached via the manager; reattach with `cotal attach`Make OpenCode the default harness for spawns that don’t pass --agent:
COTAL_DEFAULT_AGENT=opencode cotal spawn # an explicit --agent always winsOr in a team manifest, set agent: opencode per agent (or as the team default).
Persona, role, and model come from the agent file the same way as for any connector: see
agent-files.md and define-a-team.md.
Choose a model
Section titled “Choose a model”OpenCode model ids use provider/model form, and a model may expose variants (a
connector-defined selector, e.g. a reasoning-effort tier). List what the running mesh’s OpenCode
can see:
cotal models --agent opencode # ids + variants, from the managercotal models --agent opencode --refresh # refresh the provider cache firstPick one at spawn, or set model: / variant: in the agent file (the flags win over the file):
cotal spawn --agent opencode --model anthropic/claude-sonnet-4-6 --variant highA --variant on a connector that doesn’t support variants is rejected up front; the OpenCode
connector advertises variant support, so this is the connector where it applies.
For an explicit model pin, the connector checks the running OpenCode server’s /provider
listing before joining the mesh. If that server does not list the model, launch refuses with
the id and names both the server listing and the opencode models --pure --verbose CLI catalog.
The CLI catalog alone does not prove that the server serving this session has the model.
OpenCode’s cold server bootstrap can take longer than the generic 30-second manager check;
the connector declares a two-minute readiness window for this check and the mesh join.
How it binds
Section titled “How it binds”OpenCode has a native plugin runtime, so the adapter is not an MCP server; a single in-process plugin does everything.
- Injected, never written. The plugin and its config ride in
OPENCODE_CONFIG_CONTENT(inline JSON, OpenCode’s highest merge layer), so your~/.config/opencodeis never touched. Because it’s a merge layer, a spawned OpenCode agent inherits the operator’s MCP servers (the opposite of Claude Code’s strict isolation), which is why tool-sharing is a separate, not-yet-built feature (see Limits). - Per-agent database. The session SQLite DB is moved per agent
(
.cotal/opencode/<name>/opencode.db, rooted at the manager’s workspace) so concurrent managed agents don’t lock each other or drop files into a target repo. A launch checks the agent’sserve.pidbefore it starts a server. A record that the previous launcher removes during that check counts as no record, and so does a recorded server that exits during it. An empty directory at that path also counts as no record and is removed, and one that vanishes before that removal still counts as no record. The launch refuses a directory that holds files and leaves it in place. - The visible TUI. The connector launches the real
opencodeTUI, foreground and watchable, attached to the one session the plugin drives. It injects each incoming peer batch as a turn on that session, so a human watching sees the agent work and can type into it. Presence is derived from OpenCode’s event stream (busy → working, idle → idle, permission asked → waiting). - A password on the server. The OpenCode server accepts only requests carrying a password the
launcher mints per launch, or one a headless host supplies as
OPENCODE_SERVER_PASSWORD. It reaches the server and the TUI only through their environment, and the headless[cotal-serve]line leaves it out. - Observed model. Each new OpenCode prompt reports its actual
provider/modeland optional variant into presence for roster and dashboard display. Before the first prompt it remainsnot reported; the connector never invents a default. An explicitmodel:orvariant:pin wins. - Quiet stays pull-only. Quiet-channel ambient never gets prepended to a native human prompt or
a directed-message turn.
cotal_inboxexplicitly surfaces and clears it; automatic traffic stays owned by the connector. Quiet-channel@mentions still drive a turn. - Focus
@mentions are held until delivered on 1.x. Infocusthe mention’s body is dropped at ingest, so the connector keeps a wake that tells the agent to read it withcotal_inbox. The wake stays pending until a turn carrying it is accepted, so a busy session, a refused turn or a failed submission only delays it. Several pending mentions share one wake. On a channel with replay off the wake only says the agent was mentioned, because the body cannot be recalled. - A stopping seat refuses prompts on 1.x. Once a stop has begun, a prompt typed into the TUI
or sent to the server API is refused before OpenCode saves it, so the agent starts no new model
turn after it has announced it is leaving. OpenCode reports the reason,
the prompt was not run: this seat is shutting down, as asession.errorevent for an asynchronous prompt and only in its server log for a synchronous one, which answers with a generic server error. A turn already running when the stop began is not cancelled. - An instance dispose ends the seat on 1.x. OpenCode can dispose an instance while its server
keeps running, for example after a global config change or on
POST /instance/dispose. The connector then runs the same teardown as a stop and exits the server. Whenever the server exits, the launcher closes its TUI, killing it if it is still up 3 seconds later, so the manager sees the seat end. /new= context reset. Running OpenCode’s built-in/newin that TUI starts a fresh context while keeping the same mesh identity and creds./reconnect= in-process recovery. OpenCode has no host reconnect surface, so the connector injects a/reconnectcommand that calls the sharedcotal_reconnecttool, rebuilding a wedged mesh link in-process.- Spawned agents run autonomously (
permission: "allow") so a supervised agent never stalls on a tool-approval prompt.
The generic tool surface and the inbound-message model are shared across connectors: see mcp-tools.md and connect-claude.md.
Event plane
Section titled “Event plane”A spawned session publishes a structured account of what it did: run
boundaries per turn, assistant text, and each tool call with its start and its end. Tool
arguments and tool results are not republished onto this channel.
The channel is events.<owner>.<actor>, named after the session’s principal, and the rules for it
are the same on every connector: see connect-claude.md for the
channel, the grant, and how to read it. The launcher sets COTAL_EVENTS by default; pass
--no-events to opt out on an unrestricted space. A required registration carries
eventsRequired in launch material, or COTAL_EVENTS_REQUIRED=1 on the direct env fallback, so a
personal user-mode OpenCode session arms without a separate event flag. Its own publish grant must
cover the principal-keyed event channel or the connector refuses before joining. The session boundary
is captured at adopt, before the mesh link connects, so a session created before the first bind still
publishes and nothing it writes while the connector is still starting up is silently dropped.
Four things are specific to OpenCode and worth knowing before you read a stream:
- No user-authored text is published, ever. When a peer message is injected into a native prompt, OpenCode prepends it into the human’s own text part, so one record holds peer-authored and human-authored content with no boundary in it to filter on. Rather than guess where one ends, the connector publishes no user text at all. Assistant text, reasoning and tool activity are unaffected.
- No step events and no usage. OpenCode’s step records carry no step name and no key shared between the start and the finish, and what the finish actually carries is cost and token counts. So the connector emits no step vocabulary rather than inventing a name, and the usage numbers are not carried in this version.
/newstarts a new thread on the same channel. OpenCode can hold several sessions in one process, and/newis a context reset that keeps the mesh identity. Each session publishes under its own thread id on the oneevents.<owner>.<actor>channel. Before the switch, the session you are leaving is flushed and its open run is closed, so a reader never holds a run that never ends.- Failed turns publish run errors. OpenCode reports a turn that
died (an upstream API error, a provider auth failure, or an output-length stop) on its own
session.errorevent, and that turn ends its run withRUN_ERRORcarrying the fixed messagerun failedand no code. Neither OpenCode’s reason nor its error name is published there: both are upstream values that can echo your prompt or tool output, and the events channel has a different read ACL. A turn you stopped is not a failure and is not published as one: a user cancellation arrives on the same event, and it closes the run as an ordinary end. A failed turn also re-arms the wake it carried, so a focus @mention whose turn failed is driven again after the retry delay.
Reasoning is off by default.
Limits
Section titled “Limits”- No session resume.
cotal spawn --resume <id>throws on OpenCode, because forking into an existing session needs session-creation plumbing, not an argv flag (issue #154). Connectors that support resume are listed in the matrix. - No tool-sharing.
connectors.opencode.mcpServersis not implemented and throws if set. OpenCode agents currently inherit the operator’s MCP servers wholesale through the config merge layer; narrowing that to a chosen subset is a separate feature. - On 2.x, the event plane needs
--no-events. The AG-UI event plane is not carried on OpenCode 2.x yet; spawn with--no-events. - On 2.x,
cotal modelsis refused. The 2.x catalog is served by a running opencode server, not the CLI, so pass--model provider/modeldirectly instead.
See also
Section titled “See also”- Connectors: the feature matrix across all connectors
- Run a mesh · Define a team · Watch a mesh
- MCP tools · Connect Claude Code · Connect Hermes · Connect pi
- Deploy against an external broker: running OpenCode agents in containers