Coding

OpenCode ACP Control

Try it

Drive OpenCode CLI sessions programmatically over the Agent Client Protocol from another agent.

What it does

Launches an OpenCode process in the background and talks to it over JSON-RPC 2.0 using newline-delimited frames on stdin and stdout. Covers the full session lifecycle: initialize handshake with fs.readTextFile, fs.writeTextFile, and terminal capabilities declared; session/new to obtain an opaque sessionId; session/prompt to send turns; polling stdout every ~2s while collecting session/update notifications until a response with a stopReason arrives; and session/cancel as a notification. Resumes prior sessions via session/load, forwards requestPermission prompts so the caller can reply once, always, or reject, and detects auto-updates by comparing opencode --version with the latest GitHub rel…

When to use it

  • Spawn OpenCode from inside another agent to automate coding work over ACP
  • Drive multi-turn prompt, stream, and cancel loops over JSON-RPC
  • Resume a previous OpenCode session by its session id
  • Detect and trigger OpenCode auto-updates by comparing versions

The skill document

OpenCode ACP Skill

Drive an OpenCode CLI session over the Agent Client Protocol (ACP).

When to use this skill

Use it when the calling agent must:

  • Start an OpenCode process and talk to it programmatically (not as a human at a terminal).
  • Drive multi-turn coding sessions, including prompt → stream → cancel loops.
  • Resume a previous OpenCode session by ID.
  • Detect and trigger OpenCode auto-updates.

Do not use it for direct file editing, one-off shell commands, or any task that does not require the Agent Client Protocol.

Quick Reference

ActionGeneric tool call
Start OpenCodeterminal(command: "opencode acp --cwd /path/to/project", background: true)
Send JSON-RPC frameprocess.write(processId, "\n")
Read available outputprocess.poll(processId) (repeat every ~2s)
Stop OpenCodeprocess.kill(processId)
List past sessionsterminal(command: "opencode session list", workdir: "")
Get current versionterminal(command: "opencode --version")
Prompt the userask_user(question, options)

The calling agent must map these generic names to its own platform (Hermes, Clawdbot, etc.). See README.md for the mapping table.

Protocol Rules

  • Wire format: JSON-RPC 2.0, newline-delimited (one JSON object per line, each frame terminated by \n). Not LSP Content-Length.
  • Direction: requests are agent → OpenCode (stdin). Responses and server notifications arrive on stdout.
  • IDs: every request carries an integer id; the calling agent increments monotonically starting at 0. Notifications have no id and never produce a response.
  • Sessions are opaque: the sessionId returned by session/new is the only reference; treat it as a string.
  • Capabilities: declare fs.readTextFile, fs.writeTextFile, and terminal in the initialize handshake.

Standard Workflow

1. Start

terminal(
  command: "opencode acp --cwd /path/to/project",
  background: true,
  workdir: "/path/to/project"
)

Save the returned processId. All subsequent frames go through it.

2. Initialize

{"jsonrpc":"2.0","id":0,"method":"initialize","params":{
  "protocolVersion":1,
  "clientCapabilities":{
    "fs":{"readTextFile":true,"writeTextFile":true},
    "terminal":true
  },
  "clientInfo":{
    "name":"opencode-acp-control",
    "title":"OpenCode ACP Control",
    "version":"0.3.0"
  }
}}

Expect result.protocolVersion: 1.

3. Create session

{"jsonrpc":"2.0","id":1,"method":"session/new","params":{
  "cwd":"/path/to/project",
  "mcpServers":[]
}}

Save result.sessionId (e.g. "sess_abc123").

4. Send prompt

{"jsonrpc":"2.0","id":2,"method":"session/prompt","params":{
  "sessionId":"sess_abc123",
  "prompt":[{"type":"text","text":"List the TypeScript files in this repo."}]
}}

5. Stream the response

Poll stdout every ~2s until a response arrives whose id matches your request and whose result.stopReason is set. While polling you will also receive notifications:

{"jsonrpc":"2.0","method":"session/update","params":{...}}

Collect them in order — they make up the agent's streamed output.

6. Cancel (when needed)

{"jsonrpc":"2.0","method":"session/cancel","params":{"sessionId":"sess_abc123"}}

No response is sent for a cancel — it is a notification.

7. Handle permission requests

OpenCode asks for confirmation before running shell commands or editing files by sending a server-to-client request:

{"jsonrpc":"2.0","id":12,"method":"requestPermission","params":{
  "sessionId":"sess_abc123",
  "toolCall":{"toolCallId":"call_1","status":"pending",
              "title":"bash","rawInput":{"command":"npm install"},"kind":"bash"}
}}

Prompt the user, then respond with the matching id:

