Skip to main content
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:
CommandJSON 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

FlagDescription
--jsonOutput as JSON
--api-url / BOXD_API_URLAPI server URL (default: https://boxd.sh)
--token / BOXD_TOKENAuth token (overrides stored credentials)