Repository navigation
Description
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
- Confirm the exact
tool.execute.beforeabort contract — doesthrowcleanly cancel the tool call, or do we need to mutateoutput.args? - Does opencode's WSL story make spawning a Linux-side bash hook from a Windows-installed opencode workable? May need to scope this issue to Linux/macOS only initially.
- For the skill loader: is there a programmatic way to inject system-prompt content from a plugin, or does opencode's native SKILL.md discovery (which already picks up
~/.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 CLI -
install.ps1mirrors the install on Windows where feasible (may need to defer if WSL spawning proves impractical) - Manual test inside the project devcontainer: opencode refuses to run a host-poisoning command (e.g.
apt install foofrom outside the dev container) - README updated to note opencode now has the same host-protection guarantees as the other supported clients
Related
- PR Add opencode skill + MCP server installation to installers #22 — adds the skill + MCP config (this issue is the natural follow-up)
Activity