Skip to content

Add opencode host-protection hooks (devcontainer-guard plugin) #23

@aniongithub

Description

@aniongithub

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.before abort contract — does throw cleanly cancel the tool call, or do we need to mutate output.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.sh installs an opencode plugin that blocks host-contaminating bash commands using the same devcontainer-guard.sh logic as Claude Code / Copilot CLI
  • install.ps1 mirrors 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 foo from outside the dev container)
  • README updated to note opencode now has the same host-protection guarantees as the other supported clients

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions