---
name: kvbox
description: |
  Manage kvbox boxes (Firecracker microVMs) from the user's terminal with the `kvbox` CLI.
  Use when: creating, listing, connecting to, stopping/starting, hibernating or removing boxes;
  running commands inside a box; copying files in or out; snapshotting a box and starting warm
  clones; publishing ports or an HTTPS address (`https://<sub>.kvbox.app`); custom domains (`kvbox domain add`); deploying a React or Next.js app or a Node server
  to its own HTTPS address (`kvbox deploy`); orgs; usage and credit.
  Also use when the user says "kvbox", "spin up a box", "run this in a VM", "sandbox this",
  "give the agent a machine", or mentions `*.kvbox.app` URLs.
---

# kvbox CLI

kvbox gives you real Linux VMs (Firecracker microVMs, each with its own kernel) that boot in
seconds. A box has a private IP, a persistent disk and your SSH key. A running box can be
snapshotted (memory + disk) and new boxes started from that snapshot already warm, in well under a
second. Ports can be published on the internet, and a box can claim `https://<sub>.kvbox.app`.

You are on the user's machine. `kvbox` talks either to the kvbox cloud (`api.kvbox.sh`, or the
user's own control plane) after `kvbox login`, or to a `kvbox-server` on this machine (`--local`,
or after `kvbox logout`).

