Skip to content

Agent boxes

Hand a task to an isolated box and let a real coding agent — Claude Code — do it: reason, write files, run commands, report back. The agent runs on your credential, delivered only through the encrypted secrets store — no operator key, and nothing in a log.

Setup — give hopbox your agent key, once

Use a Claude Code token (claude setup-token) or an Anthropic API key, stored as a secret so your agent boxes can read it:

sh
claude setup-token                                       # prints an sk-ant-oat01-… token
printf %s "$TOKEN" | ssh cli@box.hopbox.dev secret set CLAUDE_CODE_OAUTH_TOKEN
# …or scope it to one project:  secret set -w proj CLAUDE_CODE_OAUTH_TOKEN

Either CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY works.

Run an agent

Spawn a box on the claude-code image (Claude Code + a full toolchain baked in — git, build tools, python3, a current-LTS node, ripgrep, … — see the image catalog) and run the agent:

sh
ssh fixer:claude-code@box.hopbox.dev box-guest agent \
  "add retry logic to api.py and run the tests"

box-guest agent "<task>" runs the agent headless on your task, streams its work live, exits with its status, and reports workingdone (visible in ssh cli@host ps). Add --out /wrk/result.txt to also drop the final result on your shared drive. Options:

  • --tools "Bash Edit Write Read Glob Grep" — override the allowed tool set (also HOPBOX_AGENT_TOOLS).
  • No credential set? You get a clear pointer: "no agent credential — set one with ssh cli@host secret set CLAUDE_CODE_OAUTH_TOKEN".

The claude-code image is the biggest in the catalog, but it is built by a plain hopbox-host catalog build like every other one — it used to be on-demand only, which quietly left agent boxes on a stale box-guest while the rest of the catalog moved on. Build just it with hopbox-host catalog build claude-code. :claude is an alias for it. Future agent images (codex, …) are siblings and use the same box-guest agent command.

When an agent needs a decision — ask

An agent shouldn't guess at a fork. From inside a box it can ask you a structured question and block until you answer:

sh
box-guest ask "Which datastore should I wire up?" --options "PostgreSQL,MySQL"
# blocks, prints your choice, and the agent continues

The question lands in your queue — ssh cli@host asks (or the console), and hopbox://asks on the AI-control plane — and you answer:

sh
ssh cli@host asks                      # your pending questions
ssh cli@host answer <id> PostgreSQL    # …unblocks the agent with your choice

ask is the structured cousin of the canvas (surface_render): a question + options, rendered as a trusted form, request/response. Use ask for decisions and approvals; use a canvas for rich custom UI. Options are optional — without them it's a free-text question (--text); --approve is a yes/no gate.

You don't have to prompt the agent to use it. Every agent is given a short hopbox brief on startup that teaches it the guest tools (ask, status, /wrk), so a plain task like "ask me which datastore, then wire it up" fires a real ask on its own.

Look at the web — browse

An agent can open a web page in its box, show you a screenshot, and read the page back to itself to reason over:

sh
box-guest browse https://example.com            # screenshot -> your screen; text -> the agent
box-guest browse https://example.com --full     # full-page shot

It appears on your screen (a browser surface / lilypad's "AI's screen") and the page's title + text go to the agent. Headless Chromium installs itself on first use (cached in the box's home). The brief teaches it, so a prompt like "check the changelog and tell me what's new" just works.

Show you things — surface

An agent can render an HTML view for you — a chart, a report, a preview — and hand you a URL:

sh
echo "<h1>build report</h1>…" | box-guest surface report
box-guest surface chart "<div>…</div>"      # or inline

It appears on the owner's surfaces (the console/lilypad "AI's screen"). The agent brief teaches this, so a session agent shows you things without being told how.

Talk to it — sessions

A one-shot agent takes a task and finishes. A session is a conversation: a persistent Claude Code instance living in the box — it survives your client, your laptop lid, and even the box being suspended.

Over the AI-control plane:

session_start {box, prompt?, model?, effort?}   → {session}
session_send  {session, text}                   → your next turn
hopbox://session/<id>/events?from=<seq>   → the streamed conversation (replayable)
session_list · session_stop

Events are Claude Code's raw stream-json — every assistant message and tool call, with sequence numbers: disconnect, come back, read from your last cursor, and the whole conversation replays. Both sides are recorded: your turns go into the buffer as you send them (and so does the prompt that opened the session), so a client reattaching from anywhere replays the conversation, not just the agent's half of it. If a turn ends with the agent asking you something while you're away, the box flips to needs-you in the fleet — answer with session_send from any device. One active session per box; it runs on your stored token, like every agent.

Model and effort

model and effort are chosen when the session starts and apply to every turn — each turn re-invokes Claude Code, and a conversation that silently changed model part-way through would be a worse surprise than not being able to change it. Start a new session to change either.

FieldValues
modelfable, opus, sonnet — or a full name like claude-fable-5. Omit for the box's default.
effortlow, medium, high, xhigh, max. Omit for the default.
session_start {box:"web", prompt:"why is the retry loop flaky?", model:"opus", effort:"high"}

They pass straight through to the CLI in the box, which is the authority on what it accepts — a value it rejects comes back as the CLI's own error rather than being silently dropped. In the box the same thing is box-guest session start --model opus --effort high "…".

Lifecycle — fire-and-forget, or keep

A delegated agent is fire-and-forget: it runs, its task + result are recorded, and when it finishes the box is archived — its disk (the box's home + VM) is freed, but the record stays, so you still see what it did in ps / the console. An archived box can't be resumed (resume says so); rm clears the record. This keeps a busy fleet from piling up idle boxes.

To keep a working agent around — resumable, disk intact — pass keep:

box_delegate {task, agent:true, keep:true}     # persistent + resumable

Durable output should go to /wrk or be returned as the result — not left in the box's home, which archiving reclaims. (Boxes you make yourself with ssh name@host are unaffected — they stay persistent.)

Over the AI-control plane

box_delegate {task, agent:true} and fleet_apply (with agent:true per box) let an orchestrating AI fan tasks out to agent boxes and collect results from hopbox://fleet — one AI driving a fleet of worker agents. (This path runs the same box-guest agent under the hood; it needs the plane to hold an agent credential — see the AI-control plane.)

Instant isolated compute — for humans and AIs