# Vers CLI reference Source: https://docs.vers.sh/cli-reference/overview The complete command, configuration, output, and exit contract for the pre-launch Rust CLI. The standalone `vers` binary talks directly to the HTTPS control plane. It does not maintain a local project, `HEAD`, checkout, alias database, or implicit target. Anonymous command telemetry is a separate, optional product signal; use an environment opt-out when an invocation must leave no telemetry state. `COMPUTER` accepts a full computer UUID, its exact remote name, or an unambiguous UUID prefix of at least eight characters. The standalone binary and `ssh try.vers.sh` gateway have different root command sets. This page describes only the locally installed binary. The gateway generates mutation recovery identities automatically. Buffered mutations omit them from normal success output; pending or ambiguous results include the identity and an exact retry command. Gateway `run` instead sends its recovery identities on stderr before initiating image import or computer creation. Advanced callers may supply `--idempotency-key UUID` for `create`, `run`, and `delete`, or `--commit-id UUID` for `commit`, when they need to retain the identity independently of the SSH response. Gateway account-key revocation also requires explicit confirmation with `ssh-key revoke FINGERPRINT --yes`. ## SSH gateway and shell access The SSH gateway is a separate command surface. Connect without a command for trial onboarding or an interactive command shell: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} ssh try.vers.sh ``` An unrecognized SSH key creates or resumes a trial. With a terminal, the gateway opens the trial shell; without one, it prints the trial help and exits. A valid one-shot command that needs an identity can create or resume the trial and continue on the same connection. Identity-free commands such as `help` do not allocate a trial. A known key can also run a one-shot gateway command: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} ssh try.vers.sh list ssh try.vers.sh help create ssh try.vers.sh run ubuntu:24.04 -- echo hello ssh try.vers.sh ssh devbox ``` `ssh try.vers.sh ssh COMPUTER` opens an interactive SSH session to that computer. The gateway supports `help`, `whoami`, `list`, `get`, `create`, `run`, `delete`, `commit`, `resize`, `write`, `ssh`, `ssh-key`, `env`, `org`, `link`, `upgrade`, and interactive `exit`. Gateway `run` accepts a public OCI reference or existing Vers image UUID. Local image selection with `--local` and copying a local environment value with bare `-e KEY` remain standalone-only; pass gateway environment overrides as `-e KEY=VALUE`. The gateway does not expose durable operation inspection, portable `checkpoint`/`restore`, retained logs, `cat`, workload `exec`, or telemetry preferences. The ordinary gateway create path needs neither an image UUID nor a recovery UUID: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} ssh try.vers.sh create --name devbox ``` Non-streaming one-shot gateway mutations buffer their response. If surviving a complete SSH channel loss is required, supply an advanced recovery identity before running one; otherwise a disconnect can hide the automatically generated value. Foreground gateway `run` streams workload output and sends its create operation identity, plus a derived image-import identity when needed, on stderr before the corresponding remote effects. Detached `run` prints the computer ID and returns after admission. Losing the SSH channel detaches the observer without cancelling admitted durable work; Ctrl-C does the same in the interactive gateway shell. ## Local help These commands do not read control-plane configuration or contact the control plane: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} vers vers --help vers --version vers help vers help create vers help env set ``` Add `--json` to `vers help` when a program needs structured help metadata. `vers COMMAND --help` (or `-h`) is the same as `vers help COMMAND` when the flag directly follows the command path, including nested commands such as `vers image import --help`. Anything after the flag, or options before it, is a usage error that names the matching `vers help` command. Root help and version are not captured by telemetry. Targeted `vers help` commands are ordinary parsed commands and may emit the documented anonymous telemetry event. Unknown commands, misspelled options, missing confirmation flags, and invalid values are usage errors. The interactive SSH gateway prompt supports Up/Down history, cursor movement, Ctrl-A/Ctrl-E, Ctrl-U, and Tab completion. Ctrl-C clears the current line; at an already-empty prompt, pressing Ctrl-C twice in a row disconnects. Ctrl-D also disconnects from an empty prompt and otherwise deletes at the cursor. Commands that can contain secret values (`env`, `run`, and `write`) are not saved in the in-session history. ## Authentication and endpoint Configuration precedence is: 1. `VERS_API_KEY` and `VERS_ORCHESTRATOR_URL` 2. `~/.versrc` 3. the default endpoint, `https://api.vers.sh` `~/.versrc` is JSON: ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "apiKey": "", "orchestratorUrl": "https://api.vers.sh" } ``` The API key is required for control-plane commands. Local help, version, `upgrade`, and `telemetry` preference commands do not require it. There is no `login`, `logout`, `signup`, or command-line API-key option. A custom control-plane URL must use HTTPS; HTTP is accepted only for `localhost` or a numeric loopback development endpoint. It must be a bare origin: credentials, a non-root path, query parameters, and fragments are rejected. ## Anonymous telemetry Anonymous CLI telemetry is enabled by default and sent directly to PostHog on a best-effort basis. It records `cli_command_invoked` for a parsed command and `cli_command_failed` for a nonzero completed result. Properties are limited to: * a random installation ID; * the typed command name, never its arguments; * CLI version and operating system/version and architecture; * failure exit status and elapsed duration. Person profiles and GeoIP enrichment are disabled. The CLI never sends paths, command output, environment values, API credentials, or error messages. Telemetry delivery cannot change command output or exit status. Root help, version, parse failures, and telemetry-management commands are not captured. The persistent preference and random ID live in `~/.vers/telemetry.json`. Inspect or change them locally: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} vers telemetry status vers telemetry disable vers telemetry enable ``` `disable` removes the installation ID. For an environment-only opt-out that does not create telemetry state, set either variable: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} VERS_TELEMETRY=0 vers list DO_NOT_TRACK=1 vers list ``` ## Updating ```text theme={"theme":{"light":"min-light","dark":"min-dark"}} vers upgrade [--check-only] ``` `vers upgrade` checks the stable `latest.json` release manifest, selects the exact binary for the current operating system and architecture, and installs it beside the running executable. `--check-only` reports the current and latest stable versions without changing the binary. There are no automatic or background update checks. Before replacement, the updater enforces the manifest's byte count and SHA-256 digest, runs the staged binary's `--version`, and then atomically renames it over the current executable. The executable's directory must be writable. The checksum detects corruption relative to the HTTPS manifest; it is not an independent signature or publisher-authentication mechanism. ## Computers Structured computer commands support `--json` unless noted otherwise. | Command | Purpose and options | | - | - | | `vers whoami [--json]` | Show the authenticated user and current organization. | | `vers list [--json]` | List computers, newest first. | | `vers get COMPUTER [--json]` | Show one computer's metadata. | | `vers exec COMPUTER [OPTIONS] [--json] -- COMMAND [ARG...]` | Run one bounded process inside the primary OCI workload. | | `vers session COMPUTER [OPTIONS] [-- COMMAND [ARG...]]` | Start or reattach to the computer's interactive session. | | `vers create [--oci-image IMAGE_ID] [OPTIONS] [--json]` | Create a detached computer from the managed default or an explicitly imported image. | | `vers delete COMPUTER_ID --yes [OPTIONS] [--json]` | Durably delete a computer by immutable UUID. | | `vers wake COMPUTER [--json]` | Wake a sleeping computer and wait until it is running. | | `vers sleep COMPUTER [--skip-wait-boot] [--json]` | Put a running computer to sleep, keeping its state. | | `vers commit COMPUTER_ID [OPTIONS] [--json]` | Create a snapshot of a computer by immutable UUID. | | `vers checkpoint COMPUTER_ID [OPTIONS] [--json]` | Durably capture a portable whole-computer checkpoint. | | `vers checkpoint list [--computer COMPUTER] [--json]` | List the checkpoints and commits in your organization or projects, newest first. | | `vers checkpoint delete CHECKPOINT_ID --yes [--json]` | Delete a checkpoint or commit; an already-deleted one succeeds. | | `vers restore CHECKPOINT_ID [OPTIONS] [--json]` | Restore a portable checkpoint into a new computer. | | `vers resize COMPUTER_ID --disk N [OPTIONS] [--json]` | Grow a computer's root disk by immutable UUID. | `vers get` resolves names and UUID prefixes through the computer inventory, then uses the v2 observation resource for the selected immutable ID. Its displayed state, identity, labels, owner, and creation time come from v2. The reserved IP, CPU, memory, and lineage JSON fields remain `null`, and their human-readable lines are omitted. ### `wake` and `sleep` ```text theme={"theme":{"light":"min-light","dark":"min-dark"}} vers wake COMPUTER [--json] vers sleep COMPUTER [--skip-wait-boot] [--json] ``` Both accept a UUID, an unambiguous UUID prefix of at least 8 characters, or an exact name, and print the computer's resulting metadata exactly like `vers get`. Asking for the state a computer is already in succeeds. `sleep` waits for a booting computer unless `--skip-wait-boot` is present. Commands that need a running computer (`exec`, `cat`, `write`, `logs`, `workload-logs`, and `ssh-key`) wake a sleeping computer automatically: when Vers rejects the request because the computer is sleeping, the CLI writes `vers: computer ID is sleeping; waking it` to stderr, wakes it, and sends the request once more. The rejection guarantees the first request had no effect, so the retry cannot run a process or write a file twice. Add `--no-wake` to fail instead. `commit`, `checkpoint`, and `resize` never wake a computer implicitly. ### `exec` ```text theme={"theme":{"light":"min-light","dark":"min-dark"}} vers exec COMPUTER [--workdir PATH] [--env KEY=VALUE]... [--timeout SECONDS] [--json] -- COMMAND [ARG...] ``` `exec` sends the argv after `--` exactly as separate arguments. It does not insert a shell and never falls back to SSH for platform-init computers. The executable must exist in the computer's OCI image; use an absolute path when the image's `PATH` is uncertain. Environment overrides use repeated `--env` options, and duplicate keys are rejected. The timeout range is 1–3600 seconds; the server default is 300 seconds. Exec admission is not idempotent: `exec_id` correlates the response but does not deduplicate starts. If the HTTP request times out, the connection is lost, or the terminal response cannot be validated, the command may already have started. The CLI says so explicitly; do not blindly retry such an ambiguous request. A structured `exec_unavailable` response is also ambiguous because the bridge could have lost the terminal reply after starting the process. A structured `command_timeout` response is different: it is the guest's definite terminal result for the requested execution. Human mode copies guest stdout to stdout and guest stderr to stderr, then exits with the guest status. JSON mode writes one object containing `computer_id`, `exec_id`, `exit_code`, `stdout`, and `stderr` to stdout and does not duplicate guest stderr onto the diagnostic channel. Portable process statuses are 0–255. The guest protocol uses `-1` for signal termination; JSON retains that exact value, while a status outside 0–255 maps the local CLI process status to 1 instead of being silently truncated by the operating system. This `exec` bridge is synchronous and string-based. It does not support stdin, streaming, attach/resume, or binary-safe output; invalid UTF-8 returned by the guest is replaced by the server's UTF-8 conversion. Use `vers session` for an interactive PTY-backed process. ### `session` ```text theme={"theme":{"light":"min-light","dark":"min-dark"}} vers session COMPUTER -- COMMAND [ARG...] vers session COMPUTER vers session COMPUTER --stop ``` The first form starts a platform-init computer's single interactive session, passing the argv after `--` exactly and using the local terminal's current rows and columns for its PTY. The second reattaches to that same PTY after a disconnect and applies the current terminal size. Later window changes are forwarded while attached. Press **Ctrl-]** to detach without terminating the guest process; Ctrl-C is sent to the guest normally. Use `--stop` if a wedged or abandoned session must be terminated. The MVP permits one session and one attachment per computer. Session state lives in the running guest agent, so it does not survive a guest or computer restart. It has no named sessions, session listing, durable scrollback, or automatic reconnection. ### `create` ```text theme={"theme":{"light":"min-light","dark":"min-dark"}} vers create [--oci-image IMAGE_ID] [--name NAME] [--cpus N] [--memory-mib N] [--disk N [--gib | --mib]] [--no-wait] [--timeout SECONDS] [--idempotency-key UUID] [--json] ``` Defaults are 2 vCPUs, 4096 MiB memory, a 120-second wait, and the platform's managed default image. Pass `--oci-image IMAGE_ID` to select an immutable image previously published with `vers image import`. `--disk` is in GiB unless `--mib` is present. Use `run` to acquire and start an OCI workload in one command; `create` is the lower-level detached-computer command. `--no-wait` returns the admitted pending outcome with status 0 rather than claiming the computer is ready, and names `vers operation wait OPERATION_ID` on stderr. If the default wait reaches its deadline first, the command exits 4. The create operation UUID is written and flushed to stderr before the first control-plane request, including with `--json`, so an interrupted client keeps the recovery handle without contaminating stdout. ### `delete` ```text theme={"theme":{"light":"min-light","dark":"min-dark"}} vers delete COMPUTER_ID --yes [--no-wait] [--timeout SECONDS] [--idempotency-key UUID] [--skip-wait-boot] [--json] ``` The CLI writes and flushes a durable operation identity to stderr before deleting the computer. `COMPUTER_ID` must be an immutable UUID obtained from `vers get COMPUTER` or `vers list --json`; names are intentionally rejected so a lost-response retry cannot be redirected after the original computer disappears or a name is reused. `delete` waits up to 120 seconds (`--timeout`) for the deletion to finish and exits 0 once it has. `--no-wait` returns exit 0 as soon as Vers accepts the deletion. While the operation is still running, human output names `vers operation wait OPERATION_ID`; a wait that reaches its deadline exits 4. After an uncertain response, rerun the exact invocation with the printed `--idempotency-key`; do not mint a new one for the same retry. ### `checkpoint` and `restore` ```text theme={"theme":{"light":"min-light","dark":"min-dark"}} vers checkpoint COMPUTER_ID [--idempotency-key UUID] [--name NAME] [--description TEXT] [--keep-paused] [--skip-wait-boot] [--no-wait] [--timeout SECONDS] [--json] vers checkpoint list [--computer COMPUTER] [--json] vers checkpoint delete CHECKPOINT_ID --yes [--json] vers restore CHECKPOINT_ID [--idempotency-key UUID] [--name NAME] [--label KEY=VALUE]... [--allow-auto-sleep] [--no-wait] [--timeout SECONDS] [--json] ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} vers checkpoint list vers checkpoint list --computer devbox --json vers checkpoint delete CHECKPOINT_ID --yes ``` `checkpoint list` shows checkpoints and commits newest first, with the same visibility as `vers list`: an organization-wide API key sees every snapshot in its organization, and a project-restricted key sees every snapshot in its projects, whichever key or teammate created it. Checkpoints from `vers checkpoint` and snapshots from `vers commit` share one catalog, so each row names its `KIND` (`checkpoint` or `commit`) along with its ID, name, source computer, creation time, and `AVAILABILITY`: `portable` snapshots restore on any node, `local` ones are still being published, and `unavailable` ones are waiting for their source node to recover. `--computer` accepts a UUID, an unambiguous UUID prefix of at least 8 characters, or an exact name; pass the full UUID to list snapshots of a computer that has already been deleted. `--json` prints `{"checkpoints": [...]}` with the same fields plus `description`, `is_public`, and `project_ids`. `checkpoint delete` removes one checkpoint or commit by ID and requires `--yes`. Deleting a snapshot that is already gone succeeds with `already absent` (exit 0\); `--json` prints `{"checkpoint_id": ..., "deleted": true|false}`. Vers refuses to delete a snapshot that still has computers restored from it, or one that is still being published; the error says which, and what to do first. You can delete any snapshot `checkpoint list` shows, including ones another key or teammate created. A snapshot outside your organization or projects is reported as `already absent`, exactly like one that does not exist. `checkpoint` captures CPU, memory, device, workload, and root-volume state. A successful result is published only after it is portable across Vers nodes. `restore` resumes that state into a new computer; labels describe the new control-plane resource and do not rewrite the hostname already running inside the guest. Restored computers remain awake by default, matching `vers create`; `--allow-auto-sleep` opts into inactivity sleeping. `checkpoint` waits up to 1800 seconds by default for portable publication; `restore` uses the same 120-second startup default as `create`. `--no-wait` returns exit status 0 once the validated operation is admitted and names `vers operation wait OPERATION_ID`; a terminal operation returned during admission keeps its normal terminal exit status. A default wait that reaches its deadline exits 4. The CLI writes and flushes the operation UUID to stderr before admission. Transient admission responses are replayed automatically with that exact identity inside the command deadline. After an uncertain final response, rerun the exact invocation with the printed `--idempotency-key`; the same key and arguments converge on the original operation and result. ### Legacy `commit` and `resize` ```text theme={"theme":{"light":"min-light","dark":"min-dark"}} vers commit COMPUTER_ID [--commit-id UUID] [--name NAME] [--description TEXT] [--keep-paused] [--skip-wait-boot] [--json] vers resize COMPUTER_ID --disk N [--gib | --mib] [--skip-wait-boot] [--json] ``` The CLI chooses a commit ID before sending the request and flushes it to stderr before that effect. `COMPUTER_ID` must be an immutable UUID obtained from `vers get COMPUTER` or `vers list --json`; names are rejected so retries cannot follow a later name binding. After an ambiguous response, rerun the exact same command with the same `--commit-id`; changing the computer ID, name, description, or wait policy conflicts with that identity instead of creating a different snapshot. `commit` retains the legacy v1 snapshot contract for compatibility. Commits and checkpoints share one catalog, so `vers restore` also accepts a commit ID, but a commit is not a v2 portable checkpoint and may only be restorable where its source computer ran. New checkpoint-and-resume workflows should use `checkpoint` and `restore` together. Disk sizes default to GiB. Resizing only grows the root disk and requires the immutable computer UUID so a reused name cannot redirect a retry. ## Workloads ### `run` ```text theme={"theme":{"light":"min-light","dark":"min-dark"}} vers run [OPTIONS] IMAGE [-- COMMAND [ARG...]] ``` Public OCI references are the default. `--local` exports the image selected by Docker, OrbStack, or Podman on the client machine. An image UUID returned by `vers image import` is also accepted. | Option | Meaning | | - | - | | `-d`, `--detach` | Return after durable admission and print the computer UUID; startup may continue remotely. | | `--rm` | Ask Vers to durably remove the computer after its primary workload exits. | | `--name NAME` | Set the computer's name label. | | `-e`, `--env KEY[=VALUE]` | Set an environment value, or copy `KEY` from the local process. Repeatable. | | `--entrypoint COMMAND` | Replace the image entrypoint. | | `-w`, `--workdir PATH` | Replace the image working directory. | | `--cpus N` | Set virtual CPUs. Must be greater than zero. | | `--memory-mib N` | Set memory in MiB. Must be greater than zero. | | `--disk-mib N` | Set root disk size in MiB. Must be greater than zero. | | `--local` | Read the image from a local container engine. | | `--container-engine auto\|docker\|podman` | Select the local engine; requires `--local`. | | `--timeout SECONDS` | One deadline for image preparation and startup. Default: 1800. | | `--idempotency-key UUID` | Resume the exact image-preparation and startup attempt. | | `--verbose` | Print additional internal stages, source details, and retry diagnostics to stderr. | `run` chooses the computer-create identity before any image effect and derives the image-import and local-upload identities from it. It writes and flushes the create and image-import identities to stderr before image resolution, export, or upload begins. If preparation or startup is uncertain, rerun the exact same invocation with the printed `--idempotency-key`. Changing verified image content, the public reference or source mode, the image name, or the computer request conflicts instead of creating a second interpretation of that identity. Supplying an existing image UUID performs only a catalog read before computer creation. Use `--` before a workload command, especially when it has options: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} vers run ubuntu:24.04 -- sh -lc 'echo ready; sleep 5' vers run --local my-app:dev -- ./server --port 8080 ``` Foreground mode passes workload stdout to local stdout, workload stderr to local stderr, and returns the workload's exit status. It does not support `--json`, stdin forwarding, or signal forwarding. Ctrl-C cancels local CLI work, kills and reaps any container-engine process group, waits for archive verification to stop, and exits 130. It does not signal or delete the remote computer: foreground `run` detaches, and server-owned `--rm` cleanup continues. ### Logs ```text theme={"theme":{"light":"min-light","dark":"min-dark"}} vers logs COMPUTER [--offset BYTES] [--max-entries N] [--stream stdout | --stream stderr] [--json] vers workload-logs COMPUTER [--follow] [--cursor BYTES] [--json] ``` `logs` reads retained command-execution entries. `workload-logs` reads the primary OCI workload. With both `--follow` and `--json`, each streamed event is one newline-delimited JSON value. ## Files and SSH material ```text theme={"theme":{"light":"min-light","dark":"min-dark"}} vers cat COMPUTER ABSOLUTE_PATH [--json] vers write COMPUTER_ID ABSOLUTE_PATH [--mode OCTAL] [--content TEXT] [--create-dirs] [--json] vers ssh-key COMPUTER [--json] ``` Human `cat` output is the file's bytes. Its JSON form contains base64. `write` reads piped stdin when `--content` is absent and accepts at most 16 MiB. It refuses to read implicitly from a terminal, so an interactive invocation must pass `--content`. File modes are octal (`0644`, `755`, or `0o600`) and may not exceed `7777`. `write` requires the immutable computer UUID; `cat` continues to accept any `COMPUTER` reference. When the computer's filesystem refuses a path, `cat` and `write` fail and the API reports a stable error `code`: `file_not_found` (HTTP 404), or HTTP 422 with `path_is_directory`, `not_a_regular_file`, `parent_directory_missing`, `parent_not_directory`, `invalid_path`, `file_too_large`, `permission_denied`, `read_only_filesystem`, `no_space_left`, or `mode_not_applied`. For a missing parent directory, `write` suggests `--create-dirs`; for a full disk, `vers resize`; for `mode_not_applied`, it explains that the content was written but its mode was not set. `ssh-key` returns private connection material for one computer. Treat its stdout as a secret. Account SSH-key management is a gateway capability and is not part of the standalone parser. ## Environment variables These account-level values are applied to future computers: ```text theme={"theme":{"light":"min-light","dark":"min-dark"}} vers env list [--json] vers env set KEY=VALUE [KEY=VALUE ...] [--json] vers env replace KEY=VALUE [KEY=VALUE ...] [--json] vers env clear --yes [--json] vers env delete KEY --yes [--json] ``` Keys must be shell identifiers: a letter or underscore followed by letters, digits, or underscores. A mutation accepts at most 100 pairs; keys are limited to 256 UTF-8 bytes and values to 8192 UTF-8 bytes. An account retains at most 256 variables. `replace` replaces the entire set. `clear` atomically replaces the set with an empty environment. Clear and deletion require `--yes`. Environment values are sensitive and terminal-untrusted. List output never returns their contents: human output uses `` (or ``), and JSON returns `value_present` plus `redacted` metadata for each key. Mutation output contains only the affected keys and whether the operation replaced the set. ## Images ```text theme={"theme":{"light":"min-light","dark":"min-dark"}} vers image import SOURCE [--name NAME] [--local] [--container-engine auto | docker | podman] [--timeout SECONDS] [--idempotency-key UUID] [--verbose] [--json] vers image status IMPORT_ID [--wait [--timeout SECONDS]] [--json] vers image list [--limit N] [--offset N] [--json] vers image inspect IMAGE_ID_OR_NAME [--json] vers image delete IMAGE_ID --yes [--json] ``` Without `--local`, `SOURCE` is a public registry reference. With `--local`, the CLI uses the selected local engine and keeps private-registry credentials on the client. The CLI writes and flushes the import operation ID to stderr before resolution or local engine work. Vers runs Linux/AMD64 images. Public multi-platform references select that variant; a local source must export it. On another host architecture, pull or build with `--platform linux/amd64` before retrying. A local exported archive may be at most 5,000,000,000 bytes and contain at most 17 GiB of total uncompressed layer data. Imports are durable after admission. If a wait deadline expires after the CLI has received a validated pending snapshot, it renders that snapshot on stdout and returns status 4. A deadline can instead expire during preparation, the initial operation read, or admission before any snapshot exists; those paths do not promise recovery output on stdout. Resume admitted work with `image status` or `operation wait`. `--idempotency-key` recovers an uncertain import admission. Retry the exact same invocation with the same key. For a local source, Vers always inspects, exports, verifies, and hashes the current archive again before resending the durable request. The key is bound to the public source reference or local verified content, optional name, image identity, archive size, digest, and format; changed semantic input fails with a conflict instead of being mistaken for the original import. Local source spelling and the engine that produced byte-identical verified content are acquisition details, not different remote effects. Transient admission failures and a reservation that is too close to expiry are retried with the same identities inside the command's one timeout budget. Local archive reads and seeks observe that deadline and Ctrl-C cooperatively before later hashing or upload can begin. Image JSON is a CLI-owned contract rather than a pass-through of control-plane responses. Import and inspect return one image object; list returns one bounded array page of those objects. `--limit` defaults to 50 and cannot exceed 100; `--offset` selects the next page without making the CLI aggregate an unbounded catalog in memory. A pending import or status wait returns the same recovery shape as a non-waiting status: `import_id`, `status`, `cursor`, `stage`, `image_id`, `failure`, and its three timestamps. Delete returns `{ "deleted": false, "image_id": "" }` on an already-absent replay and the same shape with `deleted` set to `true` on the first deletion. Image deletion accepts only the immutable UUID printed by `image list` or `image inspect`; names are intentionally rejected because they can be reused. Repeating deletion of the same UUID succeeds and reports that the image is already absent. An image referenced by a live computer or lifecycle resource still fails with a conflict. ## Durable operations ```text theme={"theme":{"light":"min-light","dark":"min-dark"}} vers operation get OPERATION_ID [--json] vers operation wait OPERATION_ID [--timeout SECONDS] [--json] ``` `get` reads one snapshot. `wait` follows the durable event stream and resumes across reconnects until the operation becomes terminal or the deadline expires. The default wait deadline is 1800 seconds. After `wait` obtains a validated snapshot, a later timeout renders the last pending snapshot on stdout before returning status 4. If the initial read itself reaches the deadline, no snapshot is available to render. ## JSON and output channels `--json` exists only on commands that advertise it above. Structured commands render one CLI-owned result in either human or JSON form. Progress and recovery diagnostics go to stderr, leaving stdout available for results and pipelines. Foreground `run` and interactive `session` are different: they preserve the guest process's output and status instead of wrapping them in JSON. ## Exit status | Status | Meaning | | -: | - | | `0` | Success. Observational `operation get`, non-waiting `image status`, `try status`, and `try inspect` also use 0 for a validated pending snapshot. | | `1` | Generic failure, network error, upstream server failure, or terminal failed/quarantined operation. | | `2` | Usage or local configuration error. | | `3` | The API key was rejected. | | `4` | A wait deadline elapsed while durable work was still running; follow it with `vers operation wait OPERATION_ID`. Requests made with `--no-wait` exit 0 once admitted. | | `5` | A durable create, checkpoint, restore, run, or image request was definitely not admitted before its deadline. | | `6` | Create, checkpoint, restore, run, or image admission is ambiguous; retry the exact request with the same idempotency key. | | `130` | Ctrl-C interrupted the client after tracked local cleanup completed. Durable remote work may continue under the ID already printed to stderr. | Foreground `run` and interactive `session` may return any guest process exit status. If the process terminates by signal, the CLI returns `128 + signal`. Anonymous CLI telemetry follows the documented default-on, explicit-opt-out contract above. # Content-addressable everything Source: https://docs.vers.sh/essays/content-addressable-everything Git's quiet revolution was that it made code identity a function of content, not location. Apply the same idea to running machines and three decades of problems dissolve at once. The most important idea in git isn't branches. It isn't distributed workflow. It isn't the rebase vs. merge argument. It's that **the identity of a commit is the hash of its content**. That sentence is easy to gloss past. It's also the reason git won and the reason every version-control system that came after it — Mercurial aside — looks like git. The content-addressable store made possible a whole family of properties that previous systems had to fight for and usually failed to get. Reproducibility. Deduplication. Untrusted replication. Offline work. Cheap branching. All of these fall out of one decision: *stop naming things by where they live or when they were made. Name them by what they are.* Now apply the same decision to a running machine. The consequences are at least as interesting. ## What location-addressing gets wrong Almost everything in systems has historically been *location*-addressed. A file lives at `/etc/nginx.conf`. A row lives at primary-key 12. A VM lives on `node-042`. When you want the thing, you ask for its location, and whoever owns that location gives it to you. Location-addressing has one deep problem: the name doesn't tell you what the content is. Two files at the same path on two different servers can be wildly different. Two VMs with the same name on different days can be unrelated. The address is an index into mutable storage; the same address can point to different bytes over time. Version control before git inherited this model. Subversion gave every file a path and a monotonically-increasing revision number. If you wanted "the file as it was at revision 1428," you asked the central server to look it up. If you wanted to know whether your copy of `src/foo.c@1428` was the same bytes as somebody else's `src/foo.c@1428`, you asked the server, because the revision number didn't encode the content. Two servers could disagree about what `@1428` meant and you'd have no way to tell. This is a bigger problem than it sounds like. It means: * You need a trusted central server to resolve names * Replication is lossy by default (two replicas can silently drift) * Deduplication requires a coordinator who can compare content * The identity of history depends on who's telling you about it These aren't minor ergonomic issues. They make whole classes of software hard to build. ## Git's idea Linus's trick, stolen from earlier content-addressable stores, was: every object in the repository is named by the SHA of its own bytes. A file blob's name is `sha1(contents)`. A tree's name is `sha1(tree entries)`. A commit's name is `sha1(tree-ref + parent-refs + metadata)`. Four consequences fall out of this, each of which would have been worth a paper on its own: ### 1. Identity equals content Two objects with the same hash are byte-identical. Two objects with different hashes are not. You never have to ask "is this the same version I saw before" — you look at the hash. No coordination, no server call, no ambiguity. ### 2. Deduplication for free If two repositories, two branches, or two machines ever produce the same content, they produce the same hash. Storage systems can deduplicate aggressively without any semantic understanding of what they're storing. The whole world of git objects, across every repo on earth, is a single flat namespace where identical content hashes identically. ### 3. Reproducibility "The project at commit `c1a2b3c4`" means the same thing forever, everywhere, to everyone. There is no "my version of c1a2b3c4" vs "your version of c1a2b3c4." If the hash matches, the bytes match. This is the only reliable way to reproduce a build: not by describing the source, but by pinning its content hash. ### 4. Untrusted replication Because the hash is derived from content, you can pull objects from anyone — a stranger on the internet, a mirror you don't know the operator of, a cached copy from a proxy — and verify their authenticity by recomputing the hash. The sender can't lie about what a commit is. They can refuse to give it to you, but they can't substitute a different commit under the same name. ## The decade after git Most software built in the decade after git borrowed this trick, sometimes explicitly, usually without acknowledging the debt: * **Docker image layers**: content-addressed. You can pull a layer from any registry; the hash verifies authenticity and enables dedup. * **IPFS**: the whole filesystem is content-addressed. Same idea, generalized beyond source code. * **Nix**: every package is hashed. Two builds with identical inputs produce bit-identical output with identical names. Reproducible builds become the default instead of an aspiration. * **Blockchain**: every block is hashed, every transaction is a commitment to content. The mechanism is content-addressing at the tip of an append-only log. * **CDN caches**: static asset URLs are increasingly hash-suffixed (`app.c1a2b3c4.js`) so changing content can't silently collide with cached versions. Every one of these is the same move: *stop referring to things by where they are; refer to them by what they are*. And every one of these, in its domain, unlocked capabilities that were painful or impossible before. ## The primitive still missing The glaring hole in this list is the thing we interact with every day: **running computers**. VMs are still location-addressed. "The staging VM." "VM-042 on node-07." "The EC2 instance in us-east-1c." Every one of those names refers to a specific machine at a specific address, and what's running on it is mutable — anything can happen to it between now and the next time you look. This is why: * **Snapshots are second-class.** You take a snapshot, you get an ID, the ID refers to a blob in some vendor's storage. It's not a commit; it's a reference to a managed object. Different vendors use different schemes. You can't copy a snapshot from AWS to GCP without re-encoding everything. * **Reproducing production is hard.** "The bug only happens in prod" is a phenomenon that exists only because we lack a way to say "give me a byte-identical copy of the machine that was running at the moment of the bug." We can approximate with AMIs, runbooks, terraform, but each of those is a recipe for *approximately* the same thing, not a way to get the same thing exactly. * **CI preview environments are a bespoke feature.** Every company that offers them — Vercel, Netlify, Render — had to invent per-stack machinery. There's no universal primitive to say "branch the production machine at this commit, make it the preview." * **Dev/prod drift is endless.** Your local environment is an arbitrary accumulation of years of `apt-get` calls. Prod is a different accumulation. They diverge because there's no hash of their content to compare against. All of these are the VCS problems of the 90s. All of them get solved by the same move: *make running machines content-addressable*. ## Content-addressable machines Apply git's idea to a VM: * The content of a VM at a moment in time is its memory state plus its filesystem state plus some minimal metadata (CPU registers, open files, etc.) * Hash that content. Call the hash the commit ID. * Two VMs with the same hash are byte-identical running machines. * Two different hashes are different machines. * A VM commit is immutable. A machine at commit `c1a2b3c4` is that machine forever, restorable anywhere, verifiable by anyone. Now work out the consequences: ### Deduplication across the fleet A thousand VMs that were all branched from the same golden image share their base layers. They don't each copy ten gigabytes — they each reference the same ten gigabytes by hash. Storage usage is proportional to *divergence*, not to number of VMs. A project that spawns a million ephemeral workers doesn't need a million times the disk. ### Reproducibility without runbooks "Run the workload against `c1a2b3c4`" is a thing you can say today, tomorrow, next year. The commit is byte-identical. Runbooks become unnecessary. The environment is the hash. ### Moving state becomes moving hashes You don't have to ship gigabytes of VM image to another region, another cluster, another customer. You ship the hash, and the receiving side either already has the bytes or pulls them on demand. Exactly like pulling a git object you don't have yet. Same mental model, whole-machine granularity. ### Merges, forks, and provenance If a VM's identity is its content hash, and each commit records its parent hash, you have a directed acyclic graph of state over time. You can walk it. You can ask "what VMs derive from commit `c1a2b3c4`?" You can identify the lineage of a production bug. You can branch off an old commit to test a fix against the state at the time of the failure. ### Trust without a central authority Because the hash is derived from content, you can verify a commit is what it claims to be by inspecting its bytes. This matters for air-gapped environments, untrusted transport, and adversarial scenarios. A malicious intermediate can't give you a different VM under the same commit ID. The commit is what the commit *is*. ### Cross-vendor portability A content-addressable VM format doesn't care where it runs. If two clouds implement the same hash scheme, `c1a2b3c4` on AWS and `c1a2b3c4` on GCP are the same machine. The vendor lock-in that plagues snapshots goes away the moment the ID is derived from content rather than issued by the vendor. ## What this unlocks Once VMs are content-addressable, a bunch of workflows that were always painful become obvious: * **Bisecting production bugs.** Walk the commit history, restore any commit, test against it. Same loop as `git bisect` — applied to whole machines. * **Sharing environments.** Send someone a commit ID. They restore. They're running the exact same machine as you. * **Cache warming across the fleet.** A warmed cache is just a committed state. Share the commit, skip the warming. * **Deterministic CI.** Build in a committed environment. The commit is the reproducibility guarantee. Same commit → same output forever. * **Rollback with memory.** Restoring to `c1a2b3c4` isn't "redeploy the binaries from commit c1a2b3c4." It's "boot the exact machine, memory and processes included, that was running when you committed c1a2b3c4." Full rollback, not a re-approximation. These aren't new ideas in the abstract — they're the obvious consequences of content-addressing, as soon as the primitive is cheap. The reason none of them were standard-issue is that content-addressing running machines requires sub-millisecond branching and restoration. Without that, you can theorize about content-addressable VMs but you can't use them in a tight loop. ## Where git's model breaks down The analogy isn't perfect. Two places it diverges: 1. **A running VM includes memory.** Git only had to hash bytes on disk. Memory pages change faster and are larger than source code. A content-addressable VM store has to dedup memory aggressively or it drowns in storage. Copy-on-write at the memory-page level is the way out, but it means the VM's commit identity has to be defined carefully — it's the hash of the semantic state, not naively the hash of the memory image. 2. **Running is state itself.** A git commit is a static thing. A VM commit is the state of a running process — a pause point in a continuous execution. Restoring a VM commit isn't like checking out a file; it's resurrecting a process tree with its open sockets and in-memory data structures. This is harder than git, and it's why the primitive wasn't cheap until recently. Both of these are engineering problems, not conceptual ones. The conceptual move — *identity equals content* — transfers directly. ## The shift Once you've seen the move, you start seeing everywhere it hasn't been applied yet. Kubernetes pod state. Database replicas. Long-running LLM sessions. Build caches. Dev environments. Every one of them is a location-addressed thing today that should probably be content-addressed tomorrow. The reason they're not yet is almost always the same reason VMs weren't: the primitive to do it cheaply didn't exist. Once it does, the pattern spreads. This is what Vers is. The observation that running machines deserved what source code got thirty years ago. Everything that falls out of that observation — branching, restoring, snapshotting, deduplication, reproducibility, shareable state — falls out quickly, because it's the same story git told, applied to a bigger artifact. ## Further reading The current product and remote-computer model. What debugging looks like when every commit is a restoration point. The case for treating running state as something worth preserving at all. The current commands for computers, commits, images, and durable operations. # The cost of rebuilding state Source: https://docs.vers.sh/essays/cost-of-rebuilding-state Most engineering time is not spent doing work. It is spent re-creating the conditions under which work can be done. Once you see the pattern, you cannot unsee it. Think about a day in your week where you were maximally productive. You shipped a feature, fixed three bugs, refactored a module. Now think carefully about *where the hours actually went*. A generous estimate: maybe forty percent was spent on the thing. The rest went into getting to the place where the thing could be done, or getting back to that place after something threw you out of it. This essay is about that sixty percent. It's not a new observation — every engineer has felt it — but the costs compound in a way that's rarely reckoned with, and the reason they persist is that no one built a primitive to make them go away. That's changing. It's worth spelling out the problem clearly before talking about the solution. ## The setup tax Every task in software has a setup cost and a doing cost. The doing cost is the change you care about: fix this bug, add this endpoint, write this migration. The setup cost is everything else you have to do *before* the change becomes possible: * Clone the repo. Check out the branch. * Install dependencies. (Did the lockfile change? Did a native module break?) * Boot the database. Seed it with fixtures. * Start the backend. Start the frontend. Start the background worker. * Navigate the UI to the screen you're working on. * Sign in. Reach the correct account. Get past the paywall. * Reproduce the bug once, as a sanity check. None of this is the work. All of it is the *conditions under which the work can happen*. For a small change, the setup might take fifteen minutes. For a mature codebase with a dozen services, it can take an hour. For a task that requires landing on a specific failure mode — "the bug only happens after the second login on a Tuesday" — it can take most of the day. The naive defense is "we only pay this once per task." That's false in practice. We pay it: * Every time we switch branches to review a colleague's PR * Every time we rebase and a dependency changes * Every time Docker decides to nuke its cache * Every time a test run leaves things in a broken state * Every time we come back to a task the next morning and have to reconstruct where we were * Every time a flaky test sends us back to step one The setup isn't paid once. It's paid over and over, and every payment is work that produces nothing except the ability to start doing the work. ## The debugging tax Debugging is the purest form of the problem. A bug report arrives with a list of steps, and the goal is to get into the state where the bug reproduces. You follow the steps. It doesn't reproduce. You look closer. One of the steps assumed an account that was created three weeks ago. You create the account. Now another step fails because its fixture data isn't loaded. You reseed the database. Now the bug's environment differs from production in ways you don't yet know. By the time you have a repro, the interesting part of debugging — *inspecting the state that produces the bug* — is thirty minutes of work. The setup took two hours. Then you try a fix. It fails. The state is now corrupt from the attempted fix. You have to reset everything and start over. You try another fix. Same thing. After four iterations your local environment is a graveyard of half-applied experiments. The loop isn't "form hypothesis, test hypothesis, learn." It's "spend an hour recovering, form hypothesis, test hypothesis, spend an hour recovering." And the cost isn't linear in the number of attempts — it's quadratic, because each recovery also reintroduces small variations that require re-validating the environment before you can trust the next result. ## The experimentation tax Software engineering claims to be an empirical discipline. We form hypotheses, run experiments, update our beliefs. But we don't actually run many experiments, because each experiment is expensive in exactly the way described above. Imagine you want to test three different approaches to a performance problem. In principle, you'd try all three in parallel and pick the best. In practice: * Approach A: implement, run benchmark, record number. (20 min setup, 5 min work) * Approach B: revert A, implement B, run benchmark, record number. (25 min setup because reverting is never clean, 5 min work) * Approach C: revert B, implement C, run benchmark. (30 min setup because something from A or B leaked, 5 min work) Three experiments produce fifteen minutes of actual signal and seventy-five minutes of rebuilding state. Faced with that math, engineers *stop running experiments*. They commit to one approach, ship it, and rationalize. The cost of comparing alternatives has priced alternatives out of the practice. This is why most production systems have exactly one implementation of each component, even when three alternatives are plausible and the correct choice isn't obvious. Not because the team decided A is better than B and C. Because trying B and C was too expensive to bother with. ## The collaboration tax You pair with someone on a problem. They've spent an hour getting into a useful state: repros running, debug server attached, three relevant log streams tailing in three terminals. You sit down to take a shift. Can they hand you their state? No. Not really. They describe it to you. They walk you through the steps. You re-do the setup, approximately, on your own machine, and produce an *approximately* equivalent state. But the repro is different. The log contents differ. You're not really debugging the same thing; you're debugging a thing shaped like the same thing. Pair programming is famously valuable. One of the reasons it's valuable is that you share state — both people are literally looking at the same screen, same terminal, same repro. The moment that shared context ends, each of you has to rebuild the other's state to resume alone. ## The reviewer tax A colleague sends you a PR. To review it well, you need to see the code running. To see the code running, you need: * The PR branch checked out locally * Dependencies up to date with whatever that branch requires * A database in a state compatible with any migrations in the PR * Environment variables matching whatever the PR expects * The exact scenario in which the PR's code path fires For a thoughtful review of a medium-sized change, this is 30-60 minutes of setup *per review*. Most reviewers, honestly, don't pay it. They review the diff, trust the CI, and move on. The sophistication of the review is bounded by the cost of reproducing the PR's environment. This is why CI preview environments were a breakthrough when they arrived at platforms like Vercel and Netlify. "Every PR has a running version, click the link" is the simplest possible answer to the reviewer tax. It's also notable that a decade into modern web development, only a handful of deployment targets have solved this, and mostly for frontend web apps. For backend services with databases and long-running migrations, the reviewer tax is still crushing. ## The pattern All of these are the same tax. You spent expensive effort to get to a state where useful work can happen. The state is ephemeral — it lives in a running process, a populated filesystem, a warm cache, a loaded dataset. Any interruption — a reboot, a branch switch, a broken experiment, a second person needing the state — costs you the full price to rebuild. The deep reason is that **state isn't a first-class value in our infrastructure**. Code is a value: we version it, diff it, merge it, ship it around. Data is a value: we back it up, snapshot it, replicate it. But the *state of a running system*, the thing that took you forty-five minutes to create — that's not a value anywhere. You can't hand it to someone. You can't store it. You can't branch it. Maximum you can do is describe it in words and hope the next reconstruction comes out close enough. The right question isn't "how do we work around the setup tax?" It's "why isn't the running state of a machine a first-class value we can fork, commit, and restore like code?" ## What changes when state becomes a value The cost disappears. Not shrinks — disappears. Because once the setup cost is paid *once*, the state can be committed, branched, and restored in microseconds, and every future operation starts from the committed state instead of rebuilding it. Concretely: * **Debugging.** Commit your environment the moment you reach a repro. Every experimental fix goes on a branch of that commit. Each failed attempt is free to throw away — you restore instantly. You run five fixes in the time the old loop ran one. * **Reviews.** The author commits their PR's state. You restore it in under a second. You're debugging *their* repro, not an approximation. * **Pair work.** The other engineer commits. You branch from their commit. You inherit their exact running state, live — server processes, terminal history, warm caches, open connections. * **Experimentation.** Commit the baseline once. Branch for each variant. Run them in parallel. Compare in seconds. Discard losers without remorse, because there was nothing to lose. * **CI and reviews.** Preview environments for free, because every PR's state is just a commit the reviewer can restore. * **Recovery.** Crashes stop being crises. The state before the crash is a commit; you restore and keep going. Each of these is a day-to-day pain that engineers have absorbed because the primitive didn't exist. Once the primitive exists, the pain goes away *because you can cheaply inherit state*, not because anyone sped up the work itself. ## The economics If sixty percent of engineering time is rebuilding state, and a primitive that makes state branchable and restorable in microseconds turns that sixty percent into near-zero, the implication is that engineering productivity has a lot of slack in it. Not because anyone is slacking — because the cost has been hiding in setup, in reruns, in careful avoidance of experiments people would have run if they were cheap. This is the unsexy, boring-looking, enormously consequential thing about branchable VMs: they don't make the doing faster. They make the *setup* free. And in any workflow where the doing is a tenth of the effort, making the other ninety percent free is a step-function improvement in what a team can get done in a week. The inside joke of building this kind of primitive is that the first reaction is always "okay, cool, why do I need that?" and the second reaction, a month later, is "oh, I can't work without that anymore." That's how it goes with primitives that remove costs people have stopped seeing. ## Further reading The same problem, applied to autonomous agents: context isn't a message array, and throwing it away every restart is catastrophic. What debugging looks like when commits are restoration points and branches are experiment tracks. The current commands for remote computers and snapshots. The stateless remote-computer model behind the primitive. # Every infrastructure primitive is secretly a way to avoid paying a cost Source: https://docs.vers.sh/essays/every-primitive-avoids-a-cost Indexes, caches, CDNs, fork(), git, containers — every durable primitive you can name started as someone's answer to a recurring cost. The frame is useful because it predicts where the next primitives are coming from. Here is a frame for thinking about infrastructure that, once you have it, is hard to put down: **every durable primitive you can name was someone's answer to a cost being paid over and over**. The primitive didn't make the work faster. It made some recurring cost go away, or made it payable once instead of many times, and the savings were large enough that nobody wanted to go back. This frame is useful for two reasons. It explains why certain primitives stuck and others didn't. And it predicts, roughly, where the next ones are coming from — because it tells you what to look for. ## The pattern Pick any primitive that became part of the standard infrastructure toolkit. Ask: *what cost was being paid before this primitive existed?* The answer is always concrete, always something programmers were doing by hand, and always something the primitive made go away or made marginal. **Indexes.** Before indexes, every query against a table scanned the table. The cost was linear in the table size, paid per query. An index pays a bounded cost at write time to amortize scans at read time. The recurring cost — the scan — goes away. What's left is a small, predictable lookup. **Caches.** Before caches, every derived value was recomputed each time it was needed. The cost was in the recomputation, paid per read. A cache pays a one-time cost to store the result and makes every subsequent read cheap. The primitive didn't make the computation faster; it made the *repeated* computation go away. **CDNs.** Before CDNs, every request for a static asset made a round trip to the origin server. The cost was in the latency and bandwidth of that round trip, paid per request. A CDN pays a one-time replication cost and turns every subsequent request into a short local hop. The work didn't get faster; the *recurring cost of doing it far away* got paid once, per edge location, forever. **Virtual memory.** Before virtual memory, programs had to manage physical RAM directly, paging things in and out with explicit system calls. The cost was in the programmer's time to reason about memory locations and in the bugs that arose when they got it wrong. Virtual memory pays a modest per-access tax (a TLB lookup) and in exchange makes the entire class of "manage your own physical memory" work go away. **Garbage collection.** Before GC, every program had to track every allocation and free it by hand. The cost was in programmer attention and in the bugs that arose from forgetting. GC pays runtime overhead and sporadic pause time to make the tracking go away. **Connection pools.** Before connection pools, every database query opened a new TCP connection and negotiated a new TLS handshake. The cost was tens of milliseconds of handshake per query, paid per query. A pool pays a one-time handshake cost and reuses the connection. Queries went from "network round-trip plus handshake plus result" to "result," and databases suddenly seemed dramatically faster. **Package managers.** Before package managers, every programmer installing a dependency had to find the source, download it, build it, resolve its transitive dependencies by reading its README. The cost was in each installation, paid per dependency per project. A package manager pays a one-time cost to index the world's code and turns installation into a single command. **DNS.** Before DNS, every networked program had to know the IP addresses of the machines it talked to. The cost was in human memory and in the changes needed every time an address changed. DNS pays a small query tax per lookup to make the human-readable name work, and the cost of maintaining a mental map of IP addresses goes away. **JIT compilation.** Before JIT, interpreted languages paid the cost of interpretation on every instruction. JIT pays a compile-time cost (both in wall-time and in memory) to amortize the per-instruction cost of interpretation. The work the CPU does isn't faster; the *number of times you do that work per program execution* drops by orders of magnitude. **`fork()`.** Before `fork()`, creating a new process meant specifying a program and getting a fresh process built around it. The cost was in re-establishing all the state the new process wanted — file descriptors, environment, memory. `fork()` makes the new process start with all the state the parent had, paying a (with copy-on-write, near-zero) duplication cost in exchange for the entire class of "set up the child's context from scratch" work going away. **`git`.** Before git, collaboration meant mailing patches, dealing with merge tooling that lost history, working in lockstep with centralized servers. The cost was in coordination, paid per contribution. Git pays a per-object content-addressing cost and in exchange makes offline work, untrusted replication, and cheap branching all free. The actual code didn't get better; the *cost of working on it with other people* collapsed. **Container images.** Before container images, dev/prod drift was endless and every new deployment re-derived the environment from a mix of scripts, configuration management, and apology emails. Container images pay a build-time cost and turn "recreate this environment" into "pull and run this hash." The deployment isn't faster; the *reconstruction* is amortized. **Kubernetes.** Before orchestration, running a service at scale meant hand-rolled scripts to schedule containers across nodes, detect failures, restart crashed workers, balance load. The cost was in operational labor, paid continuously. Orchestrators pay with configuration complexity and a control-plane overhead to make the per-deployment labor go away. Pick any of them. Ask what cost they eliminated. The story is always the same shape: *a recurring cost you were paying turned into a one-time cost you pay at a different moment, and the difference is the savings compounded over every use of the primitive forever*. ## Why this frame is useful The frame is useful in two directions. Looking backward, it explains *why certain primitives stuck and others didn't*. Primitives that successfully eliminated recurring costs became indispensable; primitives that just made existing work slightly more convenient faded. The infrastructure graveyard is full of ideas that were clever but didn't make a cost go away. The winners all have the same shape. Looking forward, it predicts *where the next primitive is likely to come from*. The recipe is: 1. Identify a cost being paid over and over in the current era of infrastructure. 2. Check whether someone has named it yet — if the cost is unnamed, it's easier to not see. 3. Ask what it would take to pay that cost once instead of every time. 4. That answer, made cheap enough to be a hot-path primitive, is the next thing. Most proposed infrastructure primitives fail this test. They *add* a tool without making a cost go away. They make the same work possible through a different syntax, or provide a marginally more ergonomic interface. Those tools exist for years, have a loyal user base, and never go mainstream, because mainstream adoption requires killing a cost most people didn't realize they were paying. The primitives that do succeed often feel, in hindsight, obvious. *Of course* you should index a table you query repeatedly. *Of course* you should cache a computation you do many times. *Of course* you should reuse connections. Each of these was a research paper and then a decade of adoption. The obviousness is retrospective. What was really happening was that a cost had grown large enough that someone got fed up, built the workaround, and discovered the workaround was general. ## The tell One tell of an about-to-emerge primitive: you can find developers who have built custom, fragile, per-project workarounds for the same cost. * Before package managers: every project had a `./install.sh` that downloaded the world. * Before CI/CD: every team had a developer who was the "build person." * Before container images: every company had a runbook for re-provisioning servers. * Before version control: every programmer had a `code-backup-2004-11-13/` directory. The per-project workaround is the sign that a real primitive is needed. The workaround is always janky, always slightly different per-project, always costing more to maintain than anyone admits. At some point someone consolidates the workarounds into a general primitive, writes it cleanly, and the primitive spreads because it's strictly better than the hand-rolled version every team was already paying to maintain. If you're looking for the next primitives, look at what sophisticated teams are building for themselves and hating. The things they build ad hoc because nothing off the shelf does it. That's where the next primitives are incubating. ## The Vers-shaped cost Here's the cost this essay is, if you're being honest, designed to make you notice: **rebuilding state**. Nobody logs the time they spent re-provisioning a test environment, reseeding a database, re-warming a cache, re-navigating an app to the page where a bug reproduces. It's not on the calendar. It's in the cracks between the scheduled work. For most engineers, it's thirty to sixty percent of the week. For debugging-heavy work, it's more. For teams building agent systems, where every worker accumulates filesystem state, running processes, loaded models, and session tokens, it's the *dominant* cost and nobody has named it yet. The workaround, today, is ad hoc: custom scripts to spin up dev environments, seed scripts to populate databases, golden AMIs baked by hand, careful runbooks to recreate production state. Every sophisticated team has some version of this, built for themselves, maintained at a cost nobody wants to talk about. The pattern is identical to "before package managers, every project had its own install script." A primitive that lets you *branch state in microseconds, commit it as a content-addressable snapshot, and restore it anywhere* is the thing that consolidates all those workarounds. It's the "of course" that will be obvious in retrospect. The state isn't something you rebuild; it's a value you branch from, like a git commit. The recurring cost of reconstruction goes away. That's what Vers is. Not because Vers is an especially clever VM hypervisor — there are many good VM hypervisors — but because the primitive it exposes eliminates a cost that infrastructure has been quietly accumulating for twenty years. Once you have it, the shape of the work changes. You stop budgeting for the setup tax. You stop choosing workflows around the fact that reproduction is expensive. You do the things you wouldn't have done before, because the cost was too high to justify. That's how primitives work. That's the whole pattern. The interesting question isn't whether branchable state is useful — it obviously is — it's what else you'll be able to do with it once it's cheap enough to use without thinking. ## What's next The frame gives you a way to evaluate new infrastructure proposals. When you see one, ask: 1. What recurring cost does this eliminate? 2. Is the cost being paid frequently enough that eliminating it matters? 3. Is the primitive cheap enough that it can live in the hot path, not just in occasional batch jobs? If the answer to (1) is vague, the primitive won't stick. If the answer to (2) is "only in edge cases," the primitive has a narrow user base forever. If the answer to (3) is "no," the primitive is interesting but designed around rather than designed with. Every primitive in the standard toolkit answers yes to all three. The ones that became infrastructure rather than tools answered yes decisively. The ones that stayed niche usually answered no to (3) — they were conceptually right but too slow to use casually. This is the test we should be applying to the infrastructure we build and the infrastructure we adopt. The primitives that matter aren't the ones that make things marginally more convenient. They're the ones that kill costs we'd stopped noticing we were paying. ## Further reading The canonical example: a primitive that killed the "rebuild the process state from scratch" cost and became the shape of Unix. The other side of this essay: the concrete, per-engineer cost that branchable state kills. Git as the consolidation of per-project version-control hacks into a general primitive. The user-facing model of the primitive discussed here. # Essays Source: https://docs.vers.sh/essays/overview Long-form pieces on branchable compute, stateful infrastructure, and why the shape of the work changes when forking a live machine is cheap. Reference docs describe what *is*. These essays argue for what *should be*. They're the intellectual foundation of Vers — long-form pieces about the problems, patterns, and primitives that make branchable compute interesting. Each essay stands on its own. They're ordered roughly by which question you're asking. Stateless compute was correct for request/response. It is wrong for agents. Context isn't a message array — it's everything the worker has built up, and statelessness throws all of it away. Most engineering time is not spent doing work. It's spent re-creating the conditions under which work can be done. Once you see the pattern, you can't unsee it. Git's quiet revolution was that it made code identity a function of content, not location. Apply the same idea to running machines and thirty years of problems dissolve at once. Most bugs are hard because by the time you notice them, the state that produced them is gone. Make every commit a restoration point and debugging stops looking like archaeology. fork() is fifty-five years old. It's the primitive that made Unix generative, the one every serious system still borrows from, and the one whose logical conclusion is to branch whole running machines. Indexes, caches, CDNs, fork(), git, containers — every durable primitive started as someone's answer to a recurring cost. The frame predicts where the next ones are coming from. ## Why these live in docs If you're building on Vers, you're working with a primitive most infrastructure doesn't have yet: sub-millisecond branchable state. The core primitive is small, but the *shape of what you can build* changes in ways that aren't obvious from the reference pages. These essays are an attempt to make that shape explicit. They're the arguments for why the primitive matters, the patterns it enables, and the problems it solves that were invisible while the cost of solving them was too high. Read one, all of them, or none. The reference docs stand on their own. The essays are for when you want the *why*. # A short history of fork() Source: https://docs.vers.sh/essays/short-history-of-fork fork() is fifty-five years old. It's the primitive that made Unix generative, the one every serious system still borrows from, and the one whose logical conclusion is to branch whole running machines. In November 1969, Ken Thompson was writing the first version of Unix on a PDP-7 at Bell Labs. He needed a way for one process to create another. The obvious design — common in operating systems of the era — was a call that took the name of a program and started it running: `newProc("/bin/sh")`. Multics had something like this. DEC's systems had something like this. Nearly every operating system written before or around 1969 had something like this. Thompson and Dennis Ritchie chose something different, and that choice turned out to be the one that made Unix generative in the way it was. ## What came before Before Unix, the dominant model for creating a new process was *specify the program, create the process*. You pointed the OS at an executable, asked for it to be loaded, and the OS returned a handle to a process that had been built from scratch around that program. The new process had no relationship to the creator beyond being allocated by the same operating system. This model has an obvious appeal: it's clean. The parent process says what it wants (a running instance of program X) and the OS does the thing. It's also restrictive in ways that take a while to notice. You can't pass in-memory state from parent to child. You can't hand over open file handles or network connections without explicit OS primitives for each one. You can't easily set up a child's environment to be a modified version of the parent's — everything has to be specified up front. Pipelines, in particular, are awkward: to run `A | B`, you have to plumb a pipe through the OS-level process-creation API for both programs. Multics had `new_proc`. RSTS/E had `RUN`. VM/CMS had `EXEC`. They all worked the same way: you tell the OS what to run, it builds a fresh process around the executable. Reasonable, and limiting. ## The Unix move Thompson's `fork()` inverted the order. Instead of creating a new process by specifying a program, `fork()` *duplicated the calling process* — same memory, same file descriptors, same execution state — and returned twice. The parent got the child's process ID; the child got zero. From that moment the two processes ran independently, sharing nothing but the state they had at the moment of the fork. On its own, `fork()` just gave you two identical processes. That's not useful. The second piece of the design was `exec()`, which replaced the running program's image with a different executable while keeping the process's identity, file descriptors, and environment. The typical Unix pattern became: ```c theme={"theme":{"light":"min-light","dark":"min-dark"}} pid_t pid = fork(); if (pid == 0) { /* We're the child. Replace ourselves with a different program. */ execvp("/bin/sh", argv); } else { /* We're the parent. Maybe wait for the child; maybe not. */ } ``` Two primitives, separable. `fork()` to duplicate; `exec()` to replace. Most uses want both; some only want `fork()` (when the child's job is so small it doesn't need its own program); the separation is what made the design generative. What did you get from this design that the `newProc(program)` style didn't give you? **Inherited file descriptors.** The child begins life holding every file and socket the parent had open at the fork. This is how Unix pipelines work — the shell forks, sets up the pipe, forks again for the second command, and the file descriptors flow naturally because the child inherited them. **Inherited environment.** Environment variables, current working directory, signal handlers, resource limits — all inherited. The child doesn't have to be told; it already has them. **Inherited address space.** Every byte of the parent's memory is available to the child at the fork. The child can read the parent's loaded data structures, the parent's computed state, the parent's open database connection. Then, if it wants a completely fresh start, it calls `exec()` and replaces everything. If it doesn't, it keeps the parent's state and diverges. **A natural shape for servers.** A server that wants one worker per connection does `accept()` in a loop, `fork()` on each accept, hand the connection to the child. The child inherits the connection file descriptor; the parent goes back to accepting. This is the classic pre-`epoll` server structure and it's cleanly expressible because `fork()` hands off state. You can see why Unix's authors didn't feel the need to add many other primitives for process communication: if you control what state is present at the fork, you've already communicated whatever you needed to communicate. ## What copy-on-write added The original `fork()` had a performance problem. Duplicating a process meant duplicating its entire address space — every page of memory, copied. For a large process this was slow, and it was often wasted work when the child immediately called `exec()` and discarded the duplicated memory. Berkeley's response in the early 80s was `vfork()` — a variant that *didn't* copy the address space, on the promise that the child would `exec()` before touching any of it. `vfork()` worked but was brittle, and any programmer who violated the "don't touch memory before exec" contract got mysterious corruption. The better answer was copy-on-write. Introduced roughly concurrently in Mach and in later Unixes, copy-on-write made `fork()` cheap: instead of duplicating pages eagerly, the OS marked them read-only and shared between parent and child. A page was physically copied only when one of the two processes wrote to it. In the common case where the child quickly called `exec()`, almost no pages were ever copied at all. With copy-on-write, `fork()` became a genuinely cheap primitive — fast enough to run in tight loops, which is exactly the shape of the patterns people had already developed (shell pipelines, one-fork-per-accept servers). The mental model didn't change; the performance finally caught up to the model. ## Linux generalizes it When Linus Torvalds was implementing Linux in the early 1990s, he needed `fork()` for Unix compatibility but he also wanted threads. Threads and forks are related — a thread is basically a process that shares address space with its peers — and Linux's answer was `clone()`, a generalized version of `fork()` that took flags specifying *exactly what* the child should share with the parent. `CLONE_VM` to share memory (a thread). `CLONE_FILES` to share file descriptors. `CLONE_SIGHAND` to share signal handlers. Combine flags to get different flavors of child process, from "heavyweight process" (no sharing, like `fork()`) to "lightweight thread" (share everything, like pthreads). Linux's `clone()` is still the basis for thread creation on Linux today, and it's the underlying primitive behind container namespacing — `CLONE_NEWPID` creates a child in a new PID namespace, `CLONE_NEWNS` in a new mount namespace, and so on. Every Linux container you've ever run was created by `clone()`, under the hood. The design traces back to Thompson's 1969 choice: instead of `newProc(program)`, duplicate the running process and then optionally diverge. ## The critique `fork()` is also controversial in its details. The 2019 HotOS paper *"A fork() in the road"* by Baumann, Appavoo, Krieger, and Roscoe argues that `fork()` has become incompatible with modern systems: it doesn't compose with threads (forking a multithreaded process is a minefield), it doesn't compose with shared memory segments (children inherit the mappings but the semantics get weird), it doesn't compose with accelerators (GPU state, locked memory), it doesn't compose with kernel-managed resources that have server-side state (tracked by pid, which the child now duplicates). The authors suggest that the Unix community should move past `fork()` toward more explicit primitives. The paper is right about the details. `fork()` is a hack in many specific ways, and if we were designing Unix from scratch today we probably wouldn't reach for its exact shape. But the *idea* — duplicate a running state, diverge — has been so generative that it keeps getting re-invented at different altitudes: * Process-level: `fork()` / `clone()`. * Filesystem-level: Btrfs and ZFS snapshots. "Fork this filesystem." Copy-on-write at the block level. * Image-level: Docker image layers. "Fork this base image." Copy-on-write at the filesystem layer. * Container-level: `docker commit`. "Fork this running container into an image." * Language-level: Erlang's `spawn` (actors inherit nothing, but the pattern of "spawn, diverge, communicate" is the same). * VM-level, historically: VMware snapshots, KVM live migration. Expensive, rarely used as a hot-path primitive, but conceptually the same move. Every one of these is the same act at a different granularity. The complaint in the HotOS paper isn't that `fork()`'s idea is wrong; it's that `fork()`'s *specific implementation* has accreted enough edge cases that it doesn't scale cleanly to modern systems. The idea keeps getting re-invented because the idea is correct. ## What's missing at the top of the stack Notice the pattern in the list above. Process-level `fork()` is cheap (microseconds on Linux). Filesystem-level snapshots are cheap (hundreds of milliseconds). Container image layering is cheap (milliseconds to seconds, with layer caching). And then at the VM level — the top of the granularity ladder, where you have a whole running machine to duplicate — the primitive has historically been *expensive*. VMware snapshots take seconds to tens of seconds. Cloud provider snapshots take tens of seconds to minutes, and their restore times are similar. KVM live migration is fast but its target isn't "two running copies" — it's "move one running VM to another host." The pattern of "fork a running VM, diverge, keep both" has been available but rarely practical, because the latency priced it out of the hot path. This is exactly the moment in the `fork()` story where vfork/CoW arrived — the primitive existed but was too slow to use widely, so people designed around its absence. The unlock happens when the latency drops by enough orders of magnitude that you stop designing around it and start designing with it. ## Vers in the lineage Vers is `fork()` at the VM granularity, with a latency low enough that the shape of the software changes. Branch p50 is 258.3µs; branch p99 is 329.2µs. That's the same order of magnitude as process-level `fork()` on Linux. The primitive is finally fast enough, at whole-machine scale, to live in the same tight loops that process-level `fork()` has lived in for fifty-five years. When that shift happens, you get the same generative effects: * **One-fork-per-task becomes cheap.** Just as Unix servers could fork per connection, agent infrastructure can fork per trajectory. Each task gets an isolated world and the cost of creating that world is negligible. * **Pipelines at machine granularity.** The shell's `A | B | C` was possible because `fork()` was cheap enough to chain. Whole-machine pipelines — branch, run step, commit, branch from that commit, run next step — become expressible when VM branching sits at microsecond latency. * **Inheritance of state is the delivery mechanism.** In Unix, you communicate with a child by setting up state before the fork. In Vers, you communicate with a branched VM by reaching a state before the branch. Same move, bigger unit. * **Copy-on-write at the memory-page level** does the same job for running machines that Berkeley's CoW did for processes: the fork is physically cheap because divergent pages are only copied when actually written. You can read the history of `fork()` as an accidental design by Thompson that turned out to be the most generative primitive in the history of operating systems. Or you can read it as an inevitable choice: the shape that preserves the most state across process creation *must* win, because state is what makes processes useful, and rebuilding state from scratch is always the cost you're trying to avoid. Either reading lands in the same place. The primitive wasn't done when Thompson stopped typing in 1970. It had more altitudes to reach. ## Further reading The git equivalent of this essay: another primitive that turned out to have altitudes left to reach. Why `fork()` stuck: it made the "rebuild the state from scratch" cost go away. How the whole-machine `fork()` idea appears in the current product. The current computer and snapshot command surface. # Why stateless compute ran out of runway for agents Source: https://docs.vers.sh/essays/stateless-compute-ran-out Stateless compute was the right answer for request/response. It is the wrong answer for agents. Context is not a message array — it's everything the worker has built up, and statelessness throws all of it away. Stateless compute is one of the best-aged ideas in infrastructure. The horizontally-scaled web, container orchestration, serverless, edge workers — all of it rests on the same assumption: *a worker has no memory of what came before*. Receive a request, produce a response, terminate. Anything that survives between requests goes in a database, a cache, or a queue. That assumption was right for twenty years. It is wrong for agents. ## What statelessness actually assumes The stateless worker is a useful fiction. It lets you run a thousand of them in parallel, replace any of them without coordination, and scale horizontally without distributed-systems pain. But it only works because of an implicit bargain: *the worker's job is small enough and self-contained enough that its context fits in the request*. A web handler processing a POST has its context right there in the body. A Kubernetes pod running a Go service has its context in the config it booted with. A Lambda handler gets its context as the event payload. In every case, the worker needs to know nothing about what happened yesterday, what it did an hour ago, or what's in `/tmp`. Whatever's needed arrives with the request. The operating assumption is that **context is data**. Data can be moved. Data can be serialized. Data goes in a database. ## Then the workers got bigger jobs Agents broke the bargain. An agent running a coding task or an autonomous research job isn't processing a request — it's doing *work*. For a long time. With accumulating state. The job takes twenty minutes, an hour, sometimes longer. Over that span, the worker: * Clones three repos * Runs `npm install` and waits eight minutes * Seeds a local Postgres * Opens four long-lived shells * Edits forty files in `/workspace` * Spawns a dev server bound to `:3000` * Logs in to three OAuth-authenticated services * Loads a 7GB model into memory * Caches tokenizer output * Pre-computes embeddings for a 400-page PDF * JIT-compiles hot paths of a Python library * Has a `node_modules/` directory it's been warming for half an hour At minute 45, it crashes. Or gets preempted. Or its container gets rescheduled. Or the cluster autoscaler decides to recycle its pod. Or the user cancels the task and asks to "pick up from here" tomorrow. What's the context to restore? ## Context is not a message array When an agent framework tries to "checkpoint" an agent, it usually serializes the message history — the transcript of prompts and tool calls — and calls it done. That's the smallest possible definition of context, and it's the only one that fits in the stateless-compute model. The rest of the context, the context that actually mattered, is gone: | | | | - | - | | **Filesystem state** | All the files the agent created, edited, downloaded, cached. Forty modified source files. A seeded database. A built binary. | | **Process state** | The running dev server. The background `tsc --watch`. The shell with the half-typed command. The language server holding its symbol index. | | **Network state** | Open sockets with keep-alives still alive. OAuth refresh tokens the agent negotiated. Rate-limit windows it's tracking. A WebSocket to a collaboration tool. | | **Memory state** | The model weights it spent three minutes loading. The LRU cache warmed by fifty tool calls. The JIT-compiled hot path. The in-process vector store. | | **Environmental state** | The packages the agent installed to fix a dependency error. The env vars it exported. The DNS override it needed for a flaky service. | | **Collaboration state** | The branch it pushed to, the pull request it opened, the review comment it was half-way through writing. | Restore the message array and you've restored the *description* of the work. The work itself — every byte the agent actually produced, every pid still running, every cache that wasn't cold — is lost. The next run starts over. For a chat assistant handling one turn at a time, this is fine. For an agent ten minutes into a compound task, it's catastrophic. It means the checkpoint is theater: a thing you can load and feel like you're resuming from, while actually paying the full provisioning cost again, just with the transcript preloaded. ## The two bad workarounds Everyone building serious agent systems has hit this wall. There are two bad workarounds everyone tries. ### 1. Make the agent itself stateless Pretend the worker has no context between tool calls. Serialize everything the agent needs into the next prompt. Recompute the world before every operation. This works in demos. It does not work in production, because: * Every tool call pays the provisioning cost again. * Compounding work (install, then build, then test) serializes the whole pipeline into each step's context window. * The agent loses the cheap check that's fundamental to engineering: *is the thing I built still there?* * Long-running side effects (background processes, open connections, filesystem caches) can't be expressed in a prompt at all. This is the "recursive cron job" school of agent design. It reduces a 45-minute job to 45 sequential one-minute jobs, each paying the full setup tax. Total wall time explodes, and you're still fragile — the agent can't inherit anything useful from its prior self. ### 2. Keep the agent running forever Put the worker on a long-lived VM or container and never kill it. Now the state is real, because the process holding it never dies. But: * You've given up horizontal scale. The worker can only do one thing at a time, on one machine. * Forking into parallel explorations is impossible. The state is a single timeline on a single host. * Recovery from crashes is manual. When the process dies, its state dies. * Rescheduling is impossible. The worker is pinned to its node. * Snapshots are possible in theory but cost seconds-to-minutes in practice, so they happen rarely if ever. This is the "big expensive VM" school. It accepts the statelessness is the wrong answer and doubles down on statefulness without making statefulness *cheap*. You end up with one agent per VM, zero parallelism, and panic when the VM crashes. ## What agents actually need The primitive agents need isn't statelessness and it isn't a long-lived single VM. It's something that doesn't have a common name yet: * **Branchable state.** At any point, an agent should be able to fork its current state into N parallel continuations, try them all, keep the winners, discard the rest. * **Committable state.** At any milestone, the worker should be able to snapshot its full state — filesystem, memory, processes, sockets — into an immutable, content-addressable reference it can return to. * **Restorable state.** That reference should boot a fresh worker to byte-identical state anywhere, anytime, in microseconds. * **Cheap.** All of this in latencies that allow it *inside* the hot path, not as a batch operation. If checkpointing takes thirty seconds, you'll checkpoint once an hour. If it takes 258µs, you'll checkpoint on every tool call. With those primitives, the failure modes of the two bad workarounds disappear. You don't pay provisioning cost per tool call — you inherit state from the previous one. You don't pin one agent to one host — you branch across hosts. You don't panic when a worker crashes — you restore from the last commit. The reason those primitives aren't standard yet is that they were too expensive to build until recently. A VM fork that takes two seconds is novel but useless inside a tool-call loop. A VM fork that takes 258µs is a primitive — and the software you can build on top of it looks nothing like what stateless compute enables. ## What this implies If you're building agent infrastructure today, you're going to hit this wall. Every serious team has. The question isn't whether context exceeds the message array — it does, always — it's what you do when you notice. The reflex is to work around: serialize more into prompts, sit on longer-lived VMs, build bespoke snapshotting on top of whatever hypervisor you're using. These are reasonable tactics and they take you further than you'd expect. They do not take you all the way. At some point the cost of rebuilding the context from scratch, or the cost of pinning your agent to a single long-lived node, becomes the dominant cost in your system. At that point you need a different primitive. That's what Vers is. Branching, committing, restoring a running VM in microseconds — because the thing holding your agent's context isn't a message history, it's an entire machine, and the only way to preserve it honestly is to preserve the machine itself. Statelessness was correct. It was correct for a kind of worker that almost always fit the bargain. Agents don't fit. Once you accept that, the shape of the right infrastructure clarifies quickly. ## Further reading Most engineering time is not spent doing work — it's spent re-creating the conditions under which work can be done. The current computer and snapshot command surface. The current remote-computer model. Start a computer and use the current CLI end to end. # Time-travel debugging, productionized Source: https://docs.vers.sh/essays/time-travel-debugging Most bugs are hard because by the time you notice them, the state that produced them is gone. Make every commit a restoration point and branches a way to explore alternate pasts, and debugging stops looking like archaeology. Here is the typical shape of a production bug investigation. A user reports something weird. By the time the report reaches engineering, it's six hours old. The logs from the moment of the bug have rolled off. The user's session is gone. The container that handled their request was recycled at the next deploy. The data they triggered the bug on may have been modified by them or by a subsequent job. You have their description, some log lines, maybe a stack trace, and a very strong hunch that nobody will reproduce this in a test environment. So you do archaeology. You read the code. You stare at the logs you have. You try to imagine the chain of events that produced the symptoms. You write a one-off script to try to reconstruct an approximation of the state. You fail, twice. Eventually you either find the bug by reasoning alone or you shrug and close the ticket because it hasn't recurred. This is how almost all serious production debugging is done. It is fundamentally a detective activity performed on corpses. It is expensive, it is error-prone, and it is entirely downstream of one property of our infrastructure: **the state that produced the bug isn't preserved anywhere, so you can't go back to it**. Now imagine you could. That's what this essay is about. ## What "time-travel debugging" has meant so far The phrase has an existing meaning in the trade. Tools like [rr](https://rr-project.org), [Pernosco](https://pernos.co/), and Microsoft's Time-Travel-Debugging in WinDbg record a deterministic replay of a single process and let you step backward through its execution. Fantastic technology; I use rr regularly. These tools are limited in exactly one way: they have to be turned on *before* the bug happens. You run your test under rr, the bug reproduces, you step backward through it. If you didn't start recording, the replay doesn't exist. And since the overhead is non-trivial (5–10%, worse for some workloads), you don't run every production service under rr all the time. Deterministic replay is an incredible technique for the bugs you can reproduce, where "reproduce" means "trigger a second time in a controlled setting." It does not help with the bug that already happened in production while you weren't looking. That's the class of bug time-travel-debugging-as-a-production-primitive would help with. ## What changes when every commit is a restoration point Imagine your production environment takes a commit every N seconds, or on every deploy, or on every significant state change — whatever fits the workload. A commit here doesn't mean a source-code commit; it means a [content-addressable snapshot](/essays/content-addressable-everything) of the full VM: memory, filesystem, open sockets, running processes, in-flight requests. Call this rate of commits *C*. What does debugging look like at different values of C? ### C = 0: today, for most services Never commit. State is lost the moment it changes. Debugging is detective work. ### C = once per deploy Committing on deploy is basically a blue-green rollout. When a deploy breaks, you restore the previous commit. Useful, but the window of "this commit ran" is hours to days. Too wide to pin a specific bug. ### C = once per minute Now you have a sliding window of the last hundred minutes of production. When a bug report arrives, you restore the commit closest to the user's timestamp. You're looking at the actual machine, memory and all, as it was. Logs still exist because the processes that wrote them are still running. Caches still have entries. Open sockets are still open (well, almost — see below). This is a big jump. At C=1/min, "reproduce the bug in prod state" stops being a research project. ### C = every request Now the debugging primitive isn't "restore a minute-accurate snapshot of prod." It's "restore the exact state at the moment this user's request was handled." You can step backward through the state the request saw. You can run the request again, against the committed state, and see what happened. You can branch the commit, change one variable, and see what *would have* happened. ### C = every tool call (for agents) For long-running agents, a commit per tool call is the natural rate. Every time the agent makes a decision, the state before and after is committed. When the agent ends up somewhere weird, you walk back through the commit graph to find the decision that took it there, restore to before it, branch, try a different decision, see the alternate history. What's blocking C from being high isn't philosophical. It's the latency and storage cost of a commit. If each commit takes two seconds and a gigabyte of dedicated storage, committing per-request is absurd. If each commit takes 258µs and deduplicates aggressively against the base image, committing per-request is routine. ## The debugging workflows that become possible ### Bisect the state, not just the code `git bisect` lets you find the commit that introduced a bug by searching through source-code history. It is one of the most powerful debugging tools in common use, and it works because commits are cheap, restorable, and content-addressed. Now imagine a state-bisecting tool: walk through the production state commits, restore each one, run a predicate against it, and converge on the commit where the bug first appeared. The code might not have changed between commits. The data might have. Or the load pattern. Or the feature-flag configuration. Bisecting on state — not source — finds bugs that have no source-code origin. ### Branch from the moment of failure The classic debugging flow is "reproduce the bug, then try to fix it." The second half — trying to fix it — is what chews through reproductions. Each fix attempt modifies the state, and if the fix is wrong, you need to get back to the buggy state to try again. With commits and branches, the loop looks different: 1. Restore the commit at the moment of failure. 2. Branch it. (This is the "scratch environment" where you try things.) 3. Try a fix. Did it work? If yes, note it; if no, discard the branch. 4. Return to the commit-of-failure. Branch again. Try another fix. 5. Run five fix attempts against the same exact state without ever having to rebuild it. The iteration speed on this loop is bounded by how fast you can branch, which is 258µs, which is effectively instant. Debugging stops feeling like a chain of expensive reproduction attempts and starts feeling like exploring a garden of alternate timelines. ### Share the bug state You're working on a bug. Your colleague has an idea. Today you describe the state to them, they try to reproduce it locally, they get a different state, they offer a fix based on the state they got, you try it against yours, it doesn't work. The chain of translation has lost signal at every step. With commits: you send them the commit ID. They restore. They're debugging the *exact* same machine you're debugging. Not a re-creation. The original. The conversation isn't about "what state are you in" — both of you are in the same state, by hash. ### A/B the fix in the state that produced the bug Here's the most interesting one. You have a fix. Before shipping, you want to know if the fix actually addresses the bug. Today you ship it to staging and hope. Or you write a test that approximates the bug's conditions and validate against that. With committed state, you apply the fix to a branch of the commit-of-failure and observe directly whether the bug still reproduces. You're A/B-testing in the literal state that produced the bug, not in a synthetic approximation. If the fix doesn't address it, you know instantly, because the bug is *right there*, not hiding behind a failed reproduction. ### Regression tests that are machines, not scripts The test suite entry for a bug fix usually looks like a script: set up conditions, trigger the bug, assert the fix worked. Writing one is expensive (you're re-implementing the bug's conditions in code), brittle (the conditions drift), and lossy (the real state had properties the script can't encode). With committed state, the regression test is: "restore commit `c1a2b3c4`, run the request, expect success." The conditions aren't simulated — they *are* the conditions, byte-identical. The test runs against an actual machine in the state that previously failed. No drift. No encoding loss. ## What the state is vs. what the logs say Logs are the current-state-of-the-art way we try to preserve enough of past state to debug later. And logs are limited in obvious ways: they capture what you thought to log, at the time you wrote the code. Anything you didn't think was worth logging is gone. Anything in memory is gone. The state of caches is gone. The exact timing of events relative to each other is often fuzzy. A committed VM is a lossless capture of state. Everything in memory. Every file. Every open socket's buffered bytes. The kernel's view of the process tree. Restoring a commit isn't "replaying the logs" — it's *booting the actual machine* as it was at the commit moment. The difference in debugging power is enormous. Logs tell you what you thought was important. A committed state lets you ask questions you didn't know to ask. "What was in the LRU cache?" Fine, restore, dump it. "What was the in-flight HTTP request's partial body?" Fine, look. "What state was the connection pool in?" Fine, inspect it. The thing that made this impractical was the cost of committing. If committing takes a second and a gigabyte, you don't commit continuously in production. You commit rarely. The logs remain your primary tool, because they're cheap-to-produce. Once commits are microseconds and dedup-heavy, the economics flip. Commit continuously. Use commits as the primary debugging artifact. Logs become summaries, not the source of truth. ## The obvious counterargument: storage If you commit production state every minute at a microsecond cost, where does the storage go? Two answers: 1. **Content-addressing dedups aggressively.** A commit that only changed a handful of memory pages from its parent stores only those pages; the rest are references to the parent's content. Consecutive commits that are mostly-unchanged are cheap. You don't store N copies of the VM; you store the divergence. 2. **Retention policies apply, obviously.** You don't keep every minute of commits forever. You keep the last hour at minute-granularity, the last day at hour-granularity, the last week at day-granularity, the last month at checkpoint-granularity. Same rolling-window logic you already use for logs and metrics. The difference is that each retained commit is orders of magnitude richer than a log line. ## Where this is and where it's going The loop described in this essay — commit-heavy production, branch-to-investigate, restore-to-reproduce — isn't theoretical. It works today with Vers for the workloads where the commits and branches are cheap enough. The class of bugs that are addressable this way grows as the commit rate you can afford in production grows. The interesting bet is that production debugging as detective-work-on-corpses is a temporary artifact of expensive state. Once the state is cheap to preserve, debugging stops being about reconstruction and starts being about *inspection*. We didn't reason our way to git bisect from first principles; cheap commits made it possible, and then someone noticed and wrote the tool. The same sequence will play out for production state. I expect that within a few years, production services that aren't routinely committing their state will look the same way services without structured logs look today: under-instrumented, under-auditable, debugging the hard way out of habit. The tooling hasn't caught up to the primitive yet, but the primitive is here. ## Further reading Why committing state is coherent at all — and how git's idea generalizes to running machines. The general case: most engineering time goes into reconstructing state. Debugging is one acute instance. The current commands for computers and snapshots. The stateless remote-computer model behind these workflows. # Static Web App (GitHub Import) Source: https://docs.vers.sh/examples/static-web-app-github Deploy a static web app to a Vers computer using GitHub import From the dashboard, click on the **New** button, then click **App**. Give your project a name, select **GitHub** as the source, pick your repo, and click **Create**. Once the computer is created, you'll see it on the dashboard with an **Active** status. Click the open link icon to view it in your browser. # Get your API key Source: https://docs.vers.sh/get-api-key Create an account and mint an API key from the Vers dashboard. Go to [vers.sh](https://vers.sh) and sign in. Select your organization, open **Settings**, then open **API Keys**. The **User-created** view shows keys for your CLI and integrations. The **System-managed** view lets you inspect credentials that Vers creates for projects, launches, and Buddy tasks, including their purpose and expiration. Vers manages those credentials automatically; use the corresponding project or session controls to manage the work they belong to. System-managed credentials show only active keys by default. Choose **History** in the status filter to inspect expired or revoked keys. Vers removes these credentials after 30 days unless a resource or audit record still references them. Cleanup does not delete their VMs, commits, or other resources. Select **Create a new API key**, give the key a descriptive name, and copy it when it is shown. Treat the value as a password: do not paste it into tickets, logs, screenshots, or commands that will remain in shell history. The CLI does not have a `login` command. Configure the key with the `VERS_API_KEY` environment variable or the private `~/.versrc` file described in [Installation](/installation#authentication). # Installation Source: https://docs.vers.sh/installation Install the Vers CLI on macOS or Linux and configure an API key. ## Requirements Linux or macOS on x86-64 or ARM64, with `curl` and either `sha256sum` or `shasum`. ## Install the CLI These instructions target CLI v0.2.0, which is not yet published. Until then, the installer provides an earlier release with different commands. ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL https://vers.sh/api/install | sh vers --version ``` The default destination is `/usr/local/bin/vers`. To install without elevated permissions, use a directory you own: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} mkdir -p "$HOME/.local/bin" curl -fsSL https://vers.sh/api/install \ | VERS_INSTALL_DIR="$HOME/.local/bin" sh export PATH="$HOME/.local/bin:$PATH" ``` Add that `PATH` setting to your shell startup file. The installer uses the latest stable release; set `VERS_VERSION` to pin a published version. ### Manual download Immutable versioned artifacts live at `https://releases.vers.sh/cli/v/`: | Platform | Asset | | - | - | | Linux x86-64 | `vers-linux-amd64` | | Linux ARM64 | `vers-linux-arm64` | | macOS Intel | `vers-darwin-amd64` | | macOS Apple Silicon | `vers-darwin-arm64` | Download the matching binary and adjacent `.sha256` file, verify the checksum, make the binary executable, and move it onto your `PATH`. ## Authentication Get an API key from the [Vers dashboard](/get-api-key) and set `VERS_API_KEY`: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} export VERS_API_KEY='' vers whoami ``` Use a secret manager or CI secret store for persistent use; keep the key out of source control and shell history. For file-based configuration or custom endpoints, see [Configuration](/cli-reference/overview#authentication-and-endpoint). If you use `~/.versrc`, restrict its permissions with `chmod 600 ~/.versrc`. Once `vers whoami` succeeds, follow the [Quickstart](/quickstart). ## Anonymous telemetry Anonymous command telemetry is enabled by default. To turn it off: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} vers telemetry disable ``` To disable it for one invocation without saving a preference: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} VERS_TELEMETRY=0 vers list ``` See [Telemetry](/cli-reference/overview#anonymous-telemetry) for the data collected and all preference commands. ## Updating Check for or install the latest stable release: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} vers upgrade --check-only vers upgrade ``` The install directory must be writable. Updates only run when requested. ## Troubleshooting ### Command not found Check that the install directory is on your `PATH`: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} command -v vers ``` ### Authentication failed Update `VERS_API_KEY` with a valid dashboard key and run `vers whoami` again. # What is Vers? Source: https://docs.vers.sh/overview Run Linux workloads on remote computers, then save and restore their state. Vers runs Linux workloads on remote computers. Start from a container image, run commands, and save a checkpoint to resume your work in a new computer. ## Run a workload Install the [CLI](/installation), then run a command: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} vers run --rm ubuntu:24.04 -- uname -a ``` This starts a computer, prints the command's output, and removes the computer when the workload exits. Use `--detach` to leave a workload running and return its computer ID. ## Work with a computer * Run a command with [`vers exec`](/cli-reference/overview#exec). * Open an interactive session with [`vers session`](/cli-reference/overview#session). * Save and resume work with [`vers checkpoint` and `vers restore`](/cli-reference/overview#checkpoint-and-restore). ## Start here Install and authenticate. Create a computer, run commands, and clean up. Commands, options, and automation behavior. # Quickstart Source: https://docs.vers.sh/quickstart Create a computer, run commands, write a file, and clean up. ## 1. Install and authenticate Follow [Installation](/installation), then check your connection: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} vers whoami ``` ## 2. Start a computer Start an Ubuntu computer with a workload that stays running. `--detach` returns its ID so you can use it in later commands: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} computer_id="$(vers run --detach --name quickstart ubuntu:24.04 -- sleep infinity)" ``` Startup continues in the background. Check the state and wait for `running` before continuing: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} vers get "$computer_id" ``` ## 3. Run a command Run a command inside that computer. Arguments after `--` go to the command: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} vers exec "$computer_id" -- uname -a ``` ## 4. Write and read a file ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} vers write "$computer_id" /tmp/hello.txt --content 'hello from Vers' vers cat "$computer_id" /tmp/hello.txt ``` ## 5. Clean up Delete the computer when you're done: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} vers delete "$computer_id" --yes ``` If deletion is still pending, follow the retry command printed by the CLI. ## Next steps See every supported command and option. Run an image from Docker, OrbStack, or Podman. # Api keys Source: https://docs.vers.sh/shell-auth/api-keys # Create API Key Create an API key for an authenticated user. Requires a verified SSH key pair and an active organization. ## Endpoint ``` POST /api/shell-auth/api-keys ``` ## Request ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "email": "user@example.com", "ssh_public_key": "ssh-ed25519 AAAA...", "label": "my-api-key", "org_name": "acme-corp" } ``` | Field | Type | Required | Description | | - | - | - | - | | `email` | string | Yes | User's email address | | `ssh_public_key` | string | Yes | Verified SSH public key | | `label` | string | Yes | Label for the API key (min 5 characters) | | `org_name` | string | No | Organization to create the key for. Defaults to user's first org. | ## Response ### Success (200) ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "success": true, "api_key": "", "api_key_id": "", "org_id": "", "org_name": "acme-corp" } ``` | Field | Description | | - | - | | `api_key` | The full API key — store it securely, it cannot be retrieved again | | `api_key_id` | The key's unique identifier | | `org_id` | The organization the key belongs to | | `org_name` | The organization name | ### Errors | Status | Error | Cause | | - | - | - | | 400 | Invalid request | Validation failed (email, key format, label too short) | | 400 | User has no organizations | User needs to complete signup first | | 401 | Invalid or unverified SSH key | SSH key not verified or not found | | 402 | Billing could not be verified | Temporary issue — retry later | | 404 | Organization not found | Specified `org_name` doesn't exist or user doesn't have access | ## Permissions API keys created via Shell Auth are granted: * **View Clusters** — read access to VMs * **Create Clusters** — create and manage VMs Keys are also visible in the Vers web dashboard under your organization's API Keys settings. ## Examples ### Create key for default org ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -X POST https://vers.sh/api/shell-auth/api-keys \ -H "Content-Type: application/json" \ -d '{ "email": "alice@company.com", "ssh_public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample...", "label": "dev-machine-key" }' ``` ### Create key for a specific org ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -X POST https://vers.sh/api/shell-auth/api-keys \ -H "Content-Type: application/json" \ -d '{ "email": "alice@company.com", "ssh_public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample...", "label": "acme-project-key", "org_name": "acme-corp" }' ``` ## Complete integration example ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # 1. Initiate auth curl -s -X POST https://vers.sh/api/shell-auth \ -H "Content-Type: application/json" \ -d '{"email":"alice@company.com","ssh_public_key":"ssh-ed25519 AAAA..."}' echo "Verification email sent. Check your inbox." # 2. Poll for verification while true; do VERIFY=$(curl -s -X POST https://vers.sh/api/shell-auth/verify-key \ -H "Content-Type: application/json" \ -d '{"email":"alice@company.com","ssh_public_key":"ssh-ed25519 AAAA..."}') VERIFIED=$(echo $VERIFY | jq -r '.verified') if [ "$VERIFIED" = "true" ]; then echo "Verified!" echo $VERIFY | jq '.orgs' # Show available orgs break fi sleep 3 done # 3. Create API key for selected org API_KEY=$(curl -s -X POST https://vers.sh/api/shell-auth/api-keys \ -H "Content-Type: application/json" \ -d '{ "email":"alice@company.com", "ssh_public_key":"ssh-ed25519 AAAA...", "label":"my-key", "org_name":"acme-corp" }' | jq -r '.api_key') echo "API key: $API_KEY" # Store this key securely — it won't be shown again ``` # Initiate Source: https://docs.vers.sh/shell-auth/initiate # Initiate Authentication Start the Shell Auth flow by submitting an email and SSH public key. Sends a verification email to confirm ownership. ## Endpoint ``` POST /api/shell-auth ``` ## Request ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "email": "user@example.com", "ssh_public_key": "ssh-ed25519 AAAA...", "body": "optional message included in verification email" } ``` | Field | Type | Required | Description | | - | - | - | - | | `email` | string | Yes | User's email address | | `ssh_public_key` | string | Yes | SSH public key (any format) | | `body` | string | No | Custom message appended to the verification email | ## Response ### Success (200) ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "success": true, "user_id": "550e8400-e29b-41d4-a716-446655440000", "email": "user@example.com", "is_new_user": true, "nonce": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "ssh_public_key": "ssh-ed25519 AAAA..." } ``` | Field | Description | | - | - | | `user_id` | The user's ID | | `is_new_user` | Whether this is a first-time user | | `nonce` | Server-generated nonce for tracking this verification | ### Errors | Status | Error | Cause | | - | - | - | | 400 | Invalid email or SSH key format | Validation failed | | 403 | Base account does not exist | Alias email (`+` syntax) where the primary account hasn't signed up | | 403 | Base account is not verified | Alias email where the primary account hasn't completed verification | | 409 | SSH key already registered to another account | This SSH key is bound to a different email | ## What happens next After calling this endpoint: * **New users**: An account is created. On email verification, an organization and free subscription are provisioned automatically. * **Existing users**: The SSH key is registered. On email verification, it's activated for authentication. ## Alias emails You can use `+` syntax (e.g., `alice+agent@company.com`) to create agent accounts that share the primary user's organizations. The primary account (`alice@company.com`) must exist and be verified first. ## Example ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -X POST https://vers.sh/api/shell-auth \ -H "Content-Type: application/json" \ -d '{ "email": "alice@company.com", "ssh_public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample...", "body": "Login from dev-machine-01" }' ``` ## Next step Poll [POST /api/shell-auth/verify-key](/shell-auth/verify-key) until the user clicks the verification link. # Overview Source: https://docs.vers.sh/shell-auth/overview # Shell Auth Programmatic authentication for the Vers platform via SSH key verification and email confirmation. ## What is Shell Auth? Shell Auth lets you authenticate with Vers and create API keys from the terminal. It's designed for CLI tools, AI agents, and automated workflows that need to interact with the Vers API. The only browser interaction is clicking a one-time email verification link. ## How it works ``` 1. Submit your email + SSH public key 2. Receive a verification email 3. Click the verification link 4. Poll for verification status → receive your org list 5. Create an API key for your selected org ``` ## Integration flow ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # Step 1: Initiate authentication POST /api/shell-auth → Sends verification email → Returns nonce for polling # Step 2: Poll for verification POST /api/shell-auth/verify-key → Returns verified: false (waiting for email click) → ... user clicks email link ... → Returns verified: true + list of organizations # Step 3: Create API key POST /api/shell-auth/api-keys → Creates API key for selected organization → Returns the API key (shown only once) ``` New users get an account and organization created automatically on first verification. Existing users just verify their SSH key and select an org. ## Security * Email verification is required before any API key can be created * Verification links expire after 10 minutes and are single-use * Each SSH key is uniquely bound to one account * API keys are never stored in plaintext — only the holder has the key * All API communication is over HTTPS ## Endpoints | Endpoint | Purpose | | - | - | | [POST /api/shell-auth](/shell-auth/initiate) | Initiate authentication | | [POST /api/shell-auth/verify-key](/shell-auth/verify-key) | Check verification status + get orgs | | [POST /api/shell-auth/verify-public-key](/shell-auth/verify-public-key) | Lookup verified key (diagnostic) | | [POST /api/shell-auth/api-keys](/shell-auth/api-keys) | Create API key | # Verify key Source: https://docs.vers.sh/shell-auth/verify-key # Verify Key Check if an email + SSH key pair has been verified. Returns the user's organization list for org selection. ## Endpoint ``` POST /api/shell-auth/verify-key ``` ## Request ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "email": "user@example.com", "ssh_public_key": "ssh-ed25519 AAAA..." } ``` | Field | Type | Required | Description | | - | - | - | - | | `email` | string | Yes | User's email address | | `ssh_public_key` | string | Yes | SSH public key submitted during initiation | ## Response ### Verified (200) ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "verified": true, "is_active": true, "user_id": "550e8400-e29b-41d4-a716-446655440000", "key_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "orgs": [ { "org_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "alice", "role": "admin" }, { "org_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "name": "acme-corp", "role": "member" } ] } ``` | Field | Description | | - | - | | `verified` | Whether the SSH key pair is verified | | `is_active` | Whether the user account is active | | `user_id` | The authenticated user's ID | | `key_id` | The SSH key record ID | | `orgs` | List of organizations the user belongs to | | `orgs[].org_id` | Organization ID (use this when creating API keys) | | `orgs[].name` | Organization display name | | `orgs[].role` | User's role: `admin`, `member`, or `owner` | ### Not verified (401) ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "verified": false, "is_active": false, "reason": "No verified SSH key found for this email and public key" } ``` The user hasn't clicked the verification link yet. Continue polling. ## Polling pattern Poll this endpoint after calling [POST /api/shell-auth](/shell-auth/initiate) until `verified` is `true`: ``` loop: response = POST /api/shell-auth/verify-key if response.verified == true: break → proceed to org selection + API key creation wait 2-3 seconds repeat ``` Verification links expire after 10 minutes. If the user doesn't verify in time, re-initiate with [POST /api/shell-auth](/shell-auth/initiate). ## Org selection The `orgs` array contains all organizations the user has access to. Use this to present an org picker to the user before creating an API key. * Users with **one org** can skip the picker * Users with **multiple orgs** should select which org to create the API key for * The `role` field indicates the user's permissions in each org ## Example ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -X POST https://vers.sh/api/shell-auth/verify-key \ -H "Content-Type: application/json" \ -d '{ "email": "alice@company.com", "ssh_public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample..." }' ``` ## Next step Once verified, create an API key with [POST /api/shell-auth/api-keys](/shell-auth/api-keys), passing the selected `org_name`. # Verify public key Source: https://docs.vers.sh/shell-auth/verify-public-key # Verify Public Key Lookup all verified records for a given SSH public key. Diagnostic endpoint — useful when you have the key but not the associated email. ## Endpoint ``` POST /api/shell-auth/verify-public-key ``` ## Request ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "ssh_public_key": "ssh-ed25519 AAAA..." } ``` | Field | Type | Required | Description | | - | - | - | - | | `ssh_public_key` | string | Yes | SSH public key to look up | ## Response ### Found (200) ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "verified": true, "count": 1, "matches": [ { "user_id": "550e8400-e29b-41d4-a716-446655440000", "key_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "email": "alice@company.com", "is_active": true, "public_key_verified": true, "verified_at": "2026-03-01T10:30:00Z", "last_seen_at": "2026-03-02T15:45:00Z" } ] } ``` | Field | Description | | - | - | | `count` | Number of verified records for this key | | `matches[].email` | The email associated with this key | | `matches[].verified_at` | When the key was verified | | `matches[].last_seen_at` | Last time the key was used for authentication | ### Not found (404) ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "verified": false, "reason": "No verified SSH key records found for this public key" } ``` ## When to use This endpoint is for diagnostics and debugging: * Checking if a key is already registered before initiating auth * Looking up which email is associated with a key * Verifying key status without knowing the email For normal authentication flows, use [POST /api/shell-auth/verify-key](/shell-auth/verify-key) instead. ## Example ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -X POST https://vers.sh/api/shell-auth/verify-public-key \ -H "Content-Type: application/json" \ -d '{ "ssh_public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample..." }' ``` # Building pi extensions Source: https://docs.vers.sh/tutorials/pi-extensions Give a coding agent a Vers computer and transparently route every bash call to it. ~80 lines of TypeScript. Build a pi extension that gives the agent computer-management tools and transparently routes every `bash` call to a Vers computer — so the agent acts on a remote machine without knowing it. This direct API example uses retained v1 wire names such as `/vm`, `vm_id`, and `vm_exec`. Those are protocol and example identifiers; the product resource is called a computer. ## What you'll build * An extension that registers `create_vm`, `vm_exec`, `vm_use`, `vm_local`, and an overridden `bash` tool * A **transparent routing** pattern that generalizes to containers, cloud VMs, SSH hosts, or any remote execution target * **Time:** \~20 minutes — mostly reading, \~80 lines to write ## Prerequisites * [pi coding agent](https://github.com/badlogic/pi-mono) installed * Vers account with `VERS_API_KEY` set in your environment * `ssh` and `openssl` on your `PATH` ## Step 1: Create the Extension Create `~/.pi/agent/extensions/my-vers.ts`: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} // ~/.pi/agent/extensions/my-vers.ts import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { execFile } from "node:child_process"; import { writeFile } from "node:fs/promises"; export default function myVersExtension(pi: ExtensionAPI) { const API_KEY = process.env.VERS_API_KEY || ""; const BASE = "https://api.vers.sh/api/v1"; let activeVmId: string | undefined; async function versApi(method: string, path: string, body?: unknown) { const res = await fetch(`${BASE}${path}`, { method, headers: { "Content-Type": "application/json", Authorization: `Bearer ${API_KEY}` }, body: body ? JSON.stringify(body) : undefined, }); if (!res.ok) throw new Error(`Vers API ${res.status}: ${await res.text()}`); return res.headers.get("content-type")?.includes("json") ? res.json() : undefined; } // We'll add tools in the next steps } ``` Test it loads: run `pi -e ~/.pi/agent/extensions/my-vers.ts`. Pi should start with no errors. ## Step 2: Add Computer Creation Add this inside the `myVersExtension` function: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} pi.registerTool({ name: "create_vm", label: "Create Computer", description: "Create a new Vers computer and wait for it to boot", parameters: Type.Object({}), async execute() { const r = await versApi("POST", "/vm/new_root?wait_boot=true", { vm_config: { mem_size_mib: 4096, fs_size_mib: 8192 }, }); return { content: [{ type: "text", text: `Computer created: ${r.vm_id}` }] }; }, }); ``` Reload pi (`/reload` in the TUI). Ask the agent to create a computer — it calls your tool and returns a computer ID. ## Step 3: Add SSH Execution Vers computers use SSH over TLS on port 443. Add the SSH helper and a tool: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} async function getKeyPath(vmId: string): Promise { const info = await versApi("GET", `/vm/${vmId}/ssh_key`); const path = `/tmp/vers-${vmId.slice(0, 12)}.pem`; await writeFile(path, info.ssh_private_key, { mode: 0o600 }); return path; } function sshExec(keyPath: string, vmId: string, command: string): Promise { return new Promise((resolve, reject) => { execFile("ssh", [ "-i", keyPath, "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "LogLevel=ERROR", "-o", `ProxyCommand=openssl s_client -connect %h:443 -servername %h -verify_hostname %h -verify_return_error -quiet 2>/dev/null`, `root@${vmId}.vm.vers.sh`, command, ], (err, stdout, stderr) => { if (err) reject(new Error(stderr || err.message)); else resolve(stdout); }); }); } pi.registerTool({ name: "vm_exec", label: "Run on Computer", description: "Execute a command on a Vers computer via SSH", parameters: Type.Object({ vmId: Type.String({ description: "Computer ID" }), command: Type.String({ description: "Shell command" }), }), async execute(_id, params) { const key = await getKeyPath(params.vmId); const out = await sshExec(key, params.vmId, params.command); return { content: [{ type: "text", text: out || "(no output)" }] }; }, }); ``` Reload. Ask the agent to create a computer then run `uname -a` on it. You'll see the computer's kernel info. The SSH connection uses `openssl s_client` as a ProxyCommand to tunnel through TLS on port 443. This is how Vers exposes SSH without opening additional ports. ## Step 4: Override bash for Transparent Routing This is the key pattern. Instead of making the LLM use `vm_exec`, override the built-in `bash` so normal commands route to the computer automatically: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} pi.registerTool({ name: "vm_use", label: "Use Computer", description: "Route all bash commands to this computer. Use vm_local to switch back.", parameters: Type.Object({ vmId: Type.String({ description: "Computer ID" }) }), async execute(_id, params, _s, _u, ctx) { activeVmId = params.vmId; ctx.ui.setStatus("vers", `vers: ${params.vmId.slice(0, 12)}`); return { content: [{ type: "text", text: `Routing to ${params.vmId}` }] }; }, }); pi.registerTool({ name: "vm_local", label: "Use Local", description: "Stop routing to the computer and run commands locally", parameters: Type.Object({}), async execute(_id, _p, _s, _u, ctx) { activeVmId = undefined; ctx.ui.setStatus("vers", undefined); return { content: [{ type: "text", text: "Back to local" }] }; }, }); pi.registerTool({ name: "bash", label: "bash", description: "Execute a bash command. Routes to the active computer if set, otherwise runs locally.", parameters: Type.Object({ command: Type.String({ description: "Command to execute" }), timeout: Type.Optional(Type.Number({ description: "Timeout in seconds" })), }), async execute(_id, params) { if (!activeVmId) { return new Promise((resolve) => { execFile("bash", ["-c", params.command], (err, stdout, stderr) => { const out = (stdout || "") + (stderr || ""); if (err) resolve({ content: [{ type: "text", text: `${out}\nExit: ${err.code}` }] }); else resolve({ content: [{ type: "text", text: out || "(no output)" }] }); }); }); } const key = await getKeyPath(activeVmId); const out = await sshExec(key, activeVmId, params.command); return { content: [{ type: "text", text: out || "(no output)" }] }; }, }); ``` Reload. Now: 1. Ask the agent to create a computer 2. Ask it to `vm_use` that computer 3. Ask it to install Node.js — it runs `apt-get install nodejs` over SSH without the LLM knowing The LLM calls `bash` as usual. Your extension decides where it runs. ## Step 5: Add Status on Startup Show the computer count in the footer when pi starts: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} pi.on("session_start", async (_ev, ctx) => { try { const vms = await versApi("GET", "/vms"); ctx.ui.setStatus("vers", `vers: ${vms.length} computer(s)`); } catch { ctx.ui.setStatus("vers", "vers: offline"); } }); ``` Reload. You'll see "vers: N computer(s)" in the footer. ## The Complete Extension The full file is \~80 lines of logic. Copy it from the assembled steps above, or see the production version with all four tool overrides (`bash`, `read`, `edit`, `write`) plus branching, commits, and SCP at [hdresearch/pi-v](https://github.com/hdresearch/pi-v/blob/main/extensions/vers-vm.ts). ## Key Patterns ### Tool override routing ``` LLM calls bash → extension checks activeVmId → SSH or local ``` The LLM doesn't need to know about routing. It calls the same tools regardless. This pattern works for any remote execution target — containers, cloud VMs, SSH hosts. ### API client as closure The `versApi` function, `activeVmId` state, and SSH helpers all live in the extension's closure. Multiple tools share them. No global state, no classes — just functions and variables scoped to the extension. ### Status feedback `ctx.ui.setStatus` shows persistent info in the footer. `ctx.ui.setWidget` shows multi-line info above the editor. Use these to keep the user informed without interrupting the agent. ## What's next The supported command-line surface for remote computers. Production version: `read`/`edit`/`write` overrides, commits, branches, SCP. Create an API key through the terminal-oriented authentication flow. The product model behind the API.