Skip to content

The AI-control plane

hopbox ships an MCP control plane: the same box fleet a human reaches over SSH, an AI drives over the Model Context Protocol. It is event-driven (subscribe and react to pushed changes; never poll) and shares the daemon's engine — so an AI spawns real boxes, not a simulation.

Enable it on the daemon with --mcp-addr (the installer defaults it on, at unix:/run/hopboxd-mcp.sock).

Resources (subscribe, then react to notifications/resources/updated)

URIWhat
hopbox://fleetEvery box with its live phase and the agent's self-reported status.
hopbox://surface/<name>/eventsA rendered surface's interaction events (the canvas loop).
hopbox://imagesThe box image catalog with a per-image skill summary — pick one for box_delegate / fleet_apply.
hopbox://guideThis plane's own how-to, also delivered as the MCP instructions.

Tools

A box ref below is a box id from hopbox://fleet.

Fan work out

ToolWhat
box_delegate {task, agent?, keep?, ttl?}Spawn a box, run a task in the background; the result surfaces on hopbox://fleet. agent:true runs a Claude Code agent (on your token). Fire-and-forget by default — the box is archived (disk freed, record kept) when done; keep:true leaves it persistent + resumable. ttl:"30m" sets a hard deadline (see below).
fleet_apply {boxes:[{key,image,task,ttl?}]}Declare a desired set of task-boxes; hopbox converges to it, idempotent per key. Prefer over many box_delegate calls.
box_spawn {name?, ttl?} / fleet_getSpawn an empty box / snapshot the fleet.

Bound work you aren't watching

ttl is a deadline from creation, enforced by the daemon. The box is torn down when it expires whatever it is doing — it beats an attached session, a keep-alive pin and the durable flag, so an agent that loops or a build that wedges stops costing you at a time you chose. It also survives the orchestrator that started the box crashing, which a timeout held in your own process cannot.

Set it on any delegation you won't be watching. Omit it and your tier decides: a fire-and-forget delegation gets its tier's default, and anything you ask for is clamped to the tier ceiling.

Drive a box (everything a human does over SSH, over MCP)

ToolWhat
box_exec {box, cmd, timeout_s?}Run a command and get the output back in the call — stdout/stderr/exit, synchronously. Auto-resumes a suspended box. The workhorse.
box_write {box, path, content} · box_read {box, path}Stage a file into a box / read one out (bytes-safe).
box_suspend · box_resume · box_rm {box}Lifecycle. An AI cleans up and checkpoints its own fleet.
session_start {box, prompt?, model?, effort?} / session_send / session_list / session_stopA persistent, conversational agent session in a box; events stream on hopbox://session/<id>/events?from=<seq> (cursor-replayable — reattach from anywhere, and both sides of the conversation are in the buffer). model / effort are fixed for the session — see model and effort.

Share state across boxes — the shared workspace as the AI's hand-off drive

ToolWhat
wrk_write {path, content, ws?} · wrk_read {path, ws?} · wrk_list {path?, ws?}The plane's /wrk: box A writes a shard, the orchestrator reads it, box B picks it up. Pass ws to target a specific workspace's drive (omit = default). The same drive the plane's boxes in that workspace mount at /wrk.

Talk to a human

ToolWhat
surface_render {name, html}Render an interactive canvas at a URL — the canvas loop, below. Arbitrary HTML, fire-and-forget.
ask_answer {id, choice} + hopbox://asksAnswer an ask — a structured question an (often in-box) agent raised and is blocked on. hopbox://asks lists the pending ones; answering unblocks the agent. The request/response, form-shaped cousin of surface_render.

An AI can drive a box entirely without a human: box_exec to run, box_read / wrk_read to collect, surface_render to report. Everything is owner-scoped — the plane drives only its own boxes, never a human's. On connect, the server sends an instructions guide teaching all of the above.

The canvas loop

