Background
PR #22 added opencode skill + MCP server installation to both installers. It explicitly does not wire up host-protection hooks for opencode (the equivalent of the PreToolUse / SessionStart integration we already ship for Claude Code and GitHub Copilot CLI).
This issue tracks adding that integration.
Why opencode wasn't covered in PR #22
For Claude Code and Copilot CLI, our installer drops two shell scripts into ~/.local/share/devcontainer-mcp/hooks/:
devcontainer-guard.sh — blocks host-contaminating bash commands (PreToolUse)
devcontainer-skill-loader.sh — injects SKILL.md at session start (SessionStart)
…and points each agent's config file at those scripts via a type: command JSON entry.
opencode does have hooks, but the model is different: it uses a JS/TS plugin system (https://opencode.ai/docs/plugins/) where plugins live in ~/.config/opencode/plugins/ (or are loaded from npm via "plugin": [...] in opencode.json) and subscribe to events programmatically. There is no "point this config field at a shell script" idiom.
Proposed approach
Ship a thin opencode plugin that delegates to our existing shell hooks so we don't fork the security logic:
1. devcontainer-guard.js (or .ts)
Subscribes to the tool.execute.before event. When input.tool === "bash", spawn ~/.local/share/devcontainer-mcp/hooks/devcontainer-guard.sh, pass the command on stdin (matching the Claude Code hook payload contract), and abort the tool call if the script exits non-zero.
// sketch
import { spawnSync } from "child_process"
import { homedir } from "os"
import { join } from "path"
const GUARD = join(homedir(), ".local/share/devcontainer-mcp/hooks/devcontainer-guard.sh")
export const DevcontainerGuard = async (_ctx) => ({
"tool.execute.before": async (input, output) => {
if (input.tool !== "bash") return
const res = spawnSync(GUARD, [], {
input: JSON.stringify({ tool_input: { command: output.args.command } }),
encoding: "utf8",
})
if (res.status !== 0) {
throw new Error(res.stderr || "blocked by devcontainer-guard")
}
},
})
2. devcontainer-skill-loader.js
Subscribes to session.created and injects SKILL.md into the session context. Open question: how exactly to do this through the plugin API — likely via client operations or a tui prompt append. May need to read the SDK surface before settling on the approach.
3. Installer wiring
install.sh: drop the plugin file(s) into ~/.config/opencode/plugins/devcontainer-guard.js (and skill-loader). No config merge needed — opencode auto-loads everything in that directory.
install.ps1: same, but inside WSL since the opencode plugin would have to spawn the WSL-side guard script.
Open questions
Acceptance criteria
Related
Background
PR #22 added opencode skill + MCP server installation to both installers. It explicitly does not wire up host-protection hooks for opencode (the equivalent of the
PreToolUse/SessionStartintegration we already ship for Claude Code and GitHub Copilot CLI).This issue tracks adding that integration.
Why opencode wasn't covered in PR #22
For Claude Code and Copilot CLI, our installer drops two shell scripts into
~/.local/share/devcontainer-mcp/hooks/:devcontainer-guard.sh— blocks host-contaminating bash commands (PreToolUse)devcontainer-skill-loader.sh— injects SKILL.md at session start (SessionStart)…and points each agent's config file at those scripts via a
type: commandJSON entry.opencode does have hooks, but the model is different: it uses a JS/TS plugin system (https://opencode.ai/docs/plugins/) where plugins live in
~/.config/opencode/plugins/(or are loaded from npm via"plugin": [...]inopencode.json) and subscribe to events programmatically. There is no "point this config field at a shell script" idiom.Proposed approach
Ship a thin opencode plugin that delegates to our existing shell hooks so we don't fork the security logic:
1.
devcontainer-guard.js(or.ts)Subscribes to the
tool.execute.beforeevent. Wheninput.tool === "bash", spawn~/.local/share/devcontainer-mcp/hooks/devcontainer-guard.sh, pass the command on stdin (matching the Claude Code hook payload contract), and abort the tool call if the script exits non-zero.2.
devcontainer-skill-loader.jsSubscribes to
session.createdand injects SKILL.md into the session context. Open question: how exactly to do this through the plugin API — likely viaclientoperations or a tui prompt append. May need to read the SDK surface before settling on the approach.3. Installer wiring
install.sh: drop the plugin file(s) into~/.config/opencode/plugins/devcontainer-guard.js(and skill-loader). No config merge needed — opencode auto-loads everything in that directory.install.ps1: same, but inside WSL since the opencode plugin would have to spawn the WSL-side guard script.Open questions
tool.execute.beforeabort contract — doesthrowcleanly cancel the tool call, or do we need to mutateoutput.args?~/.config/opencode/skills/devcontainer-mcp/SKILL.md, installed in PR Add opencode skill + MCP server installation to installers #22) make this hook redundant for opencode?Acceptance criteria
install.shinstalls an opencode plugin that blocks host-contaminating bash commands using the samedevcontainer-guard.shlogic as Claude Code / Copilot CLIinstall.ps1mirrors the install on Windows where feasible (may need to defer if WSL spawning proves impractical)apt install foofrom outside the dev container)Related