Skip to content

OpenCode Overview

OpenCode is a model-agnostic coding agent. Agent Receipts captures its activity across two channels, mirroring the Claude Code setup:

  • Tier A — MCP tools go through mcp-proxy. This is the ingress-egress, adversary-resistant placement: the proxy runs out-of-process and signs locally, so it holds even against a misbehaving agent. Docs-only — it reuses a shipped component.
  • Tier B — native tools (bash, edit, write, webfetch, …) go through the @obsigna/opencode-plugin plugin. This is the execd-side, honest-operator placement: maximum coverage of native calls, emitted to the daemon for signing.

OpenCode process

tool.execute.after

stdio / JSON-RPC

Unix socket (emit only)

Unix socket

Native tools — bash, edit, write…

opencode-plugin

MCP tools — GitHub, Atlassian…

mcp-proxy

obsigna-daemon

receipts.db

The plugin runs inside the OpenCode process, so it is an emitter only. It forwards each tool call to obsigna-daemon, which holds the key, canonicalises (RFC 8785), signs (Ed25519), and chains the receipt (ADR-0010). The plugin never signs and never holds a key.

This placement is honest-operator-grade, not adversary-resistant. The agent controls the plugin, so a compromised OpenCode can omit or misreport a native tool call. If you need a placement that holds against the agent itself, route those tools through MCP and the proxy (Tier A). The two tiers are complementary: Tier A is adversary-resistant for the MCP channel; Tier B maximises coverage of the native channel.

OpenCode exposes a plugin API (@opencode-ai/plugin) with tool.execute.before/tool.execute.after hooks. The plugin:

  1. Records each call’s intent/params in tool.execute.before.
  2. On tool.execute.after, builds one event — channel opencode, the tool name, the resolved action type, the input args, the output, and decision: "allowed" — and emits it to the daemon over a Unix socket.
  3. Releases the per-session socket on session.deleted (and on plugin teardown). It deliberately does not release on session.idle, which OpenCode fires after every turn rather than at session end.

The daemon hashes the input args (parameters_hash, RFC 8785) — cleartext parameters are never stored.

The plugin resolves each OpenCode tool name to an action taxonomy type and forwards it to the daemon as action_type:

OpenCode toolAction typeRisk
bashsystem.command.executehigh
edit, write, patchfilesystem.file.modifymedium
read, glob, grep, listfilesystem.file.readlow
webfetchdata.api.readlow
(unmapped)opencode.<tool> (daemon fallback)medium

The daemon re-derives risk_level from the type, so a mislabelled call cannot downgrade its own risk to evade parameter disclosure. Override or extend the map via config (see Installation).

Each OpenCode sessionID gets its own emitter, so every receipt carries the originating session id and all of a session’s calls group together in the daemon chain.

mcp-proxy (Tier A)opencode-plugin (Tier B)
CoversMCP tool callsNative host tool calls
PlacementOut-of-processInside OpenCode
Adversary-resistantYesNo — honest-operator
SigningIn-proxy Ed25519Daemon signs (emit only)
Policy enforcementYes — pass, flag, pause, blockNo — emits after the tool ran

Default is catch-and-warn (ADR-0025): a tool call is never aborted because the daemon is unreachable — the drop is logged loudly. Best-effort emission means a daemon outage produces a chain gap, not a completeness guarantee. Set AGENT_RECEIPTS_STRICT=1 to surface emit failures instead. See Installation.