When an AI needs a human's decision, input, or attention, it isn't limited to chat — it can render an interactive UI and watch the human use it, live:

  1. surface_render {name:"approve", html:"<button id=ok>Approve</button>"} returns a URL. The AI gives it to the human.
  2. The AI subscribes to hopbox://surface/approve/events.
  3. Each click/input is pushed to the AI as {kind, target, value} — it reacts: re-render, branch its work, or unblock a waiting task.

Serve surfaces over HTTP with --surface-addr — on the live host they appear at box.hopbox.dev/s/…. The loop is bidirectional: the AI renders, the human acts, the AI observes.

Your own plane — ssh cli@host mcp

The plane is yours, by SSH key — like every other hopbox surface. Point any MCP client at hopbox using ssh as its transport; your key is your login, and the plane binds to your identity.

Claude Code — one command:

sh
claude mcp add hopbox -- ssh -T -o StrictHostKeyChecking=accept-new cli@box.hopbox.dev mcp
claude mcp list        # hopbox … ✔ Connected

Claude Desktop (and any stdio MCP client) — add to its config (claude_desktop_config.jsonmcpServers):

jsonc
{
  "hopbox": {
    "command": "ssh",
    "args": ["-T", "-o", "StrictHostKeyChecking=accept-new", "cli@box.hopbox.dev", "mcp"]
  }
}

The flags matter: -T runs ssh without a pty (the plane is a byte stream), and StrictHostKeyChecking=accept-new accepts the host key on first use instead of hanging on the interactive "continue connecting?" prompt — the #1 reason a GUI MCP client silently never starts.

"Permission denied (publickey)" — esp. Claude Desktop on macOS

Test the transport from a terminal first: ssh cli@box.hopbox.dev ls. If that lists your boxes but the app logs Permission denied (publickey), the app launched sshwithout SSH_AUTH_SOCK — a GUI app doesn't inherit your shell's environment, so ssh can't reach the agent holding your key (and if your key is agent-only, there's no file to fall back to). Recover the agent with a one-line wrapper and point the server's command at it:

sh
# ~/.hopbox/mcp-ssh   (then: chmod +x ~/.hopbox/mcp-ssh)
#!/bin/sh
[ -n "$SSH_AUTH_SOCK" ] || export SSH_AUTH_SOCK="$(launchctl getenv SSH_AUTH_SOCK)"
exec /usr/bin/ssh "$@"
jsonc
{ "hopbox": {
    "command": "/Users/you/.hopbox/mcp-ssh",
    "args": ["-T", "-o", "StrictHostKeyChecking=accept-new", "cli@box.hopbox.dev", "mcp"] } }

If your hopbox identity is instead a key file, skip the wrapper and add "-i", "/path/to/key" to the args (it must be the key that owns your boxes — a different key gets a different, empty fleet). Can't use SSH at all (a browser, a locked-down client)? Use the hbx_ WebSocket transport below.

hopbox://fleet then shows only your boxes; box_spawn/delegate create boxes you own; wrk.* is your /wrk; and rendered surfaces are yours. A different key gets a different, fully isolated plane. Because your boxes are yours, an agent box delegated from here runs on your stored token — box_delegate {task, agent:true} just works, no operator key.

From a browser or a third-party app — anything that can't present an SSH key — connect over a WebSocket with your hbx_ key instead: wss://host/v1/mcp?key=hbx_…. Same per-owner plane, same tools; it is what the web console is built on. Workspaces (ws.*) and secrets (secret.*, values never returned) are on the plane too, so a UI can manage them.

The client — hopbox-mcp

sh
hopbox-mcp --ssh cli@box.hopbox.dev            # your plane over SSH (wraps the above)
hopbox-mcp ps   --connect unix:/run/hopboxd-mcp.sock    # the operator/local plane (root socket)
hopbox-mcp watch --connect <sock> hopbox://surface/<name>/events   # print interactions live
hopbox-mcp --demo                              # self-drive a demo against box.hopbox.dev

The --connect socket is the operator/local plane (its own owner); the ssh cli@host mcp path is the per-customer one.

Humans get the same engine

Everything here has a human twin over SSH — ssh cli@host lists and removes boxes, key-authed, zero-install. One fleet, one engine, two front doors.

Instant isolated compute — for humans and AIs