OpenCode Plugin

August 17, 2026 · View on GitHub

Capability label: Observe.

The plugin records normalized tool-call evidence and repeated-work recommendations in a local Decision Ledger. It never blocks a tool call, never rewrites tool arguments, and never throws out of a hook. Shadow Mode is the only mode this integration supports today.

Validated against OpenCode 1.18.18 on Linux.

Also validated against PrivacyCode 1.18.10, an OpenCode-compatible fork. See Compatible forks.

Install

marginal install opencode

That copies one file to ~/.config/opencode/plugins/marginal.js. Remove it with:

marginal uninstall opencode

Uninstall only deletes a file carrying MARGINAL's own marker, so a hand-written plugin with the same name is never destroyed. For the same reason, install refuses to overwrite one.

The plugin needs the marginal-ai package importable by the python3 on your PATH:

python3 -m pip install --user marginal-ai
VariableEffect
MARGINAL_PYTHONinterpreter used to run the bridge
MARGINAL_RUNTIMEdirectory or zipapp prepended to the bridge's PYTHONPATH
MARGINAL_DATA_ROOTwhere the Decision Ledger is written
MARGINAL_PRIVACY_PROFILEsafe_telemetry or aggregate_export instead of local-only
MARGINAL_TIMEOUT_MSper-request bridge timeout, default 5000

If the bridge cannot start, the plugin registers no hooks at all and OpenCode behaves exactly as if it were not installed.

Architecture

OpenCode loads plugins into its own long-running process, so the plugin owns one bridge child process and speaks newline-delimited JSON to it over pipes. There is no socket, no port, and no token, because the only possible caller is the process that spawned it. One bridge governs every session in that OpenCode process; tool calls from different sessions interleave and stay separate.

Plugin hookBridge operation
eventsession.createdsession_start
tool.execute.beforetool_start
tool.execute.aftertool_end
eventsession.deleted, disposesession_end

OpenCode has no guaranteed session-start hook in every mode, so the first observed tool call opens the session.

What it can prove

OpenCode's shell tool reports an exit code in result metadata, which is a provable success or failure signal. Most other tools report nothing that separates success from a completed failure, and those outcomes are recorded as unknown rather than assumed successful.

Two further limits:

  • a tool that throws produces no tool.execute.after call, so its proposal is settled as unobservable when the session closes;
  • per-tool token usage is not exposed, so token cost is reported as unavailable rather than as zero.

This is a weaker evidence surface than an engine that reports success and failure as separate events. It is enough to detect repeated work and to measure governance overhead; it is not enough to justify enforcement, and none is attempted.

Privacy

Tool output never leaves the OpenCode process. The plugin sends a SHA-256 digest of the result plus an allowlist of outcome signals (exit, exit_code, exitCode, success, status), so file content and command output never reach MARGINAL, not even in memory.

Tool arguments do reach the bridge, because semantic identity and verification detection depend on them. They are hashed into a semantic key and never persisted. The ledger holds digests, generic action kinds, and tool names.

LOCAL_FULL is the default profile because the ledger stays on the user's machine. Set MARGINAL_PRIVACY_PROFILE=safe_telemetry for keyed pseudonyms. Pseudonymization is not anonymization. OpenCode's own logs, database, and telemetry are outside this boundary.

Reading the evidence

marginal ledger-report ~/.local/share/marginal/opencode/ledger/*/*.jsonl

Compatible forks

OpenCode's plugin loader is reused unchanged by compatible forks, so the same plugin and the same bridge protocol govern them. A target records only what differs: the executable name, the global configuration directory, and the ledger location.

TargetExecutablePlugin pathLedger root
opencodeopencode~/.config/opencode/plugins/marginal.js~/.local/share/marginal/opencode
privacycodeprivacycode~/.config/privacycode/plugins/marginal.js~/.local/share/marginal/privacycode
marginal install privacycode
marginal uninstall privacycode

Each target keeps a distinct engine label, so one ledger never conflates two engines and a later measurement can compare them instead of pooling them. Installing binds the copied plugin to its engine, so both can be installed at once without interfering. MARGINAL_PRIVACYCODE_DATA overrides the ledger location for that target alone.

Adding another compatible fork means one OpenCodeTarget entry. Nothing about the governance contract is per-target, so a fork that has diverged in its plugin API is not a target — it is a new adapter.