Connectors

OpenCode

The OpenCode connector wires DefenseClaw into opencode via a dependency-free JavaScript bridge plugin auto-loaded from ~/.config/opencode/plugins/, whose tool.execute.before hook blocks risky tool calls by throwing.

The OpenCode connector wires DefenseClaw into OpenCode. OpenCode has no command-hook config surface: it auto-loads JavaScript/TypeScript plugins from its user plugin directory at startup. DefenseClaw installs a single, dependency-free bridge plugin (defenseclaw.js) there. The plugin's tool.execute.before hook forwards every tool call to the local DefenseClaw gateway and throws whenever the gateway returns a block decision. That documented, awaited thrown-error path is the connector's authoritative enforcement boundary. OpenCode continues to talk directly to its configured upstream model provider; DefenseClaw never inserts an LLM proxy.

No opencode.json edit, no shell shim. Local plugins are loaded automatically, so installing the connector is just writing one file. The bridge uses the runtime's global fetch, so it needs no npm dependencies and never triggers a bun install.

Platform support

PlatformStatusNotes
macOS and LinuxSupportedThe global bridge plugin is supported for the reviewed OpenCode >=1.18.10,<1.18.20 contract.
Native Windows x64SupportedDefenseClaw supports only direct native execution and admits the protected current-user SST.opencode WinGet executable. OpenCode recommends WSL but does not require it; mixed WSL/native paths, other package identities, Desktop sidecars, and ARM64 are outside this connector contract. Official-client validation metadata remains empty with live=false.

Setup

defenseclaw setup opencode                # observe (default) - record only
defenseclaw setup opencode --mode action  # block on policy hits by throwing
defenseclaw setup opencode
defenseclaw setup opencode --mode action

This connector is global/user-scoped only. DefenseClaw writes the bridge to:

  • Windows: %OPENCODE_CONFIG_DIR%\plugins\defenseclaw.js, defaulting to %USERPROFILE%\.config\opencode\plugins\defenseclaw.js
  • macOS/Linux: $OPENCODE_CONFIG_DIR/plugins/defenseclaw.js, defaulting to ~/.config/opencode/plugins/defenseclaw.js

The file carries the connector-scoped gateway token, address, and fail mode. DefenseClaw applies owner-only mode 0600 on Unix and restricts the Windows DACL to the current user, Administrators, and SYSTEM. Those controls prevent access by other standard users; they are not tamper-proof against the owning user or administrators. Doctor detects managed-plugin digest drift, and Setup repair/reconciliation restores the packaged registration. Restart OpenCode after Setup so the running client loads the new plugin.

OpenCode reconciliation publishes the custody receipt before the plugin's atomic replacement, then publishes the contract lock and active runtime state. It reports success only after re-reading the plugin/receipt, scoped token, contract lock, and active connector marker as one current registration. A failure rolls back newly published OpenCode state instead of leaving a plugin for a later setup to repair. This proves registration consistency, not that a client process has loaded it; the post-restart load heartbeat is the separate runtime signal.

OpenCode's Windows guide says that OpenCode can run directly on Windows while recommending WSL for the best experience. WSL is recommended, not required. DefenseClaw's packaged Windows setup admits only the current user's official SST.opencode WinGet package at %LOCALAPPDATA%\Microsoft\WinGet\Packages\SST.opencode_Microsoft.Winget.Source_8wekyb3d8bbwe\opencode.exe. OpenCode, DefenseClaw, the gateway, the JavaScript plugin, and their configuration paths must all remain native Windows processes and paths. Other upstream installation methods do not satisfy this protected admission path; do not substitute WSL, Docker, a VM, Git Bash, Cygwin, or MSYS.

setup opencode is the dedicated OpenCode alias. See the quick-alias reference for its common setup options and full guardrail setup for advanced scanner and judge configuration.

Version contract

The current contract is opencode-hooks-v1 / bridge v7, source-reviewed for >=1.18.10,<1.18.20 with current pin 1.18.19. The 1.18.19 plugin types, loader/config origins, MCP sanitizer, and tool execution call sites are byte-identical to 1.18.11; its reviewed config-parser delta does not alter plugin loading or hook dispatch. A detected OpenCode version outside that bounded range is refused before connector config, plugin, lock, or active-roster publication in both observe and action mode unless the explicit exploratory drift override is set. Unversioned builds retain the general observe-warning / action-fail-closed policy and are not promoted to source-reviewed compatibility. The review pins the official v1.18.19 release, published August 20, 2026, and its plugin/config/MCP source. This is source compatibility evidence only: packaged Setup and authentic official-client Windows validation remain unrecorded and live remains false; supported availability does not alter those evidence fields.

Lifecycle events

EventWhen it firesDefenseClaw roleAction verdict → behavior
tool.execute.beforeRight before a tool (bash, read, edit, …) executesInspect tool args; intercept risky commandsblock → gateway returns {decision: "deny", reason}; the bridge throws new Error(reason), aborting the tool
tool.execute.afterImmediately after a tool returnsCapture the official title, output, and metadata result fields for exact-call outcome attribution. Cannot block or undo the tool — it already ran.Awaited observe-only delivery; transport and fail-mode results are advisory and ignored

OpenCode 1.18.10-v1.18.19 expose permission.ask, chat.params, chat.headers, and experimental chat message/system transformation hooks. DefenseClaw intentionally does not implement or claim those ask/context surfaces in this focused bridge; confirm therefore has no native resumable approval path. Findings on tool.execute.after are telemetry only. OpenCode's generic lifecycle event dispatch is also non-awaited, so DefenseClaw does not claim blocking or reliable delivery for lifecycle events.

