ACP
opencode acp runs OpenCode as an Agent Client Protocol agent, so you can use it
from an ACP client such as Zed. This example adds OpenCode to Zed:
{
"agent_servers": {
"OpenCode": {
"command": "opencode",
"args": ["acp"]
}
}
}Other ACP clients use the same command and acp argument, but their configuration file and field names may differ. If
a graphical client can’t find opencode, set command to the absolute path that which opencode prints.
Authentication
OpenCode uses the provider credentials it already has. Sign in from a terminal before you start the ACP client:
opencode auth login
OpenCode offers one auth method, opencode-login. The authenticate request accepts only that method and doesn’t
collect credentials. If a prompt fails because a provider needs authentication, OpenCode returns ACP’s auth_required
error.
If the client sets clientCapabilities._meta["terminal-auth"] to true, the auth method includes the login command:
{
"id": "opencode-login",
"name": "Login with opencode",
"description": "Run `opencode auth login` in the terminal",
"_meta": { "terminal-auth": { "command": "opencode", "args": ["auth", "login"], "label": "OpenCode Login" } }
}
OpenCode doesn’t support ACP’s standard terminal auth yet. That is the clientCapabilities.auth.terminal capability
with a type: "terminal" auth method.
Troubleshooting
opencode acp waits for ACP messages on stdin, so it looks stuck when you run it directly in a terminal. Start it from
an ACP client instead.
If the client reports that the process exited or wrote invalid protocol output, add --print-logs before acp in the
client’s arguments. OpenCode then writes its ACP and private server logs to stderr. Stdout carries only ACP messages.
opencode --print-logs acp
Protocol reference
This section is for ACP client authors. It lists the parts of ACP that OpenCode supports and the _meta keys that
OpenCode adds. Clients that don’t recognize these keys can ignore them. The
ACP documentation defines the messages.
Transport
The client starts opencode acp as a child process. The two exchange newline-delimited JSON-RPC messages over stdin and
stdout, using ACP protocol version 1.
opencode acp starts a private OpenCode server for its own use. It doesn’t connect to the shared background service,
and it doesn’t open a network port for ACP. One process can serve many ACP sessions. It exits when the client closes
stdin.
Capabilities
OpenCode’s initialize response includes these agent capabilities:
{
"loadSession": true,
"mcpCapabilities": { "http": true, "sse": false },
"promptCapabilities": { "embeddedContext": true, "image": true },
"sessionCapabilities": {
"additionalDirectories": {},
"close": {},
"delete": {},
"fork": {},
"list": {},
"resume": {}
}
}
OpenCode reads these client capabilities:
| Capability | Effect |
|---|---|
elicitation.form | OpenCode asks questions as forms. |
session.compaction | OpenCode reports compaction with ACP’s updates. |
_meta["terminal-auth"] set to true | The auth method includes the login command. |
Sessions
The client sets the working directory when it creates a session. OpenCode uses that directory’s configuration, including its models, agents, commands, skills, instructions, plugins, and MCP servers.
session/loadreplays the saved messages.session/resumeandsession/forkdon’t replay them.- To load or resume a session, the client must pass the session’s own directory as
cwd. OpenCode rejects a differentcwdwith an invalid params error. - A fork keeps the source session’s directory.
session/listreturns up to 100 sessions per page. It accepts acwdfilter and acursor.session/closeinterrupts active work and detaches the session from the ACP process. OpenCode keeps the saved session.session/deleteremoves the session. Deleting a session that doesn’t exist succeeds.- A session runs one prompt at a time. OpenCode returns an error for a second
session/promptwhile the first is active.
Cancellation
session/cancel ends the active prompt with stopReason: "cancelled". A $/cancel_request for the prompt request does
the same. OpenCode rejects pending permission requests. It reports tool calls that don’t finish after a cancel as
failed.
Additional directories
The client can pass additionalDirectories on session/new, session/load, session/resume, and session/fork:
{ "cwd": "/work/app", "additionalDirectories": ["/work/shared"], "mcpServers": [] }
- Each entry must be an absolute path without
*or?. - OpenCode allows external directory access inside each root. Read, edit, and shell permission rules still apply.
- Each request’s list replaces the session’s previous list, as the additional directories RFD specifies.
- OpenCode stores the grants on the session, so they also apply when another OpenCode client uses the session. Child sessions copy the grants when OpenCode creates them.
session/list returns each session’s stored list in additionalDirectories.
Config options
New sessions use the directory’s default model and primary agent. The responses to session/new, session/load,
session/resume, and session/fork include these config options:
| ID | Category | Values |
|---|---|---|
model | model | Enabled models as provider/model. A /variant suffix also sets the effort. |
effort | thought_level | The model’s variants and default. Present only for models with variants. |
mode | mode | Visible primary agents. |
session/set_config_option takes string values:
{ "sessionId": "ses_123", "configId": "model", "value": "anthropic/claude-sonnet-4" }
session/set_mode also sets the agent. OpenCode sends config_option_update when the available models or agents
change. It also sends one when another client changes the session’s model or agent.
Prompt content
Prompts can contain text, images, embedded resources, and resource links. OpenCode ignores audio blocks. It handles the other blocks like this:
| Block | What OpenCode does |
|---|---|
text | Sends it to the model. |
text with audience assistant only | Sends it to the model as separate context, outside the user’s message. |
text with audience user only | Leaves it out of the model’s input. |
image with data | Attaches it to the prompt. |
image with only uri | Handles it as a resource link. |
resource with text | Inlines the text after the resource’s path or URI. |
resource with blob | Attaches it if it has a mimeType. Drops it otherwise. |
resource_link to a file:// URI | Attaches the file. Sends a Markdown link instead if the file is missing or unreadable. |
resource_link to a zed:// URI with a path query | Attaches the file at path. |
resource_link to another URI | Sends a Markdown link such as [spec](https://example.com/spec.md). |
The audience comes from the block’s annotations. OpenCode rejects an image block that has neither data nor uri.
This block is context for the model only:
{ "type": "text", "text": "Open file: src/app.ts", "annotations": { "audience": ["assistant"] } }
Commands
After session/new, session/load, session/resume, or session/fork, OpenCode sends available_commands_update
with the directory’s commands and the built-in compact command. It sends another update when the commands change.
{
"sessionUpdate": "available_commands_update",
"availableCommands": [{ "name": "compact", "description": "Compact the session" }]
}
If a prompt starts with / and the name of an available command, OpenCode runs that command with the rest of the text
as arguments. OpenCode sends other text that starts with / as a normal prompt. /compact runs
compaction.
MCP servers
The client can pass stdio and HTTP MCP servers on session/new, session/load, session/resume, and session/fork.
OpenCode adds them to the session’s directory for the life of the opencode acp process, so other sessions in that
directory can use them. OpenCode passes HTTP headers through and doesn’t use OAuth for these servers. It doesn’t
advertise MCP over SSE, and it doesn’t support MCP over ACP.
{ "type": "http", "name": "docs", "url": "https://mcp.example.com", "headers": [] }
Permissions
Permission requests offer three options:
[
{ "optionId": "once", "kind": "allow_once", "name": "Allow once" },
{ "optionId": "always", "kind": "allow_always", "name": "Always allow" },
{ "optionId": "reject", "kind": "reject_once", "name": "Reject" }
]
When a request changes files, OpenCode includes diff content if it can build a preview of the change.
Questions
If the client advertises elicitation.form, OpenCode asks questions with elicitation/create in form mode. The request
includes toolCallId when OpenCode sent that tool call as a session/update for this session.
{
"mode": "form",
"sessionId": "ses_123",
"toolCallId": "call_1",
"message": "Which package manager?",
"requestedSchema": {
"type": "object",
"properties": { "pm": { "type": "string", "oneOf": [{ "const": "bun", "title": "Bun" }] } },
"required": ["pm"]
}
}
- OpenCode never sends forms that might collect secrets.
- If a field accepts a typed answer next to its options, the schema adds an optional
<key>_customstring property. For a single choice, a non-empty custom answer replaces the selected option. For a multiple choice, OpenCode adds it to the selected options. - If the user declines or cancels the form, OpenCode cancels the question.
If OpenCode can’t show a question, it cancels the question and tells the model to continue without an answer.
Compaction
If the client advertises session.compaction, OpenCode reports compaction with the compaction_update and
compaction_summary_chunk updates from ACP’s session compaction
RFD, which is in Preview. When the client loads a session, OpenCode replays each completed or failed compaction as one
update.
{
"sessionUpdate": "compaction_update",
"compactionId": "msg_123",
"status": "completed",
"summary": [{ "type": "text", "text": "..." }]
}
Other clients receive the compaction marker instead. So do child session compactions.
Usage
The prompt response includes usage from the end-turn usage
RFD, which is a draft. usage sums the prompt’s model calls in this session. It doesn’t include child sessions.
{ "stopReason": "end_turn", "usage": { "inputTokens": 1200, "outputTokens": 340, "totalTokens": 1540 }, "_meta": {} }
Before the response, OpenCode sends usage_update from the
session usage RFD. It sends it only if the prompt used tokens and
OpenCode knows the model’s context size. cost is the session’s total cost in USD.
{ "sessionUpdate": "usage_update", "used": 1540, "size": 200000, "cost": { "amount": 0.012, "currency": "USD" } }
Child sessions
While a prompt runs, OpenCode sends updates from its child sessions as session/update notifications on the prompt’s
session. Each update carries _meta["opencode/child-session"]. OpenCode prefixes tool call IDs with
<childSessionId>:. It prefixes tool call titles with the child session’s title if the child session has one.
{
"sessionUpdate": "tool_call",
"toolCallId": "ses_child:call_1",
"title": "Explore the codebase: src/app.ts",
"_meta": {
"opencode/child-session": { "id": "ses_child", "parentID": "ses_123", "depth": 1, "title": "Explore the codebase" }
}
}
| Field | Type | Description |
|---|---|---|
id | string | The child session’s ID. |
parentID | string | The ID of the child session’s parent session. |
depth | number | 1 for a child of the prompt’s session, 2 for a child of that child session, and so on. |
title | string | The child session’s title. OpenCode omits it when the child session has no title. |
Permission requests from a child session carry the same key on toolCall._meta. After the prompt ends, OpenCode stops
sending child session updates. Permission requests and questions from child sessions still reach the client.
Retry status
When a model call fails and OpenCode schedules a retry, it sends session_info_update with _meta["opencode/retry"].
When the retry starts, OpenCode sends "opencode/retry": null. Clear any retry status when the prompt response arrives.
{
"sessionUpdate": "session_info_update",
"_meta": {
"opencode/retry": {
"attempt": 1,
"nextRetryAt": "2026-10-02T12:00:05.000Z",
"error": { "type": "provider.rate-limit", "message": "Rate limit exceeded", "status": 429 }
}
}
}
| Field | Type | Description |
|---|---|---|
attempt | number | The retry attempt number. |
nextRetryAt | string | The time of the next attempt, in ISO 8601 format. |
error | object | Has type and message. Also has status and response.body if the provider sent them. |
Compaction marker
If the client doesn’t advertise session.compaction, OpenCode reports compaction progress as session_info_update with
_meta["opencode/compaction"]. OpenCode always uses the marker for child session compactions.
{
"sessionUpdate": "session_info_update",
"_meta": { "opencode/compaction": { "status": "started", "messageId": "msg_123", "reason": "auto" } }
}
| Field | Type | Description |
|---|---|---|
status | string | started, completed, or failed. A cancelled compaction is failed. |
messageId | string | Pairs a compaction’s start with its end. Treat it as an opaque key. |
reason | string | auto or manual. |
error | object | Present when status is failed. Same shape as the retry error. |
The marker doesn’t include a summary. An automatic compaction can fail without a started marker. When the client
loads a session, OpenCode replays each completed or failed compaction as one marker.