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/cli/install.sh | sh
Installs to ~/.local/bin/boxd. Supports macOS (arm64) and Linux (x86_64, arm64).
The same installer 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 both 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 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 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 destroy myapp -y # destroy (requires -y or --confirm; alias: rm)
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, boxd-<vmname> is reachable to plain ssh, scp, rsync, Cursor / VS Code Remote-SSH, JetBrains Gateway, Zed Remote, etc. — without per-VM hand-rolling.
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
boxd ssh-config --user=ubuntu # override the User line (default: boxd)
boxd ssh-config --prefix=vm- # override host alias prefix (default: 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 boxd-myapp # plain SSH
cursor --remote ssh-remote+boxd-myapp /home/boxd # Cursor (Remote-SSH)
code --remote ssh-remote+boxd-myapp /home/boxd # VS Code
# JetBrains Gateway / Zed: pick `boxd-myapp` from their host picker
The block is bracketed with # BEGIN boxd / # END boxd; nothing outside the markers is ever modified. Re-run after boxd new / boxd destroy to refresh — the command is idempotent.
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
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) |