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 workspace:  secret set -w ws1 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 working → done (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).
  • --stream-json — print the agent's structured event stream (one JSON object per assistant turn, tool call and tool result) instead of the readable narration. This is what a delegated run uses, so the daemon can keep the run transcript; by hand you want the prose.
  • 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, 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 as a browser surface, 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 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:"box1", 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. 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.)

What it did — the run transcript ​

The result answers what did it conclude. The run transcript answers what did it do: every assistant turn and every tool call — the Bash, Edit, Read, Write it ran, with each tool's result — as the agent emitted them.

hopbox://box/<id>/log            → {events:[{seq,data}], next, truncated}
hopbox://box/<id>/log?from=<seq> → read on from a cursor

data is one line of Claude Code's stream-json — the same events a session streams, so one renderer handles both. Boxes that have a transcript are flagged log: true on hopbox://fleet, so an AI reviewing a fleet knows which runs are worth opening.

Two properties make it worth reading, and both come from where it is written:

  • It outlives the box. hopbox stores it outside the box's home, in a directory the daemon owns — so archiving a done agent box frees the disk and keeps the record of what it did. It goes away only with the box's record: box_rm removes both.
  • The box did not write it. The daemon tees the run's output stream as it arrives; the box only prints. Nothing in the box holds a handle on the file, so a run cannot edit, trim or delete the record of what it did afterwards.

So an unattended agent leaves every tool call and assistant turn, durable and unforgeable — which is the point when nobody was watching it work.

What it is not

A stream-json feed is an agent's output stream: what the harness chose to emit. It is not "everything the model saw". The assembled system prompt, the context injections and whatever compaction dropped exist only inside the process that builds the prompt — Claude Code, running in the box, chosen by the image. hopbox does not own that loop and does not claim to log it.

A run past its cap (4 MB, --agent-run-max-mb) drops its oldest events and says so: truncated: true means the head is gone, and seq still counts from when the run started, so a cursor stays meaningful.

A plain (non-agent) box_delegate {task} is recorded too — its transcript is the raw output it printed, one line per event, with no per-tool structure to parse.

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, each of them leaving a run transcript to review. (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