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-pluginplugin. This is the execd-side, honest-operator placement: maximum coverage of native calls, emitted to the daemon for signing.
Trust model — read this first
Section titled “Trust model — read this first”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.
How the plugin works
Section titled “How the plugin works”OpenCode exposes a plugin API (@opencode-ai/plugin) with tool.execute.before/tool.execute.after hooks. The plugin:
- Records each call’s intent/params in
tool.execute.before. - On
tool.execute.after, builds one event — channelopencode, the tool name, the resolved action type, the input args, the output, anddecision: "allowed"— and emits it to the daemon over a Unix socket. - Releases the per-session socket on
session.deleted(and on plugin teardown). It deliberately does not release onsession.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.
Action mapping
Section titled “Action mapping”The plugin resolves each OpenCode tool name to an action taxonomy type and forwards it to the daemon as action_type:
| OpenCode tool | Action type | Risk |
|---|---|---|
bash | system.command.execute | high |
edit, write, patch | filesystem.file.modify | medium |
read, glob, grep, list | filesystem.file.read | low |
webfetch | data.api.read | low |
| (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).
Chain mapping
Section titled “Chain mapping”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.
Difference from mcp-proxy
Section titled “Difference from mcp-proxy”| mcp-proxy (Tier A) | opencode-plugin (Tier B) | |
|---|---|---|
| Covers | MCP tool calls | Native host tool calls |
| Placement | Out-of-process | Inside OpenCode |
| Adversary-resistant | Yes | No — honest-operator |
| Signing | In-proxy Ed25519 | Daemon signs (emit only) |
| Policy enforcement | Yes — pass, flag, pause, block | No — emits after the tool ran |
Failure posture
Section titled “Failure posture”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.