The boxd CLI lets you manage VMs from your local terminal without SSH keys. Recommended for automation and coding agents.
Install
curl -fsSL https://boxd.sh/downloads/install.sh | sh
Installs the boxd binary to ~/.local/bin/boxd (macOS arm64, Linux x86_64/arm64). The client utilities — the bridge that brings your clipboard, local files, and browser into a VM (macOS Apple Silicon for now) — are built into the same binary; turn them on with boxd client enable.
It also drops a set of agent skills into ~/.claude/skills/ — boxd-cli for everyday CLI driving, plus three setup skills (/boxd-setup-golden, /boxd-setup-deploy, /boxd-setup-fix) that wire boxd into a GitHub repo end-to-end via webhooks. Re-run the installer any time to upgrade the binary and the skills together. (Claude Code only for now — see the Skills page if you want them on another platform.)
Authentication
boxd login # opens browser for OAuth login
boxd logout # remove stored credentials
boxd whoami # show your user ID and SSH keys
You can also authenticate via token:
boxd --token=<TOKEN> list # pass token directly
BOXD_TOKEN=<TOKEN> boxd list # or via env var
API keys
For CI pipelines, scripts, or agent integrations where the OAuth login flow isn’t an option, mint long-lived API keys. The raw value is shown only once at creation time.
boxd keys create NAME # mint a new key, raw value to stdout
boxd keys create NAME --expires-in-secs=86400 # with an expiry (1 day in this example)
boxd keys list # name, prefix, last-used, expires (alias: ls)
boxd keys delete ID # revoke a key (alias: rm)
keys create writes the raw bxd_… value to stdout; the warning + id + expiry go to stderr. To put it straight into a GitHub secret without copy/paste:
KEY=$(boxd keys create "gh-actions deploy owner/repo")
gh secret set BOXD_API_KEY --repo owner/repo --body "$KEY"
The key never lands in shell history or scrollback.
--json mode is also available on all three subcommands for scripting:
| Command | JSON shape |
|---|
keys create --json | {"id", "api_key", "expires_at"} (expires_at: 0 = no expiry) |
keys list --json | [{"id", "name", "key_prefix", "created_at", "last_used_at", "expires_at"}, …] |
keys delete --json | {"id", "status": "deleted"} |
Managing machines
boxd new --name=myapp # create a machine
boxd new --name=myapp --restart=never # restart policy (default: always)
boxd new --name=myapp --auto-suspend-timeout=60 # idle secs before auto-suspend (0 disables)
boxd new --name=myapp --shared # in an org context: shared with the whole org from birth
boxd list # list your machines (alias: ls)
boxd info myapp # detailed info (status, image, auto-suspend)
boxd fork myapp --name=myapp-v2 # fork with full disk copy
boxd fork myapp --auto-suspend-timeout=0 # fork with auto-suspend disabled
boxd fork shared-vm --shared # keep a fork of a shared VM shared (default: private to you)
boxd reboot myapp # reboot (cold — memory lost, ~2s)
boxd pause myapp # pause (warm — memory preserved, sub-ms resume)
boxd resume myapp # resume a paused machine
boxd auto-suspend myapp 300 # suspend after 5 min idle (0 = off, the default)
boxd auto-hibernate myapp 0 # never hibernate (default: 4 hours idle)
boxd destroy myapp -y # destroy (requires -y or --confirm; alias: rm)
See Suspend & resume for how the idle timers work.
Aliases: ls (list), rm (destroy), ssh (connect), proxy ls / proxy rm.
All commands accept --json for structured output.
pause vs reboot
pause / resume: warm. Freezes the VMM process, keeps memory, running processes, and open sockets intact. Resume is sub-millisecond. Same mechanism as auto-suspend, but user-triggered. Status becomes standby.
reboot: cold. Kills the VMM process and spawns a new one. Memory lost, takes ~2s.
Use pause for cost savings without losing state. Use reboot when you need a cold kernel/config restart.
Running commands
boxd exec myapp -- uname -a # run a command
boxd exec myapp 'cd /app && npm start' # shell constructs work
boxd exec myapp -e API_KEY=secret -e DEBUG=1 CMD # env vars
boxd exec myapp --timeout 30 CMD # timeout
boxd exec myapp --tty htop # allocate a pseudo-TTY (for interactive tools)
boxd exec myapp --json echo hello # JSON: {"output":"hello\n","exit_code":0}
Exit codes are forwarded — if the remote command exits 42, boxd exec exits 42.
The remote command’s stdout and stderr are surfaced separately on the
local side, so shell redirects work as you’d expect:
boxd exec myapp -- gcc -v 2>/dev/null # drop the version banner (it's on stderr)
boxd exec myapp -- cargo build 2>build.log # only warnings/errors land in build.log
boxd exec myapp -- make 1>/dev/null # see only the warnings
PTY execs (--tty) are an exception — the kernel TTY layer merges stderr
into stdout (that’s how terminals work), so everything arrives on local
stdout and 2> filters won’t separate them.
The -- separator is required between the VM name and the command. Without it, tokens like sudo or apt get parsed as flags and the command errors out.
Interactive access
boxd connect drops you straight into an interactive shell on the machine over the boxd API — no SSH config, no host keys, no extra credentials. Auto-resumes paused or hibernated machines on demand. Exit codes forward; type exit or Ctrl-D to come back to your local shell.
boxd connect myapp # drops you into a shell
boxd ssh myapp # alias of `connect`
Requires an interactive terminal — boxd connect refuses to run in scripts, pipes, or other non-TTY contexts. For automation, use boxd exec instead:
boxd exec myapp -- uptime # one-shot, scriptable
Editor integration (boxd ssh-config)
boxd ssh-config writes a managed block of Host entries into ~/.ssh/config so any editor or SSH-aware tool sees your VMs as hosts. After running it, <vmname>.boxd is reachable to plain ssh, scp, rsync, Cursor / VS Code Remote-SSH, JetBrains Gateway, Zed Remote, etc. — without per-VM hand-rolling.
Each VM is reachable on a dedicated SSH port (in the 10000–30000 range) on the shared proxy IP, so each stanza carries a HostName, a Port, and a User line. The <vmname>.boxd alias bakes the port in for you — connecting to the bare <vmname>.boxd.sh hostname instead lands you in the management REPL, not the VM.
You rarely need to run this by hand: boxd new, fork, list, and destroy auto-refresh the managed block, so your ~/.ssh/config stays in sync as VMs come and go.
boxd ssh-config # write/refresh entries for all your VMs
boxd ssh-config --remove # remove the boxd block (no API call)
boxd ssh-config --print # render to stdout, don't touch the file (shows each Port)
boxd ssh-config --user=ubuntu # override the User line (default: boxd)
boxd ssh-config --prefix=vm- # prepend a host-alias prefix (default: none → <vm>.boxd)
boxd ssh-config --identity-file=~/.ssh/boxd_key # set IdentityFile in each stanza
boxd ssh-config --path=/etc/ssh/ssh_config.d/boxd # write somewhere other than ~/.ssh/config
After running it:
ssh myapp.boxd # plain SSH (alias carries HostName + Port)
cursor --remote ssh-remote+myapp.boxd /home/boxd # Cursor (Remote-SSH)
code --remote ssh-remote+myapp.boxd /home/boxd # VS Code
# JetBrains Gateway / Zed: pick `myapp.boxd` from their host picker
The block is bracketed with # BEGIN boxd / # END boxd; nothing outside the markers is ever modified. The command is idempotent and re-runs are safe.
ssh-config --json → {"path": str, "entries": [str, ...], "removed": bool}.
Copying files
Paths after : are relative to /home/boxd unless starting with /. Uploads stream automatically — no inherent file-size cap.
boxd cp ./local.txt myapp:/home/boxd/remote.txt # upload
boxd cp myapp:/home/boxd/remote.txt ./local.txt # download
boxd cp myapp:/path/file - # download to stdout
echo data | boxd cp - myapp:/path/file # upload from stdin
Proxy management
Every machine gets https://name.boxd.sh forwarding to port 8000 by default.
boxd proxy list --vm=myapp # list proxies
boxd proxy new api --vm=myapp --port=3001 # create subdomain
boxd proxy set-port --vm=myapp --port=3000 # change port
boxd proxy set-port --vm=myapp --port=auto # auto-detect
boxd proxy delete api --vm=myapp # remove
Proxies are HTTPS-only. To expose a database, an SSH daemon, or any non-HTTP service, use a raw port forward (below).
Exposing raw TCP/UDP ports
boxd expose opens a raw TCP or UDP port on the machine’s public proxy and DNAT-forwards it straight to a port inside the VM — for anything that isn’t HTTP (databases, game servers, custom protocols). The public port is allocated for you from the 40000–60000 range; connect on the machine’s existing name.boxd.sh endpoint at that port. See Port forwarding for the concept.
boxd expose myapp 5432 # forward a public port -> :5432 in the VM (TCP)
boxd expose myapp 9999 --udp # UDP instead
boxd expose myapp 7777 --tcp --udp # both protocols on one allocated public port
boxd expose --list # list all your exposed ports
boxd expose myapp 5432 --remove # remove a forward, freeing the public port
exposed myapp.boxd.sh:48211 -> 5432 (tcp)
Up to 3 forwarded ports per VM. Re-exposing a port you already exposed keeps the same public port and just updates the protocol set. Forwards are owner-only and are removed automatically when the VM is destroyed.
Plan & billing
boxd plan list # shapes + prices, your current plan marked (alias: ls)
boxd plan upgrade --shape=4x16 # upgrade from free — opens Stripe Checkout in your browser
boxd plan change --shape=8x32 # swap shapes in place, prorated
boxd billing status # current tier, shape, status, next renewal
boxd billing portal # Stripe Customer Portal — payment method, invoices, cancel
See Pricing for what each shape includes.
Organizations
If you belong to an organization, your context decides which org a new machine is billed to and which machines list and connect see. Sharing a machine opens it to every member of the org.
boxd org list # organizations you belong to (active context marked)
boxd org switch acme # work in the "acme" org context
boxd org switch personal # back to your personal context
boxd org share myapp # share a machine with the whole org (every member can reach it)
boxd org unshare myapp # stop sharing — private to you again (org keeps paying)
- In an org context,
boxd new creates a machine billed to the org but private to you; add --shared to make it visible to the whole org from birth.
boxd billing follows the active context — in an org it shows the org’s plan and quota.
- Share / unshare is owner-only. Any member can
connect to a shared machine; a private machine (personal or org-billed) is reachable only by its owner.
- An org has two roles: admin (invites and removes members) and member. Admins manage membership themselves in the console at
/app/organizations — see Organizations. Creating an org and changing its plan is handled by the boxd team — reach out.
Sharing a machine wipes its in-VM agent logins (Claude Code, Codex, OpenCode) the instant it goes shared, so a teammate with a shell can never read your personal tokens. Unsharing restores Claude automatically; Codex/OpenCode need a one-time re-login. Forking a shared machine gives you a private fork with your logins intact. See Share a VM for the full handoff.
Shell completions
boxd completions bash >> ~/.bashrc
boxd completions zsh >> ~/.zshrc
Open the docs
boxd docs # opens https://docs.boxd.sh in your browser
Global flags
| Flag | Description |
|---|
--json | Output as JSON |
--api-url / BOXD_API_URL | API server URL (default: https://boxd.sh) |
--token / BOXD_TOKEN | Auth token (overrides stored credentials) |