OpenCode runs external plugin hooks sequentially. Its global plugin directory is loaded before project plugin directories, so a project plugin can mutate the shared output.args object after DefenseClaw's global pre-tool hook. Bridge v7 uses the official v1.18.10-v1.18.19 plugin_origins order to detect this condition. It still observes the pre-mutation arguments, but in action mode it refuses to return an authoritative allow when any later plugin exists. This is a bounded correction, not a plugin-order redesign.

This is an in-process plugin contract, not a subprocess protocol. There is no hook stdin payload, stdout decision document, or hook exit-code convention: OpenCode passes typed arguments to the plugin, awaits the Promise returned by tool.execute.before, continues on normal return, and blocks when that Promise throws. The bridge's HTTP exchange with the local gateway is internal to the plugin.

The bridge plugin

DefenseClaw writes a single managed plugin file. Its tool.execute.before resolves the gateway verdict before throwing, so a fail-open transport error never turns into an accidental block:

"tool.execute.before": async (input, output) => {
  const verdict = await defenseclawPost("tool.execute.before", input.tool, output.args, cwd);
  if (verdict?.reason) throw new Error(verdict.reason); // policy block
  if (verdict?.mode === "action" && !DC_ARGUMENTS_AUTHORITATIVE) throw new Error("unobservable later mutation");
}

When the gateway is unreachable, the bridge honors the configured fail mode: FAIL_MODE=closed throws and blocks; FAIL_MODE=open allows. This fail-closed claim applies only to the awaited tool.execute.before contract. The bridge also awaits tool.execute.after so its result is attributed before a later call starts, but it ignores that delivery's advisory verdict and transport or fail-mode result. Generic lifecycle delivery is non-awaited. Neither path becomes enforceable when fail mode is closed.

The plugin also emits a secret-free defenseclaw.plugin.loaded heartbeat from its awaited config hook. Doctor reports a warning when the managed file digest is current but the running sidecar has no heartbeat: OpenCode may be stopped or running with --pure/external plugins disabled. File presence alone is not reported as runtime health.

Local surfaces

defenseclaw.js
defenseclaw.js

DefenseClaw resolves the non-enterprise v1.18.10-v1.18.19 config layers in effective order: authenticated remote .well-known provenance (typed unverified; never fetched offline), global config.json/opencode.json/opencode.jsonc, OPENCODE_CONFIG, an explicitly pinned project's JSON/JSONC files and .opencode directory, the user ~/.opencode component directory, OPENCODE_CONFIG_DIR, and finally OPENCODE_CONFIG_CONTENT. JSONC, deep MCP overrides, and enabled:false are preserved; inline provenance is reported by label without disclosing its contents. A relative env path is resolved only when an explicit workspace is available, never against the daemon's unrelated cwd.

defenseclaw mcp list --connector opencode, mcp set, and mcp unset operate on the top-level mcp map. Writes refuse while OPENCODE_CONFIG_CONTENT is active because an inline higher-precedence layer cannot be changed or restored atomically. Windows ProgramData managed config, MDM, Team/Program/cloud dashboard state, and other enterprise layers are excluded from this PR and typed as unverified; local results must not be read as final enterprise precedence. For runtime server-scoped policy, bridge v7 mirrors OpenCode's official server/tool sanitizer, maps the effective enabled MCP server names, and rejects sanitizer collisions or prefix ambiguity instead of guessing.

For local MCP servers, mcp set refuses commands outside trusted install prefixes unless you explicitly trust the directory or force that one write. The managed defenseclaw.js registration is connector configuration, not an operator plugin. DefenseClaw excludes that exact managed file from ordinary plugin inventory, scans, and blocking. Operator and third-party assets remain eligible for their normal inventory and policy flows.

Local discovery follows OpenCode's bounded v1.18.10-v1.18.19 surfaces on Windows, macOS, and Linux:

AssetLocal sources
SkillsSingular/plural skill and skills component directories plus project/user .claude/skills and .agents/skills
PluginsDirect JS/TS files in singular/plural plugin and plugins directories plus the config plugin package list
AgentsMarkdown files in singular/plural agent and agents directories plus the config agent map
InstructionsGlobal and project AGENTS.md, CLAUDE.md fallback, and local bounded paths from config instructions
Tools and commandsDirect JS/TS files in tool/tools, Markdown commands in command/commands, and the config command map

Project discovery starts only from the explicitly pinned workspace and walks up through its nearest .git directory or regular .git file. It never borrows the DefenseClaw daemon's current directory, follows directory aliases, or searches above that repository. The legacy config tools object is a permission map and is deliberately not reported as custom tools. Skill install uses the native <workspace>/.opencode/skills target when a workspace is pinned, otherwise $OPENCODE_CONFIG_DIR/skills (default ~/.config/opencode/skills). Plugin/config assets remain discovery-only; DefenseClaw does not install third-party OpenCode plugins or rewrite those asset declarations.

Hook capabilities

Block events

  • tool.execute.before

Native ask events

None — confirm verdicts are downgraded with the raw action preserved.

Disable

defenseclaw guardrail disable --connector opencode --yes

Teardown removes the managed plugin when it is unchanged since Setup. If the owning user or an administrator changes it, Doctor reports digest drift and Setup repair/reconciliation can restore the managed bytes; teardown preserves an operator-modified file rather than deleting it as though DefenseClaw still owned the exact content. Restart OpenCode after teardown so an already-running process releases the previously loaded plugin.