New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

opencode-courier

Package Overview
Dependencies
Maintainers
1
Versions
14
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

opencode-courier

Multi-session orchestration for OpenCode V2: spawn child sessions, be woken by them. No polling.

latest
Source
npmnpm
Version
0.3.0
Version published
Weekly downloads
2.1K
Maintainers
1
Weekly downloads
 
Created
Source

opencode-courier

Your OpenCode session hands work to other sessions, ends its turn, and is woken when they report. Multi-session orchestration for OpenCode V2: spawn child sessions, be woken by them. No polling.

CI Quality Gate Coverage Security Rating Maintainability Rating npm Socket Badge License: MIT Supported OpenCode V2 version

A parent spawns two slow children, writes a README itself, and is woken by each report

A parent writes a README while two slow children work, and each report wakes it (the prompt).

Quickstart

npm install -g @opencode/cli@2.0.26   # OpenCode V2, at the version courier is tested on
opencode plugin add opencode-courier  # by name, so OpenCode offers new releases
opencode                              # in any folder

Paste this prompt:

Start two helper sessions with courier_spawn and have each report back to you. One runs `sleep 20; echo $((17 * 23))`, the other `sleep 40; echo $((2 ** 10))`. Do not run the commands yourself. When both have reported, reply with one line: RESULTS <first> <second>

The parent spawns both children and ends its turn. Within a minute the first report starts a new turn on its own, about 20 seconds later the second does, and the parent replies RESULTS 391 1024. If OpenCode asks to allow a child's command, the parent passes the question to you. Free models sometimes stall on a request; if nothing moves for a few minutes, send the prompt again. Next: Using it.

OpenCode V2 native

Courier is built on OpenCode V2's plugin API (@opencode/plugin), not the V1 one (@opencode-ai/plugin), and needs OpenCode V2. Each release is pinned to one V2 version and tested against it end to end; the current one is tested on OpenCode 2.0.26. CI also runs the live suite on the newest OpenCode release, so a host release that breaks the plugin shows up there first. Older releases and their versions: Supported OpenCode version.

Status: early. Passes an end-to-end test inside a live OpenCode V2 server driven by a scripted stand-in model, and a smoke test with real (free) models: see Development.

Courier or the built-in subagent tool?

OpenCode V2 has its own subagent tool: it starts an agent with fresh context in a child session, and the parent either waits for its final answer or, with background: true, carries on and is notified when the child finishes. Passing the child's sessionID back sends it another prompt, steering it if it is running and waking it if it is idle. Use it when one result back is all you need: a search, a review, a self-contained task that needs nothing from you on the way.

Use courier when the work needs more than one result back. On top of what the built-in tool does, it adds:

  • Messages from the child, mid-run: with the built-in tool a child's one message to its parent is its final answer. With courier a child reports progress, or asks its parent something, whenever it likes with courier_send, and any session can message any other.
  • Waking an idle session from anywhere: a message from a child, a schedule or a webhook starts a new turn in a session that has ended its own.
  • Scheduled messages: courier_later wakes a session at a set time, for check-ins and reminders.
  • Webhooks: GitHub reviews, comments and CI runs, or any signed POST, wake the subscribed session.
  • Worktree isolation: isolate: true gives a child its own git worktree, so children can edit files in parallel. An isolated child that splits hands its committed work down: its isolated children start from its HEAD, and each isolated child commits on courier/<its session ID> and reports that branch for its parent to merge (nested isolation).
  • Relaying to you: a child's permission requests and questions reach you through the parent, and your answers go back; a form only you can answer is pointed out to you.

How it works

A parent session calls courier_spawn, gets a session id back immediately and ends its turn. The child works on its own and, when it is done or stuck, calls courier_send with the parent's id, a status and what it produced. That message lands in the parent's inbox and OpenCode starts a new turn for the parent if it is idle. A child whose turn fails instead, so that it cannot report, or ends without reporting, is reported by the plugin, and a child that waits for a permission or asks a question has it passed to the parent, who asks you and passes your answer back. A child that waits on a form only you can answer, such as OpenCode asking which web search provider to use, has its parent told so.

There is no polling anywhere: a message is admitted into the target session's inbox through OpenCode's own API, which wakes the session, the same way OpenCode's background subagents report to their parent. Delivery is steer by default (injected into the target's running turn, or starts one if idle); queue: true waits until the current turn ends.

Demos

The parent does not wait. Prompt: "Spawn two children: one runs sleep 30 then lists the exports of math.js, the other runs sleep 45 then lists the exports of text.js. Don't wait for them: meanwhile write a short README.md for this folder yourself, and add their reports to it when they arrive."

