Summary
The sandbox currently runs OpenCode as an interactive TUI for a single human user. This issue proposes extending it to support on-demand invocation as a sub-agent from host-side agents like Claude Code, Open WebUI, or Codex CLI — without changing the existing security model, Dockerfile, or interactive mode.
A host agent delegates a task via tool call → Docker starts the sandbox ephemerally → OpenCode executes the task headlessly against the local vLLM server → structured JSON is returned → container stops automatically.
No data leaves your network. No cloud API keys required. No changes to the hardened security setup.
Why
| Problem |
Today |
After |
| A host agent (Claude Code etc.) can delegate tasks to the sandbox |
❌ |
✅ |
| Headless / non-interactive execution |
❌ |
✅ |
| Structured JSON output for machine-readable results |
❌ |
✅ |
| Ephemeral start only when needed |
❌ |
✅ |
Benefits:
- Data sovereignty — even delegated sub-agent tasks stay fully local
- Security isolation — Docker enforces kernel-level separation between host agent and sub-agent; a misbehaving prompt cannot escape to the host
- Cost efficiency — all inference runs against local vLLM, no token costs even at scale
- Reuse — existing commands (
/refactor-audit, /git-commit etc.) become available to all calling agents with zero extra work
What needs to change
Only 3 files need to be touched. The Dockerfile, compose.yml, and the entire security model remain completely unchanged.
1. entrypoint.sh — add a TASK branch at the end
Replace the final exec gosu ... line with:
export HOME="/home/opencode/workspace"
export OPENCODE_WORKSPACE="/home/opencode/workspace"
if [[ -n "${TASK}" ]]; then
echo "> [Entrypoint] Sub-Agent headless mode" >&2
exec gosu "$APP_USER:$APP_GROUP" \
opencode run "${TASK}" \
--format json \
--quiet
else
exec gosu "$APP_USER:$APP_GROUP" opencode
fi
Everything before this (PUID/PGID handling, chown, gosu setup) stays identical.
2. New file: compose.headless.yml
services:
opencode:
build: .
environment:
TASK: ${TASK}
PUID: ${PUID:-1000}
PGID: ${PGID:-1000}
volumes:
- ./workspace:/home/opencode/workspace
- ./data:/home/opencode/.local/share/opencode
- ./config/opencode.json:/home/opencode/.config/opencode/opencode.json:ro
- ./config/auth.json:/home/opencode/.config/opencode/auth.json:ro
- ./config/AGENTS.md:/home/opencode/.config/opencode/AGENTS.md:ro
- ./.opencode/commands:/home/opencode/.config/opencode/commands:ro
- ./.opencode/skills:/home/opencode/.config/opencode/skills:ro
cap_drop: [ALL]
cap_add: [CHOWN, SETUID, SETGID, DAC_OVERRIDE]
security_opt: [no-new-privileges:true]
stdin_open: false
tty: false
restart: "no"
3. New file: tools/mcp_sandbox_server.py (for Claude Code)
import asyncio, os, subprocess
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
SANDBOX_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
app = Server("coding-agent-sandbox")
@app.list_tools()
async def list_tools():
return [Tool(
name="run_in_sandbox",
description=(
"Runs a coding task in the hardened Docker sandbox "
"against local vLLM. Returns JSON with agent output."
),
inputSchema={
"type": "object",
"properties": {
"task": {"type": "string"},
"timeout": {"type": "integer", "default": 300}
},
"required": ["task"]
}
)]
@app.call_tool()
async def call_tool(name: str, arguments: dict):
result = subprocess.run(
["docker", "compose", "-f", "compose.headless.yml", "run", "--rm", "opencode"],
env={**os.environ, "TASK": arguments["task"]},
capture_output=True, text=True,
timeout=arguments.get("timeout", 300),
cwd=SANDBOX_DIR
)
output = result.stdout.strip()
if result.returncode != 0:
output = f'{{"error": "exit {result.returncode}", "stderr": {repr(result.stderr)}}}'
return [TextContent(type="text", text=output)]
if __name__ == "__main__":
asyncio.run(stdio_server(app))
Register with Claude Code:
claude mcp add coding-agent-sandbox -- python3 ./tools/mcp_sandbox_server.py
Smoke test
TASK="List all files in the workspace and summarize their purpose." \
docker compose -f compose.headless.yml run --rm opencode
Notes
opencode run --format json --quiet is the official non-interactive mode — no hacking required
- For high-frequency sub-agent calls,
opencode serve --port 4096 + --attach eliminates per-task container start overhead
- The Open WebUI variant is ~20 lines of Python using
subprocess.run — no MCP dependency needed
Summary
The sandbox currently runs OpenCode as an interactive TUI for a single human user. This issue proposes extending it to support on-demand invocation as a sub-agent from host-side agents like Claude Code, Open WebUI, or Codex CLI — without changing the existing security model, Dockerfile, or interactive mode.
A host agent delegates a task via tool call → Docker starts the sandbox ephemerally → OpenCode executes the task headlessly against the local vLLM server → structured JSON is returned → container stops automatically.
No data leaves your network. No cloud API keys required. No changes to the hardened security setup.
Why
Benefits:
/refactor-audit,/git-commitetc.) become available to all calling agents with zero extra workWhat needs to change
Only 3 files need to be touched. The Dockerfile,
compose.yml, and the entire security model remain completely unchanged.1.
entrypoint.sh— add aTASKbranch at the endReplace the final
exec gosu ...line with:Everything before this (PUID/PGID handling, chown, gosu setup) stays identical.
2. New file:
compose.headless.yml3. New file:
tools/mcp_sandbox_server.py(for Claude Code)Register with Claude Code:
Smoke test
TASK="List all files in the workspace and summarize their purpose." \ docker compose -f compose.headless.yml run --rm opencodeNotes
opencode run --format json --quietis the official non-interactive mode — no hacking requiredopencode serve --port 4096+--attacheliminates per-task container start overheadsubprocess.run— no MCP dependency needed