Skip to content

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:8080

The 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]
SegmentMeaning
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.
nameBox 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 :profileWhat 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.
:flavorHardware flavor. A recognized named flavor sets the box's CPU/memory caps, overriding the front-door defaults.
^ttlHard 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).
+durationStay-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 its

A 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 (or image) — 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-keys file (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 from ssh 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 with ssh 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:

FormMeans
box1box box1 in your default workspace — never a box of that name in some other workspace
ws1/box1box box1 in workspace ws1
a1b2c3d4e5f60718a full box id, from ls / hopbox://fleet
a1b2c3any 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 id

That 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 relay

The 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 ​

Instant isolated compute — for humans and AIs