> `kvbox <cmd> --help` is the authoritative reference for flags. This skill is about behavior:
> what commands do, when to use which, and the gotchas. Full docs: https://kvbox.sh/docs
> (as text: https://kvbox.sh/llms.txt).

## Rules for agents

- **Pass every argument and use `--json`.** Every listing or creating command takes `--json` and
  prints one object or an array. Parse that, never the table output.
- **Destructive commands need `-y` outside a terminal**: `rm`, `snapshots rm`, `vm proxy rm`,
  `vm proxy release`, `vm unlink`, `org rm`, `org members rm`. Never pass `-y` on something the user did not ask
  to delete.
- **Errors** are one line on stderr and a non-zero exit. Exit 2 means the feature is not built yet.
- **Don't use `connect` for scripted work**: it opens an interactive shell. Use `exec`.

## Install and log in

```bash
curl -fsSL https://kvbox.sh/install.sh | bash     # or: npm install -g @kvbox/cli (Node >= 22)
irm https://kvbox.sh/install.ps1 | iex           # Windows PowerShell
kvbox login                                       # email + password for api.kvbox.sh
kvbox login https://cp.example.com                # the user's own control plane
kvbox login -e you@example.com --password-stdin   # scripts
kvbox whoami --json                               # {user, url, sshCertExpires} or null (local mode)
```

A new account is inactive until a payment method is on file or the operator activates it: every
command but login/whoami answers "your account is not active yet". Tell the user; you can't fix it.

## Boxes

```bash
kvbox new dev --json                    # 2 vCPU / 4 GB / 8 GB disk; name defaults to box1, box2, ...
kvbox new dev --size m                  # xs: 1/2GB/8GB, s: 2/4GB/8GB, m: 4/8GB/16GB, l: 8/16GB/32GB
kvbox new dev --from warm               # warm clone from snapshot "warm"
kvbox new dev --auto-hibernate 30m      # hibernate after 30 min with no network traffic
kvbox ls --json                         # yours and your orgs' boxes
kvbox get dev --json                    # one box in full: Machine + `subdomain?` (its HTTPS name and routes)
kvbox rm dev -y                         # disk deleted; snapshots made from it are kept
```

`--json` from `new`, `ls`, `start`, `stop`, ... is the Machine object:
`{id, name, owner, ip, vcpu, memMib, diskBytes, diskUsedBytes, base, image?, createdAt, source, status,
ssh?: {host, port, user}, ports?, ownerKind?, autoHibernateSec?}`. `image` is set for a box not on the
base image (e.g. `postgres`; `base` is then that image's version); `new` adds `connect` for an image that
has one (a `postgres://` URL with the password, shown once).
`status` is `running`, `standby` (paused), `hibernated`, `stopped` or `unknown` (the host did not
answer; it is not a box state). Through a control plane, `ls` answers from what the hosts last
reported: a command you ran shows at once, a change the host made by itself (auto-hibernate, a
crash) within about 30 seconds.

`new` is refused when the host has no free RAM for the box (`--force` skips that check). Through
the cloud the box goes on the host with the most free RAM.

## Run commands: exec

```bash
kvbox exec dev -- ls /app                         # -- separates the box from the command
kvbox exec dev -- 'cd /app && npm test'           # one string to the box's shell: quote pipes, &&, cd
kvbox exec dev -e API_KEY=xxx -e DEBUG=1 -- env   # env vars (repeatable)
kvbox exec dev --timeout 30 -- ./slow-thing       # killed after 30 s, exit 124
kvbox exec dev --json -- make                     # {"output": "...", "exit_code": N}
kvbox exec dev --tty -- htop                      # interactive tool (needs a real terminal)
```

`exec`'s exit status is the command's, so `kvbox exec dev -- 'test -f /app/ready' && deploy` works.
Stdin is forwarded when it is not a terminal, with its end of file.

**Start a server and come back.** `exec` returns when the command exits *and* nothing it started still
holds its output. Detach the server from both:

```bash
kvbox exec dev -- 'cd /app; setsid -f node server.js > server.log 2>&1 < /dev/null'
kvbox exec dev -- 'curl -s localhost:3000'        # a second exec checks on it
```

Trap: `cd /app && node server.js > server.log 2>&1 &` hangs `exec` forever. The `&` backgrounds the
whole `cd && node` pair as a subshell whose own output is still the session, and `exec` waits on it.
Use `;` or put the redirects on the group: `(cd /app && node server.js) > server.log 2>&1 &`.

`exec` and `connect` resume a paused box and wake a hibernated one on their own; `connect` also
starts a stopped one. `exec` refuses a stopped box: run `kvbox start <box>` first. A visit to a box's
`https://<sub>.kvbox.app` wakes a hibernated or paused box too (at most once a minute, not when out of
credit); a stopped box stays stopped.

## Copy files

There is no `cp` command. Pipe through `exec`:

```bash
cat data.json | kvbox exec dev -- 'cat > /tmp/data.json'          # one file in
tar -cf - ./src | kvbox exec dev -- 'mkdir -p /app && tar -xf - -C /app'   # a directory in
kvbox exec dev -- 'cat /app/out.log' > out.log                    # one file out
kvbox exec dev -- 'tar -cf - -C /app dist' | tar -xf -            # a directory out
```

Don't use `--json` or `--tty` when piping bytes.

## Shell: connect

```bash
kvbox connect dev          # interactive SSH shell (short: kvbox cn dev)
kvbox connect dev -- cmd   # one command over SSH with the user's terminal
```

Through the cloud, SSH access is a one-day certificate the CLI renews by itself. `ssh` from
`kvbox ls --json` (`host`, `port`, `user`) works for plain `ssh` too, with the key the CLI keeps.

## Put a box away

| command | memory | host RAM | comes back with |
| --- | --- | --- | --- |
| `kvbox stop dev` | lost | freed | `kvbox start dev`: fresh boot, a few seconds |
| `kvbox pause dev` | kept in RAM | still used | `kvbox resume dev`: instant |
| `kvbox hibernate dev` | written to disk | freed | `kvbox wake dev`: under a second, processes intact |

Files are kept in all three. `stop` on a hibernated box throws the saved memory away. Hibernate is
usually what the user wants for "pause it for later".

Auto-hibernate: `kvbox vm set dev --auto-hibernate 4h` (or `off`). Only network traffic counts as
activity, so a CPU-only job with nobody connected looks idle: give it a longer timeout, or `off`.
`kvbox vm set dev --idle-check /home/kvbox/idle.sh` (or `off`): the host asks that script (exit 0 = idle,
runs as root, 10 s) instead of watching traffic; `get --json` has `idleCheck`. Needs base v0.12.0+.

Wake hooks (kvbox.sh only): a URL that wakes a box when called, for GitHub webhooks, cron or any automation.

```bash
kvbox vm hook create ci --json   # {id, owner, box, url, secret, createdBy, createdAt, lastCalledAt}; the secret only here
kvbox vm hook ls ci --json       # the same without `secret`
kvbox vm hook rm ci <id> -y
curl -X POST -H "Authorization: Bearer <secret>" <url>   # or GitHub's X-Hub-Signature-256 (webhook secret = the secret)
```

A call answers 202 `{box, action: "wake"|"resume"|"none", reason?}`: wakes a hibernated box, resumes a
paused one, never starts a stopped one; at most one wake a minute per hook; 402 when the slug may not spend.

## Snapshots and warm clones

```bash
kvbox snapshots new dev warm --json    # freeze a running box; it keeps running (~1 s pause)
kvbox snapshots new dev warm --replace # retake an existing name; the old one stays until the new one is done
kvbox snapshots ls --json
kvbox new --from warm dev2             # clone: new name and IP, every process where it was
kvbox snapshots rm warm -y             # boxes made from it are not affected
kvbox vm fork dev dev2 --json          # one-step clone, no snapshot left; works on a stopped box too (cold)
kvbox vm fork dev --org acme           # copy into an org: how a box moves between slugs
```

The source box must be running (a fork also takes a stopped one; a hibernated one needs `kvbox wake` first). The pattern: boot a box, install everything, start the services,
snapshot it once, then `kvbox new --from <snap>` for each test run, agent or preview. A clone starts
with no published ports and no HTTPS address.

## Volumes: disks that outlive a box

```bash
kvbox volume create data --size 50GB --box dev --json   # an empty disk made on dev's host and attached: /mnt/data inside dev (a RUNNING box is restarted); {id, name, owner, sizeGib, usedBytes, attachedTo, createdAt}
kvbox volume attach data dev                  # attach again later (after a detach); a RUNNING box is restarted (a few seconds)
kvbox volume ls --json
kvbox volume detach data                      # from the box it is on (or name it: detach data dev); data kept
kvbox volume rm data -y                       # deletes everything on it; must be detached
```

- One box at a time per volume; sizes are whole GB (1-4096) and only occupy what is written.
- A hibernated box refuses attach/detach: `kvbox wake` first.
- Snapshots and forks never carry volumes: a box made from a snapshot comes up with no volume
  attached (the old mount is dropped), attach one if it needs it. Destroying a box keeps its volumes.
- `kvbox get dev` lists a box's volumes with their mount paths.
- A volume lives on one host and attaches only to a box on that host, so on kvbox.sh `--box <box>`
  is required at create: it is made where that box runs and attached to it. Attaching to a box on
  another host is refused (the error names both hosts). With `--local` (one host) `--box` is optional.
- Price on kvbox.sh: $1/month per volume + $0.06/GB/month of its size, by the hour, attached or not.
  A box's own disk: free up to 8 GB, $0.06/GB/month above (`vm rescale --disk` is not free).
  `kvbox usage --json` has them under `volumes` ({id, name, owner, sizeGib, status, volumeHours,
  volumeGibHours, cost?}) and `totals.cost` covers boxes and volumes; with `--by-user` the shape is
  { window, users, volumes?, totals }: volumes are never per user, they are listed as they are.

## Databases: images

```bash
kvbox new db --base postgres -e POSTGRES_PASSWORD=pw -e POSTGRES_USER=app -e POSTGRES_DB=app --json
# Machine + connect: "postgres://app:pw@db.kvbox:5432/app" (password generated when not given;
# a generated one is a secret: keep it from `connect`, `env ls --box` never shows it again)
# `kvbox get db --json` has `url`: the same string, password masked as ****
kvbox new api --near db && kvbox vm link api db --port 5432   # boxes reach each other only through a link
```

- `--base <image>`: `base` (default) or an image the host has; through kvbox.sh the box goes on a host
  that has it, else "no host has image <name>". `vm update` keeps a box on its own image.
- POSTGRES_* are read once, at the box's first boot; changing them later does not change the database.
- A postgres box does not auto-hibernate unless `--auto-hibernate` is given.

## GitHub runners: the gh-runner image

```bash
kvbox new ci --base gh-runner --size m -e GITHUB_URL=https://github.com/<owner>/<repo> -e GITHUB_RUNNER_TOKEN=<t> --json
# Machine + connect: "<GITHUB_URL>/settings/actions/runners"; the runner is online there within a minute
```

- `GITHUB_RUNNER_TOKEN` is the registration token from the repo's (or org's) Settings → Actions → Runners →
  New self-hosted runner: single-use, valid one hour, read once at first boot. Optional: `GITHUB_RUNNER_NAME`
  (default the box name), `GITHUB_RUNNER_LABELS` (default `kvbox`; workflows use `runs-on: [self-hosted, kvbox]`).
- Registration failed (expired token): `kvbox exec ci -- journalctl -u kvbox-gh-runner-init`, then
  `kvbox env set GITHUB_RUNNER_TOKEN <t> --box ci` and restart the box.
- It hibernates 10 min after its last job (its idle check, not network traffic; `vm set --auto-hibernate`
  changes the time); `kvbox vm hook create ci` + a GitHub webhook (event Workflow jobs) wakes it on a queued job.
- `kvbox rm` leaves the runner listed offline on GitHub; never fork a registered runner.

## Environment variables and secrets

```bash
kvbox env set NODE_ENV production --json       # {owner, name, boxes}: boxes = how many get it at their next start
kvbox env set DATABASE_URL 'postgres://…' --secret   # write-only: never shown again (ls shows (sealed))
kvbox env ls --json                            # [{name, value?, secret, updatedAt}], value absent for a secret
kvbox env rm NODE_ENV OLD_KEY -y                # each on its own: a name not found is reported, exit 1, the rest removed
kvbox env set REGION eu --org acme             # the org's list: every acme box gets it
kvbox env set NODE_ENV staging --box api --json   # one box's own, over the account's: {box, name, running}
kvbox env rm NODE_ENV --box api -y
kvbox env ls --box api --json                  # {box, vars, inherited}: its own, and the owner's it does not override
```

- A list belongs to the account or an org (`--org`); every box of that owner gets all of it. kvbox.sh only.
  `--box <box>` (or `new -e NAME=value`) sets a box's own instead: kept on its host, wins over the
  owner's of the same name, follows the box into snapshots and forks, works with `--local` too.
- A box takes the list when it STARTS (new, start, a restore from a snapshot or fork). A running box
  keeps its old values: `kvbox stop <box> && kvbox start <box>` to apply a change. Waking a
  hibernated box does not reload them.
- Inside the box they are in every SSH session (`kvbox exec <box> -- printenv NAME` works) and in
  `/etc/kvbox/env` (systemd EnvironmentFile form; the `kvbox deploy` app reads it and restarts).
- Names: letters, digits, `_`, not starting with a digit; PATH, HOME, USER, SHELL, KVBOX_* refused.
  Values: one line, ≤ 4 KB, literal (no quoting needed). ≤ 100 per owner. Needs base v0.10.0+.

## Publish ports and HTTPS names

```bash
kvbox vm proxy add dev 3000 8080 --json     # random public port on the host: [{box, guestPort, hostPort, proto, address?}]
kvbox vm proxy add dev 9999 --udp
kvbox claim dev myapp --port 3000           # https://myapp.kvbox.app -> box port 3000 (= vm proxy claim)
kvbox vm proxy add dev 8080 --name api      # https://api.myapp.kvbox.app (after claim)
kvbox vm proxy set-port dev 4000            # point https://myapp.kvbox.app at another port
kvbox vm proxy ls --json
kvbox vm proxy rm dev 3000 -y               # one raw port
kvbox vm proxy rm dev --name api -y         # one HTTPS name
kvbox vm proxy release dev -y               # give up the subdomain and every name under it
```

- The server inside the box must listen on `0.0.0.0`, not `127.0.0.1`.
- A box claims one subdomain. Names are global and first come, first served. On "taken", pick
  another.
- The first `--name` under a subdomain waits about a minute for its TLS certificate.
- WebSockets work. Only HTTPS goes under names; raw TCP/UDP stays on `ip:port`.
- While the box is not running, its HTTPS address shows a "box is stopped" page, and its ports
  close. They reopen on the same public ports when it runs again.
- Anyone with the address can reach it. Say so when publishing something private.

## Box to box: names and links

```bash
kvbox new db --near api                     # same host as api (links need that)
kvbox vm link api db --port 5432            # api may reach db:5432; inside api: db.kvbox:5432
kvbox vm links --json                       # [{from, box, port}]
kvbox vm unlink api db --port 5432 -y
```

- Inside a box, `<name>.kvbox` is the same owner's box; another slug's is `<name>.<slug>.kvbox`
  (resolves only once linked). Base image v0.11.0+.
- Boxes are closed to each other until linked: one port per link, one way, TCP, never public.
- Different hosts: `link` refuses and names `--near`. Removing either box removes its links.

## Custom domains

```bash
kvbox domain add example.com dev [--port 3000] --json   # {domain, box, port, status, www, url, error, records: [{type, name, value}]}
kvbox domain ls --json                                  # status "pending" (DNS not pointing here yet, see error) or "live"
kvbox domain rm example.com -y
```

- `--port` defaults to the port of the box's `https://<sub>.kvbox.app` address; without one, give it.
- The user sets `records` at their DNS provider: an A record to the edge, a TXT record `_kvbox.<domain>`
  that proves the domain is theirs (keep it set), plus an optional `CNAME www` (then `www.` serves the same site).
  In a DNS editor the host field takes the name without the zone: `@`, `_kvbox`, `www` for example.com (the CLI prints them so).
  You cannot set them; tell the user exactly what to add. An AAAA record on the name keeps it pending (IPv4 only): it must go. A domain pending for 3 days is removed; add it again once the records are set.
- Checked every minute; `live` once the A record points here and the TXT record is set, certificate on the first visit.
- "claimed by another account": the error names the TXT record; once the user sets it, `domain add` again takes the domain over.
- At most 5 per user or org. Needs `kvbox login` (not `--local`).

## Deploy a site

`kvbox deploy` puts a React app (Vite or Create React App), a Next.js app or a Node server (Express, Fastify,
Koa, NestJS, Hono with `@hono/node-server`; else `--template node`) on a box with its own HTTPS address.
It makes a box if there is none, builds the app, ships it in and claims the address. Running it
again redeploys: build, ship, done, same box, same URL.

```bash
cd my-react-app
kvbox deploy -y --json           # first run: defaults (box and address named after the directory), writes kvbox.json
kvbox deploy --name site --sub mysite --size xs -y --json   # or say it all
kvbox deploy -y --json           # later runs: no questions, kvbox.json has the answers
```

- The first run on a terminal asks template, box name, address, size and (Next.js, Node) port, each with a default; off
  a terminal it needs `-y` or the flags. Answers go to `kvbox.json` next to `package.json`:
  `{template, name, subdomain, size, dist, port}`. Edit it or pass a flag to change one thing. The box
  is `xs` (React, Node) or `s` (Next.js) and hibernates after an hour without traffic (a visit wakes it);
  `kvbox vm set` changes that.
- React is built here and served by nginx on 80. Next.js ships the source (no node_modules, .next,
  .git) and the box installs, builds and runs it (`npm run start`, systemd unit `kvbox-app`, `PORT=3000`
  or `--port`, log in `journalctl -u kvbox-app`); a deploy then takes a minute or two. `size` defaults to `s`.
- Node ships the source (no node_modules, .git), installs, runs `build` only if the script exists,
  then `start` as `kvbox-app` with `PORT=3000`. Needs a `start` script (`dev` is never used). The app
  must listen on `process.env.PORT`; one on another port: `--port 8080` (saved as `port`).
- Next.js and Node: the deploy waits 30 s for the app to answer on its port; if it never does, it
  fails with the last lines of `journalctl -u kvbox-app` (a crash, or the wrong port).
- `--json`: `{box, created, template, url, dist, files, bytes}`. The URL is `https://<sub>.kvbox.app`.
  On a host's own socket (no `kvbox login`, or `--local`) there is no address: no Address question,
  no `subdomain` in `kvbox.json`, and `url` is `http://<box ip>`, reachable from that host only.
- The box is yours, or the `--org` org's (`--org acme`, saved as `"org"` in `kvbox.json`); a bare
  name never takes an org's box here. `--name` to another box drops the saved address (it was the
  old box's); a box that already answers under another name keeps it and the command says so.
- A failed build deploys nothing. "image too old": the box predates base v0.8.0; `kvbox vm update <box>` or deploy under another `--name`.
- The box is a normal box: `kvbox connect`, `exec`, `stop`, and `--auto-hibernate` all apply; a visitor
  wakes a hibernated one.

## Orgs

```bash
kvbox org new acme --name "Acme Inc"           # you are its owner
kvbox org members add bob --role admin --org acme
kvbox new lab1 --org acme                      # creates acme/lab1
kvbox connect lab1                             # bare name: your box, else the one org box with that name
kvbox connect acme/lab1                        # when two orgs have a lab1 (or --org acme)
kvbox ls --org acme
```

`--org` is per command (before or after it), and nothing is remembered between commands. Without
it, `new` creates a personal box. Members can use any box in the org but remove or rescale only the
ones they created.

## Resize, update, inspect

```bash
kvbox vm rescale dev --vcpu 4 --mem 16GB --disk 32GB   # or --size m; disk only grows; running box restarts
kvbox vm update dev        # move onto the current base image; files kept, running box restarts
kvbox vm top --json        # load, memory, disk of each running box as the box sees them
kvbox vm usage dev --json  # hours running/paused/hibernated/stopped, vCPU-h, GiB-h, cost
kvbox usage --json         # this month across your boxes and volumes (--since/--until, --org; --by-user: boxes per maker, volumes as they are)
kvbox credit --json        # prepaid credit left, yours and each org's
```

## Troubleshooting

- **Every box shows `unknown`**: the control plane can't reach the host. Nothing to do on the
  client; tell the user.
- **SSH refused**: the CLI renews the certificate and retries once by itself. If it still fails,
  `kvbox whoami` and log in again.
- **"not active yet"** (account or org): needs a payment method or the operator.
- **No route to the box from the internet**: publish the port (`vm proxy add` or `claim`); a box's
  `ip` is private to its host.