{"jsonrpc":"2.0","id":12,"result":{"reply":"once"}}      // allow once
{"jsonrpc":"2.0","id":12,"result":{"reply":"always"}}    // allow for the session
{"jsonrpc":"2.0","id":12,"result":{"reply":"reject"}}    // deny

State to Track

For each OpenCode instance the calling agent holds:

FieldSource
processIdReturned by the terminal(background:true) call
sessionIdReturned by session/new (OpenCode-internal)
nextIdInteger counter for the next request, starting at 0
stopReasonLast terminal reason observed (end_turn, cancelled, max_tokens)

Polling and Timeout Strategy

  • Interval: 2 seconds between process.poll calls.
  • Maximum wait per prompt: 5 minutes (150 polls). Treat as timeout beyond.
  • An empty poll response means the agent is still thinking — keep polling.
  • A malformed line on stdout is logged and skipped; do not abort on parse errors alone.

Resume Session

  1. terminal("opencode session list", workdir: "") returns a table of {id, updated, messages}.

  2. ask_user to pick one.

  3. terminal("opencode acp --cwd ", background:true) to restart.

  4. initialize (id=0).

  5. session/load with the chosen id, plus cwd and mcpServers:

    {"jsonrpc":"2.0","id":1,"method":"session/load","params":{
      "sessionId":"sess_abc123","cwd":"/path/to/project","mcpServers":[]
    }}
    

OpenCode streams the full conversation history back through session/update notifications.

Failure Modes

SymptomLikely causeAction
Empty polls for >5 minLong agent thinking, model stall, or network dropCancel + restart
parse error on stdoutGarbled binary output or partial frameSkip the line, continue
Process exits unexpectedlyOpenCode crashedInspect stderr, restart
initialize rejects protocolVersionOpenCode < v1.1.0 or client driftUpgrade OpenCode, align clientInfo.version
requestPermission keeps arrivingSession is in a tool-call loopCancel, narrow the prompt
session/load 404sStale or deleted session idFall back to session/new

Update Procedure

OpenCode auto-updates on restart. To check and trigger an update:

  1. terminal("opencode --version") → current version.

  2. web_fetch("https://github.com/sst/opencode/releases/latest") → latest tag in the redirect URL.

  3. Compare versions. If newer:

    • process(action:"list") to find every running opencode acp process.
    • process.kill(processId) for each.
    • Wait ~2 seconds.
    • terminal("opencode acp", background:true) to restart and trigger the auto-download.
  4. Verify with opencode --version again. If still old, fall back to a manual install (review the installer script before piping curl | bash):

    curl -fsSL https://opencode.ai/install | bash
    

Implementation Notes

  • cwd must be absolute; normalize before sending.
  • Serialize JSON deterministically (sort_keys=True if possible) to make log diffs stable.
  • Treat stderr separately from stdout if your platform exposes it — ACP frames never appear on stderr.
  • When the host agent runs in a sandbox, cwd must be inside the sandbox; the ACP server inherits the calling agent's filesystem access.
  • Session ids are opaque strings; do not parse them.
  • The first initialize after a process start must complete before any other request — JSON-RPC servers reject out-of-order calls.

See also

  • examples/acp_demo.py — runnable end-to-end demo with --dry-run and --no-prompt modes.
  • README.md — quick start and tool-platform mapping.
  • CHANGELOG.md — release notes.
  • ACP spec:
  • OpenCode:

Questions people ask

Does this skill edit files or run shell commands on its own?
No. It only wraps OpenCode over ACP. The SKILL.md explicitly says do not use it for direct file editing or one-off shell commands; OpenCode itself performs those actions through permission-gated tool calls.
How does the calling agent receive streamed output?
It polls stdout every ~2 seconds and collects session/update notifications in order until a response arrives whose id matches the request and whose result.stopReason is set (end_turn, cancelled, or max_tokens). Empty polls mean OpenCode is still thinking and do not end the wait until the 5-minute cap.
How are permission prompts handled?
OpenCode sends requestPermission as a server-to-client request with its own id. The caller prompts the user, then replies on the matching id with result.reply set to once, always, or reject. The skill does not decide policy for you.

Related skills

Join a video meeting as an AI bot with voice, avatar, and screenshare across four operating modes.

by johnpatternai22 installs8 stars

Stores durable facts in a categorized, plain-markdown vault on disk, alongside your agent's built-in memory.

by Iván558 installs18 stars

Generate and edit Draw.io, Mermaid, and Excalidraw diagrams from natural language using a structured JSON spec.

by nssa.io1.0k installs47 stars

Fetch raw ad creative, app, ranking, and revenue data from AdMapix as structured JSON.

by fly0pants4.3k installs296 stars

Find why your productivity system keeps failing, then apply the smallest fix — capacity math, bottleneck routing, durable local notes.

by Iván855 installs69 stars