Assisted-by: Codex
4.1 KiB
Host Dispatch to Container Agents
Question
Can host Claude decide which bot-bottle container to spin up for a task, while guaranteeing the work executes in the container and not on the host?
Claude Code Agent Mechanisms
Claude Code provides two mechanisms for defining reusable agent behavior:
Skills (.claude/skills/<name>/SKILL.md) run inline in the main conversation context. They're reusable workflows invoked via /skill-name, with optional tool pre-approval.
Subagents (.claude/agents/<name>.md) run in an isolated context window with a custom system prompt and a declared tool allowlist. They're invoked by natural language, @agent-name, or claude --agent. The tools: frontmatter is enforced — the subagent cannot call tools not in the list. (See Claude Code subagents docs, "Choose the subagent scope" and "Write subagent files" sections.)
"Isolated context window" means only conversational isolation (fresh LLM state, summarized output). It is not process, filesystem, or network isolation. Subagents still run on the host with full user permissions.
The Reliability Problem
The previous approach used an MCP server to bridge host Claude and bot-bottle containers. It failed because host Claude had both work-capable tools (Edit, Write, Bash) and MCP dispatch tools. Claude could choose to do the work itself rather than dispatch, with no enforcement mechanism to prevent it.
Why Tool Restriction Solves It
Claude Code's subagent tools: allowlist is architecturally enforced — not a prompt-level suggestion. If the host subagent is defined with only container-dispatch tools and no Edit/Write/Bash, it is incapable of doing implementation work. Dispatch becomes the only available path.
Reliable Dispatch Architecture
Three pieces in combination give a 100% guarantee:
-
Restricted host subagent — a
.claude/agents/bot-bottle-dispatch.mdwithtools:limited to MCP container tools and git-read operations. No Edit, Write, or arbitrary Bash. -
MCP server — exposes tools the restricted host can call:
list_agents()— available agents from the manifest (host Claude decides which to use)run_agent(agent_name, task)— starts a container non-interactively, returns a job IDget_status(job_id)— check running/doneget_output(job_id)— read results
-
Non-interactive container run mode —
cli.py run <agent> "<task>"passes the task toclaude --printinside the container and captures output. Currentlycli.py startis interactive only; this mode does not yet exist.
Proposal
Build host-dispatch-to-container in two deliverables:
Deliverable 1: Non-interactive run mode for bot-bottle
Extend cli.py with a run <agent> <task> subcommand. Starts the container, writes the task prompt to a file inside it (same docker cp pattern used for --append-system-prompt-file), invokes claude --print with the prompt, streams stdout back to the host, and exits when Claude finishes. Results committed and pushed from inside the container as usual.
Deliverable 2: MCP server wrapping bot-bottle
A minimal MCP server (bash or node) exposing list_agents, run_agent, get_status, get_output. Registered in the host Claude Code settings so a restricted dispatch subagent can call it.
The combination enforces the container boundary at the tool layer, not the prompt layer — making it structurally impossible for host Claude to do implementation work itself.
Critical: the tool restriction only applies within the dispatch agent's context. A normal Claude session has its full toolset and may never invoke the dispatch agent regardless of its description. The dispatch agent must be the entry point for the session, not an optional subagent a full-tool host might call. Two ways to enforce this:
- Launch with
claude --agent bot-bottle-dispatch— makes the dispatch agent the primary agent for the session. - Set
agent: bot-bottle-dispatchin the project.claude/settings.json— same effect automatically for anyclaudeinvocation in that directory.
Without one of these, the guarantee does not hold.