The recording at the top is this run. Watch the parent write its README and finish its turn 16 seconds in, while both children still sleep. Each report then starts a new turn on its own (the Message from ses_… line is the child's message): the first after about 30 seconds, the second after about 45, and the parent folds each into the README and cancels the check-in it had scheduled for itself.

A child's question reaches you. Prompt: "Spawn a child session to add a multiply function to math.js. It must ask me first whether to name it multiply, times or product."

A child's question opens as a form in the parent's session, times is picked, and the child carries on

Watch the child's question open in the parent's session as OpenCode's own question form, where times is picked. The parent passes it back, and the child adds times to math.js and reports.

Both recordings are real runs with a free model on OpenCode Zen (Muse Spark 1.3).

Recursive orchestration

A session splits a goal across child sessions, each child decides in turn whether to split its part again, and the results are checked and integrated at every level on the way back up. The plugin ships the playbook as an OpenCode skill, courier-orchestrate, listed in every session with the plugin loaded: how to decide whether to split, and to say which in one line of the reply (split: no, because …; considered: <the parts you weighed> or split: 3 parts: …), plan the parts, start them in one join group, end the turn, read each report by its status, verify first-hand through the project's scripts, integrate and report upward, and what to do when a part fails, ends quietly, blocks or hits a limit. It names the parts an implementation task with one tightly coupled core usually still has: its e2e scenario, its documentation, sometimes a second test file, and a review of the core by a session with fresh context (isolate: false, it edits nothing). The session does the core itself first and commits it, then starts one session per remaining part, if 2 or more remain; a review session's findings are a report to act on, and running the checks stays the session's own. Its description asks a model to load it at the start of any implementation task that may have independent parts, and a session nobody spawned is pointed to it by its system prompt as well (Session trees); /skills in the TUI attaches it to your prompt. Its text: the reference.

The smoke test's recursive scenario (COURIER_SCENARIO=recursive, docs/real-model.md) asks a free model:

Have this job done through helper sessions started with courier_spawn, one session per part, and do no part yourself. It has two parts. Part "numbers" is itself two independent halves, which the session that gets it hands to two sessions of its own: one runs `sleep 20; echo $((17 * 23))` and writes the number it printed to numbers/a.txt, the other runs `sleep 20; echo $((2 ** 10))` and writes the number it printed to numbers/b.txt. Part "count" runs `sleep 20; echo $((99 - 57))` and writes the number it printed to count/total.txt. When the parts have reported back, read the three files yourself and reply with one line: RESULTS <a> <b> <total>

The root starts two sessions and ends its turn; the "numbers" session starts two of its own, ends its turn, checks both files when their reports wake it and reports once to the root; the "count" session does its part itself. The root is woken by the two reports, reads the three files and replies RESULTS 391 1024 42. The checker verifies the shape of the tree, that no session polled or went past maxDepth, that every report carries a status and each sub-orchestrator reported once, after its children, and the files and the final line.

Install

Supported OpenCode version

Requires OpenCode V2: the opencode command from @opencode/cli (also installed as opencode2, the name of the beta line). Each release of this plugin is built and tested against exactly one OpenCode V2 version, and a release that moves the pin adds a row here. OpenCode itself never checks the version: the plugin installs and loads on any V2 host. When it loads on a version other than the pinned one, it writes one line to the server log naming both versions, so a mismatch is named before a tool fails. More in the reference.

opencode-courierOpenCode V2 (opencode and @opencode/plugin)
0.2.32.0.26
0.2.22.0.24
0.2.12.0.23
0.2.02.0.22
0.1.60.0.0-beta-19271 (the beta line: opencode2 from @opencode-ai/cli, and @opencode-ai/plugin)

Check yours with opencode --version, and install the matching CLI with:

npm install -g @opencode/cli@2.0.26

The plugin

opencode plugin add opencode-courier

This installs the package from npm and adds "opencode-courier" to plugins in the global configuration (~/.config/opencode/opencode.json). To receive webhooks, replace that entry with the object form shown under Webhooks, which carries a webhook option; the session tree limits are options of the same entry.

Install it by name, without a version: OpenCode only checks plugins for updates when their entry is not an exact version, so opencode-courier@0.2.1 is never offered a newer release. How to move an entry that carries a version to the name, and what to look at when an update still does not show: Updating the plugin.

From a local clone

git clone https://github.com/ivopogace/opencode-courier && cd opencode-courier
bun install && npm run build

Then list it in opencode.json (V2 uses plugins, plural). A local plugin path must be a directory; OpenCode loads its index.js, and ignores a path to a file with a warning:

{
  "plugins": ["/absolute/path/to/opencode-courier/dist"]
}

Using it

  • Keep the background server running so sessions can be woken while you are away (opencode service start; opencode service status to check).
  • Give the agents that run children the permissions their work needs. A child that hits an approval prompt has it passed to its parent, which asks you, but the child waits until you answer, so keep prompts for what you want to decide yourself. A child's question goes to the parent the same way, so let the agents that run children use the question tool if they should be able to ask you; OpenCode's default agent may. Choose a web search provider before children search the web (run one web search in your own session and answer OpenCode's prompt, or use its "Third-party search" setting): otherwise the first child to search shows that prompt in its own session, which only you can answer there.
  • Your own session's prompts are relayed nowhere, and each one stalls the whole tree until you answer it. Before an unattended run, allow the orchestrating session's toolchain up front: the repository's bun run and npm run scripts, git, and whatever its checks need. Prefer one rule on bun run * or npm run * over a rule per binary; the skill tells a session that orchestrates to run checks through the project's scripts rather than ./node_modules/.bin/<tool>, so one rule covers them.
  • Use isolate: true whenever children edit files in parallel. The child's worktree is made from the last commit (from an isolated session, its HEAD), so an uncommitted opencode.json is not there and the child falls back to your global config: keep providers and models in the global config, or commit the file. When you are done with an isolated child, courier_cleanup it so its worktree does not linger.
  • A child whose turn fails, or ends without courier_send, is reported to its parent by the plugin. What the plugin cannot see, such as a server that stops while a child runs, a courier_later check-in still covers: for a long-running child, schedule one for yourself and courier_cancel it when the child reports.

Tools

ToolDoes
courier_spawnCreates a session (optionally in its own git worktree with isolate: true, which from an isolated session starts at its HEAD, nested isolation) on the parent's model, sends it the task plus a brief naming the parent, how to report back and when to split the task further, and returns at once. With a group name, the reports of every session started under that name are held and delivered together, in one message (reference). Refused past the session tree limits.
courier_sendDelivers a message to a session, signed with the sender's id, waking it if idle. From a child to its parent, a status (done, partial, blocked or failed) makes it the child's report, carried as an attribute of the message (<courier from="…" status="done">), and artifacts (branch, commits, files, checks) are listed in its body in a fixed layout; a message without a status is progress, not the report (reference). From a member of a join group, a report with done, partial or failed is held with the group instead, and the result says so.
courier_statusOne look at a session: outcome, idle time, last reply and the permission requests and questions it waits on. For check-ins, not for waiting.
courier_childrenLists the sessions this one (or a given sessionID) started with courier_spawn, each with what courier_status reports plus its directory, whether it is isolated, when it was started and, for one started in a group, the group and whether its report is held, out or released.
courier_treeLists the whole tree under this session (or a given sessionID): every session started from it with courier_spawn, directly or through others, depth first, each with what courier_children shows plus its depth, its parent, and its report state (whether it owes a report, sent progress, ended without one, and the status of its last report). Cut at maxTotal sessions (reference).
courier_stopInterrupts a session you started, directly or through others, and every session under it, deepest first, and cancels the courier_later messages scheduled for them. With cleanup: true it also removes their worktrees, by courier_cleanup's rules (kept ones are listed). You are not told of what you stopped; a session between you and the target is, once, as ended="stopped" (reference).
courier_cleanupRemoves the git worktree of a child started with isolate: true and drops the child from courier_children. Keeps a worktree with uncommitted changes or commits on no branch, tag or remote and lists them, unless force: true is passed (reference). A child without a worktree is only dropped from courier_children; either way, a join group member that has not reported is dropped from its group, which is released without it (reference).
courier_answerPasses your answer to a permission request or a question that a session started from this one waits on: reply (once, always or reject, with an optional message) for a permission request, answers for a question.
courier_laterSchedules a message for a session (this one by default) in delayMinutes or at an ISO time, and returns an id. When due it is delivered like courier_send, queued behind any running turn and waking the session if idle.
courier_cancelDrops a message scheduled with courier_later, e.g. because the child it was waiting for reported first.
courier_subscribeSubscribes a session (this one by default) to webhook deliveries for a topic: owner/repo, owner/repo#12 (one pull request or issue) or a generic name. Each matching delivery arrives as a message, queued behind any running turn and waking the session if idle. Needs the webhook receiver.
courier_unsubscribeDrops one topic, or all of a session's, e.g. once its pull request is merged.

How children reach you

The short version; the long one, with every edge, is docs/reference.md.

  • A child runs on its parent's model, not on OpenCode's default, so a parent you moved to another model starts children that can reach theirs too. An agent with a model of its own keeps it. More.
  • A child can split its task with courier_spawn of its own, within the limits: its brief says when to split and when to do the work itself, and a session that splits checks its children's work and sends one combined report. More.
  • A child reports once, with a status: done, partial, blocked (it needs a decision from its parent, and says what) or failed, as an attribute of the message, with its artifacts listed in the body. A message without a status is progress, so a child that sends "halfway" and then stops is still reported as ending without a report. More.
  • A join group wakes you once. Start a batch of sessions with the same group name, and their done, partial and failed reports are held until the last one is in and your turn has ended, then delivered in one message, marked group="<name>", listing every report with its status and artifacts in the order the sessions were started. A blocked report, progress, a failed turn, a silent end, a permission request and a question reach you at once, as ever; a member whose turn fails or is interrupted leaves the group, which is released without it. A member that will not report (it ended its turn silently, or you gave up on it) holds the others back: drop it with courier_cleanup, and the group is released without it, the message naming it; a report it sends after all comes on its own. Held reports survive a restart, and the name is free again once the group has been delivered. More.
  • A child that fails cannot report, so the plugin does: every failed turn of a spawned session sends its parent a message marked failed="<error type>", with the error, waking it if idle. More.
  • A child that ends its turn without reporting to its parent has the parent told, marked ended="without-report", with its last reply, waking it if idle; it counts toward the limits until it reports. One that waits, on a request, a session it started, a scheduled message or a webhook it just subscribed to, is not reported, nor is one that answers what you typed in its session. More.
  • A child that asks for permission has the request passed to the session at the top, with what it asks for and the choices OpenCode offers (once, always, reject). That session asks you and answers with courier_answer; the child carries on. More.
  • A child that asks a question with OpenCode's question tool has it shown in the top session. When that session asks you the same question, your answer goes to the child's waiting call as if you had answered there. Whichever answer reaches the child first counts, in its session, in the parent's or by courier_answer, and the other question is withdrawn; a question stays answerable across an interrupted turn or a server restart. More.
  • A child that shows a form the plugin cannot pass on, such as OpenCode's web search asking for a provider, has the top session told, marked asks="form", with the form's choices: only you can answer it, in the child's session. More.
  • You can see and stop a whole subtree. courier_tree lists every session under yours with its depth and report state; courier_stop interrupts one of them and everything under it, deepest first, and cancels their scheduled messages. A stop is neither a failure nor a silent end, and only a session above the target may order it. More.
  • The plugin remembers. Each parent's children (courier_children) and whether each owes a report, held group reports, pending courier_later messages and open questions survive a compaction or a restart; entries are dropped after 14 days. Roster, Scheduled messages.
  • One OpenCode server per data directory, preferably. A second server on the same one, such as opencode serve next to opencode service, shares the plugin's storage: one of the two delivers courier_later messages and released groups, but a child's permission request can only be answered from the server that runs the child. More.
  • Worktrees are yours to remove. An isolated child's worktree is kept until courier_cleanup, which refuses to drop uncommitted changes or unbranched commits unless told to. More.

Session trees

A spawned session may start sessions of its own, so the sessions form a tree, and three options bound it. Set them in the plugin's options, in the global config (~/.config/opencode/opencode.json):

{
  "plugins": [{ "package": "opencode-courier", "options": { "maxDepth": 2, "maxChildren": 3 } }]
}
OptionDefault
maxDepth3How deep the tree goes: your session is depth 0, the sessions it starts depth 1. A session at maxDepth cannot start sessions.
maxChildren5How many sessions one session started may have live at once.
maxTotal20How many spawned sessions may be live at once in one tree.

courier_spawn refuses a spawn past a limit, naming it and saying what to do instead: do the work itself, or wait for a child to report. A session counts while it is live: its turn runs, it owes its parent a report (its task or a message reached it after it last reported, failed or was interrupted), or a session below it is live. One that has reported, with nothing live below it, does not. Each spawned session's model is also told its role on every request: sub-orchestrator, which may split its task, or leaf, at maxDepth, which does not see courier_spawn at all. Your own session is told, in one line, to load the courier-orchestrate skill before a task that touches several files or areas, decide whether to split it, and say which in one line of its reply (split: no, because …; considered: <the parts you weighed> or split: 3 parts: …); once it has started a session, that line becomes a root orchestrator part with the limits. The details: the reference.

Webhooks

The receiver is off unless the plugin has a webhook option. Put it in the global config (~/.config/opencode/opencode.json), since there is one receiver per OpenCode server:

{
  "plugins": [
    {
      "package": "opencode-courier",
      "options": { "webhook": { "port": 4097, "secretFile": "~/.config/opencode/courier-webhook-secret" } }
    }
  ]
}

From a local clone, package is the path to its dist directory instead. "webhook": true takes every default. If the option is given more than once, for example in a project's config as well, the first location to load wins, and the others log that their settings are ignored.

OptionDefault
port4097Port to listen on.
host127.0.0.1Address to bind. Only this machine can reach the default.
secretFileFile holding the shared secret (~ is expanded).
secretEnvCOURIER_WEBHOOK_SECRETEnvironment variable holding it, when there is no secretFile.
maxBytes1048576Largest body accepted.

The secret is never read from opencode.json itself (a secret key is refused), so the config can be committed. Make one with openssl rand -hex 32 > ~/.config/opencode/courier-webhook-secret and chmod 600 it. A file is the safer choice with opencode service start, whose environment may not be your shell's. Without a usable secret the receiver does not start, and the server log says why.

On GitHub, add a webhook to the repository (Settings → Webhooks) with content type application/json, the same secret, and the events you want (pull request reviews, review comments, issue comments, pull requests, check suites or workflow runs). GitHub must reach the receiver, and by default it only listens on 127.0.0.1: forward a public URL to it with a tunnel you trust (cloudflared tunnel --url http://127.0.0.1:4097, ngrok http 4097, or smee --url https://smee.io/<channel> --target http://127.0.0.1:4097/github, which needs no inbound port at all) and use <public URL>/github as the payload URL. Whatever you expose, only signed deliveries are acted on.

The receiver starts when OpenCode loads the plugin, which after a server start happens the first time a project is used. Until then deliveries fail; GitHub does not retry them on its own, but lists them under Recent Deliveries with a Redeliver button.

Once it runs, courier_subscribe ties a session to a topic and every matching delivery arrives as a message, queued behind any running turn and waking the session if idle:

  • POST /github takes GitHub webhook deliveries: reviews, comments, pull requests and issues being opened, closed or merged, and completed CI runs reach the sessions subscribed to owner/repo#N and owner/repo; other events with a repository reach owner/repo.
  • POST /hook/<name> takes anything else, for sessions subscribed to <name>: a JSON body's text, summary or message field, otherwise the body itself.

Every delivery must be signed (X-Hub-Signature-256, HMAC-SHA256 under the shared secret), replays are refused, and what a session sees is a short summary marked as outside text. Which events wake whom, the signing scheme for /hook/<name>, limits and status codes: the reference.

Development

bun install
bun test           # unit tests, with a fake plugin context
npm run typecheck
npm run build      # emits dist/
OPENCODE_BIN=$(which opencode) npm run test:e2e   # live test, about two minutes, no API key
OPENCODE_BIN=$(which opencode) e2e/real-model.sh  # smoke test with a real model, not in CI

The live test (e2e/run.sh) starts a real OpenCode V2 server in a throwaway project and home directory, with this plugin loaded and a scripted OpenAI-compatible stand-in model, and walks every tool through the behaviour above. It needs node, npm, bun, git, curl, jq and openssl. New behaviour gets a scenario there.

The smoke test (e2e/real-model.sh) runs the same server with a real model (by default a free one on OpenCode Zen, no key needed) and checks that the parent spawns instead of doing the work, ends its turn instead of polling, and is woken by each report; COURIER_SCENARIO=permission and COURIER_SCENARIO=question exercise the two relays, and COURIER_SCENARIO=recursive the recursive orchestration above. Which models pass and what was tuned for them: docs/real-model.md.

CI runs both suites on every push to main and every pull request, with the OpenCode CLI at the pinned version, and the live suite once more on the newest OpenCode release, where a failure warns rather than breaks the build. SonarCloud scans src/ on main and on pull requests. Releases start from GitHub and end with a maintainer's 2FA approval on npm: docs/releasing.md. What the plugin API does that the plugin had to work around: docs/plugin-api-notes.md.

Contributing

Questions and bug reports go to the issues. Pull requests are welcome: a change in behaviour comes with a unit test, an e2e/run.sh scenario and its line in this README or docs/reference.md, and CI must be green.

License

MIT, see LICENSE.

Keywords

opencode

FAQs

Package last updated on 11 Oct 2026

Related posts