Part of the OpenCode enhancement-parity track (see #113 for the baseline + progressive-enhancement model). This is the last missing enhancement column for OpenCode and the largest of the track.
OpenCode cannot arm the write guard today: run_capabilities().supports_guard is false, auto-arm stays off, and an explicit --guard warns and continues unguarded.
Verified hook surface (opencode plugins doc, v1.18.3)
- Project plugins at
.opencode/plugins/*.js (and global at ~/.config/opencode/plugins/) are loaded automatically at startup — no trust prompt is documented.
- The
tool.execute.before hook receives (input, output) with input.tool (tool id) and output.args (tool parameters), and throwing an Error blocks the tool call (the documented .env-protection example throws to deny reads).
- Tool ids verified from source:
bash (input {command}), edit/write (input {filePath, ...}), apply_patch (input {patchText}), read/glob/grep, skill (input {name}).
Proposed design
A new guard engine variant alongside the existing JSON-hooks engine (which stays the claude/codex path), selected by [guard] engine = "opencode-plugin":
- Install: stage an embedded JS plugin template at
.opencode/plugins/slow-powers-eval-guard.js with {exe}/{marker} substituted; marker + manifest via the shared src/sandbox/ machinery; teardown removes the plugin file (extend the guard_hook_cleanup_dir path).
- Plugin: forward every tool call as
{"tool_name": input.tool, "tool_input": output.args} on stdin to eval-magic guard-hook --harness opencode <marker>; empty stdout → allow; non-empty stdout → parse the verdict JSON and throw new Error(reason). Keep the JS dumb — the shared arbiter (src/sandbox/decide.rs) stays the single classification point.
- Verdict: unchanged — the generic
guard-hook entry point and guard_verdict render the descriptor's verdict_template (codex-style {"decision":"block","reason":"{reason}"} works); only the install side is new.
- Descriptor/validation:
[guard] gains an engine discriminator (default "json-hooks"); hook_entry/matcher are required only for the JSON-hooks engine, plugin-file fields for the plugin engine; [run] supports_guard = true lockstep as today; user-supplied descriptors remain barred from declaring [guard].
- Arbiter coverage (already landed with transcript ingest):
[tools] vocabulary for opencode, path_arg covers filePath, apply_patch_paths covers patchText.
Verification checklist (don't-guess guardrail — record evidence in docs/opencode-notes.md)
- A staged project plugin auto-loads under
opencode run headless with no trust prompt.
tool.execute.before fires under --auto (i.e., after permission auto-approval).
- A thrown Error aborts the tool call and surfaces in the events stream as a tool error.
- Armed guard blocks an out-of-bounds
write/bash and allows in-bounds writes; teardown-guard removes the plugin cleanly.
Rejected alternative
OPENCODE_PERMISSION env JSON with external_directory/edit/bash patterns: glob-matching is weaker than the arbiter's lexical normalization, bash path parsing is heuristic-only, and an env var is easy to drop from a hand-driven runbook — the plugin auto-loads by directory convention, exactly like the claude/codex hook files.
Acceptance
Depends on the [tools] vocabulary declared with transcript ingest (arbiter classification + descriptor validation).
Part of the OpenCode enhancement-parity track (see #113 for the baseline + progressive-enhancement model). This is the last missing enhancement column for OpenCode and the largest of the track.
OpenCode cannot arm the write guard today:
run_capabilities().supports_guardis false, auto-arm stays off, and an explicit--guardwarns and continues unguarded.Verified hook surface (opencode plugins doc, v1.18.3)
.opencode/plugins/*.js(and global at~/.config/opencode/plugins/) are loaded automatically at startup — no trust prompt is documented.tool.execute.beforehook receives(input, output)withinput.tool(tool id) andoutput.args(tool parameters), and throwing an Error blocks the tool call (the documented.env-protection example throws to deny reads).bash(input{command}),edit/write(input{filePath, ...}),apply_patch(input{patchText}),read/glob/grep,skill(input{name}).Proposed design
A new guard engine variant alongside the existing JSON-hooks engine (which stays the claude/codex path), selected by
[guard] engine = "opencode-plugin":.opencode/plugins/slow-powers-eval-guard.jswith{exe}/{marker}substituted; marker + manifest via the sharedsrc/sandbox/machinery; teardown removes the plugin file (extend theguard_hook_cleanup_dirpath).{"tool_name": input.tool, "tool_input": output.args}on stdin toeval-magic guard-hook --harness opencode <marker>; empty stdout → allow; non-empty stdout → parse the verdict JSON andthrow new Error(reason). Keep the JS dumb — the shared arbiter (src/sandbox/decide.rs) stays the single classification point.guard-hookentry point andguard_verdictrender the descriptor'sverdict_template(codex-style{"decision":"block","reason":"{reason}"}works); only the install side is new.[guard]gains anenginediscriminator (default"json-hooks");hook_entry/matcherare required only for the JSON-hooks engine, plugin-file fields for the plugin engine;[run] supports_guard = truelockstep as today; user-supplied descriptors remain barred from declaring[guard].[tools]vocabulary for opencode,path_argcoversfilePath,apply_patch_pathscoverspatchText.Verification checklist (don't-guess guardrail — record evidence in
docs/opencode-notes.md)opencode runheadless with no trust prompt.tool.execute.beforefires under--auto(i.e., after permission auto-approval).write/bashand allows in-bounds writes;teardown-guardremoves the plugin cleanly.Rejected alternative
OPENCODE_PERMISSIONenv JSON withexternal_directory/edit/bashpatterns: glob-matching is weaker than the arbiter's lexical normalization, bash path parsing is heuristic-only, and an env var is easy to drop from a hand-driven runbook — the plugin auto-loads by directory convention, exactly like the claude/codex hook files.Acceptance
run --harness opencodeauto-arms the guard on staged runs (with the armed banner);--no-guardopts out; explicit--guardworksopencode run; allows in-boundsguard-hookstdin); golden blessing for the armed bannerdocs/opencode-notes.md+ README support table (Write guard ✅)cargo test,cargo clippy --all-targets -- -D warnings,cargo fmt --checkgreenDepends on the
[tools]vocabulary declared with transcript ingest (arbiter classification + descriptor validation).