Appearance
SSH & the front door
The front door is a plain SSH listener where the username is a box spec and the client key is the identity — no signup, no pre-created box. It's how humans reach boxes; anything that speaks SSH works (ssh, VS Code Remote-SSH, scp, rsync, JetBrains Gateway), with no public port on the box.
sh
ssh box1@host # spawn/attach box "box1" (default image)
ssh box1:ubuntu-22.04@host # pick a catalog image
ssh box1:debian-12:cpu+5m@host # image + flavor, stay alive 5m after disconnect
ssh box1+@host # force a fresh box
ssh images@host # list the image catalog (spawns no box)
ssh cli@host ls # the management CLI (spawns no box)How it works
The box's agent dials out to hopboxd and serves an SSH server over that reverse tunnel — nothing routes into the box. The front door authenticates you by key, then proxies your session straight into the box's SSH server, so shells, exec, and the sftp subsystem (scp/sftp/rsync) all work end-to-end.
Port forwarding
Local forwarding (ssh -L) works — tunnel a port on a box back to your machine:
sh
ssh -L 8080:127.0.0.1:8080 box1@box.hopbox.dev # reach the box's :8080 at localhost:8080The box dials the target from inside itself, so a forward reaches only what the box can reach — its own loopback and whatever its egress allows. That is the box's existing network fence doing its job, not a separate forwarding policy; you cannot use a forward to pivot to something the box couldn't already talk to. A forward to a closed port fails with the target named (direct-tcpip: dial …: connection refused) and leaves the session intact.
This is what VS Code Remote-SSH, JetBrains Gateway, and dev-server / database tunnels use. Reverse forwarding (ssh -R) is not supported yet.
For an HTTP service, you may not need a tunnel at all. The API can proxy a box port directly — GET /v1/boxes/{box}/proxy/{port}/…, authed by your hbx_ key — so a browser can open the app a box is serving with nothing to set up. See the HTTP API. Reach for ssh -L when the thing on the port isn't HTTP (a database, a raw TCP service) or when you want it on your own machine's localhost.
Username grammar
[workspace/]name[:image|profile[:flavor]][^ttl][+duration]| Segment | Meaning |
|---|---|
workspace/ | Optional workspace prefix. Omit = your default workspace (unchanged behavior); ws1/box1 is box box1 in workspace ws1, with its own /wrk drive, home, and secrets. |
name | Box name, created on first connect. Names are per-owner, and per-workspace — your box1 is distinct from another key's box1, and ws1/box1 is distinct from your default box1. Append + to force a fresh box. |
:image or :profile | What the box starts from: a catalog image (ssh images@host lists them; with docker, any OCI ref), or one of your own box profiles — your image plus your defaults, under a name you chose. Yours wins a clash. Omit = --default-image. |
:flavor | Hardware flavor. A recognized named flavor sets the box's CPU/memory caps, overriding the front-door defaults. |
^ttl | Hard deadline from creation (30m, 2h). The box is torn down then, whatever is happening — an attached session, a keep-alive pin and the durable flag do not extend it. Clamped to your tier's ceiling. Omit = no deadline (a run box gets its tier's default). |
+duration | Stay-alive grace after disconnect (5m, 1h). Omit = the daemon default grace. |
The suffixes are written in that order — box1:go^30m+5m reads left to right as what the box is, when it dies, and how long it lingers after you disconnect. ^ rather than ! because an interactive bash or zsh would history-expand a ! inside an ssh argument.
A profile fills in whatever the rest of the spec leaves out — flavor, lifetime, deadline, egress — and anything you state explicitly wins:
sh
ssh box1:go-dev@host # your go-dev profile, with its defaults
ssh box1:go-dev:big^2h@host # ...at the big flavor, 2h deadline — yours, not itsA deadline is what makes a box safe to walk away from: a build that wedges or an agent that loops stops costing you something at a time you chose, rather than at idle-reap hours later. It is enforced by the daemon, so it survives whatever started the box crashing.
Every way of creating a box takes it, and takes it the same way — connecting to a username, cli up, snapshot fork (snapshot fork base nightly job^20m), and POST /v1/exec/{spec}. There is deliberately no path that makes a box your tier cannot bound: a deadline you can only set on some of them is one you cannot rely on.
Some usernames are reserved:
images(orimage) — prints the image catalog (spawns no box).cli— your management CLI (ls,rm, …), key-scoped (no box).sudo— the fleet-wide admin console (all owners); allowed only for keys in the daemon's--admin-keysfile (no box)._— an anonymous throwaway box: a fresh one each connect, no stable name.session-<id>— reconnect to one of your boxes by box reference (an id, an id-prefix fromssh cli@host ls, or a name).jump— the confidential relay:ssh -J jump@host root@<box>reaches a box end-to-end, with the gateway relaying ciphertext only (no box).hbxs_<token>— a share link: connects the holder to someone else's box (the one the token grants), with any key. Minted by the box owner withssh cli@host share <box>.
Box references
The username grammar above creates a box. Everything that addresses one you already have — ssh cli@host, the HTTP API, the MCP plane, ssh session-<id>@host — takes a box reference, and every one of them reads it the same way:
| Form | Means |
|---|---|
box1 | box box1 in your default workspace — never a box of that name in some other workspace |
ws1/box1 | box box1 in workspace ws1 |
a1b2c3d4e5f60718 | a full box id, from ls / hopbox://fleet |
a1b2c3 | any id prefix that matches exactly one of your boxes |
Two rules make that unambiguous. A bare name is the default workspace, matching ssh box1@host — so box1 and ws1/box1 stay two different boxes however you reach them. And an id prefix must be hex, which is why a name like box1 is never mistaken for one.
A reference that matches more than one box is refused, and the refusal names the candidates:
console
$ ssh cli@box.hopbox.dev rm abcd
box "abcd" is ambiguous — it matches one (abcd11110000), two (abcd22220000);
use a workspace prefix or an idThat refusal is the point. Addressing a box is usually the prelude to destroying it, so guessing between two candidates is the one thing this must never do.
Over HTTP, the workspace is a query parameter
A URL path can't carry the /, so the API spells it DELETE /v1/boxes/box1?ws=ws1 — the same ?ws= key /v1/wrk already uses.
Lifetime — auto-suspend
Front-door boxes auto-suspend when idle: after --idle-timeout (default 2m) with nobody attached and no shell open in the box, it is snapshotted to disk (compute stops) and your next connection resumes it — nothing is lost on disconnect, you reconnect straight back in. A box never reconnected to is eventually idle-reaped (--idle-reap, default 3h); box-guest auto-suspend off or a keep-alive pin holds it longer. See lifecycle.
Identity
Your SSH key is your identity; the box is owned by that key's fingerprint. Reconnecting with the same key reuses the same box (within its lifetime); a different key connecting to the same name is refused while that box is alive. See Identity & tiers.
Confidential access — ssh -J
By default the front door terminates your SSH: it authenticates you, opens its own session into the box, and proxies it — so the gateway can see the session. For work where the gateway must not be able to read the session, reach the box end-to-end with a jump:
sh
ssh -J jump@box.hopbox.dev root@mybox # end-to-end; the gateway only relays
scp -J jump@box.hopbox.dev ./f root@mybox:/tmp/ # scp/sftp ride the same relayThe reserved jump front-door user puts the connection in relay mode: the gateway forwards raw bytes to the box's SSH server and never opens a session of its own — it holds no key to the box and sees only ciphertext. The box authenticates your key directly (the gateway hands it your public key at spawn; your private key and the session never leave your client).
A box first reached this way is confidential for its lifetime: it authenticates you and refuses the normal terminated path — ssh mybox@host returns a pointer to use ssh -J. The jump target is a box spec (root@mybox, or root@mybox:python+1h).
Reference
- ssh cli commands —
ls,rm,help. hopboxdconfig —--ssh-addr,--grace, the front-door flags.