---
title: CLI
description: "The microvm binary has twenty-eight subcommands, declared as one clap Subcommand enum in microvms-cli/src/cli.rs and built from microvms-cli/Cargo.toml:20-22."
---

The `microvm` binary has twenty-eight subcommands, declared as one `clap` `Subcommand` enum in `microvms-cli/src/cli.rs` and built from `microvms-cli/Cargo.toml:20-22`.

## Global flags

These three are `global = true`, so they parse on either side of the subcommand. `microvm --json ls` and `microvm ls --json` are the same invocation. `microvms-cli/src/cli.rs:63-80`.

Flags:

* `--json` — emit the typed JSON envelope on stdout instead of human output; wins over every other format, including an interactive terminal. `microvms-cli/src/cli.rs:67-72`.
* `--dense` — token-lean output, for a consumer paying per token: tab-separated alone, compact one-line JSON with `--json`. `microvms-cli/src/cli.rs:74-76`.
* `--quiet` — suppress progress on stderr; warnings still print. `microvms-cli/src/cli.rs:78-80`.

The output format depends only on the two flags and on whether stdout is a terminal. `--json` is checked first, then `--dense`; after that, a terminal gets a ratatui surface and a pipe gets plain text. `microvms-cli/src/envelope.rs:298-305`.

The manifest publishes the three as `globalFlags` (#131), in the same parameter shape as a command's own, read off the root command's `global = true` arguments without building the tree — building would propagate each global into every subcommand's parameter list. `microvms-cli/src/manifest.rs:91-95`. The generated Reference overview renders that array as its "Global flags" table.

## Shared flag groups

Four flattened `Args` structs supply the flags that repeat across commands, so a relationship like the region conflict is declared once rather than per command. `RegionFlags`, `AttachFlags` and `InfraFlags` are described here; `ConfigFlags` (`--config` / `--no-config`, flattened into `run` and `doctor`) is described under `run`. `microvms-cli/src/cli.rs:460-461`, `microvms-cli/src/cli.rs:498-499`, `microvms-cli/src/cli.rs:544-545`, `microvms-cli/src/cli.rs:595-596`.

`RegionFlags` — flattened into every command that talks to AWS. `microvms-cli/src/cli.rs:460-472`.

Flags:

* `--region <REGION>` — AWS region; defaults to `$AWS_REGION`, then `$AWS_DEFAULT_REGION`, then `us-east-1`. Closed set: `us-east-1`, `us-east-2`, `us-west-2`, `eu-west-1`, `ap-northeast-1`. `microvms-cli/src/cli.rs:462-464`, domain at `microvms-cli/src/cli.rs:412-424`.
* `--unlisted-region <NAME>` — use a region this client has not seen carry MicroVMs; conflicts with `--region`. An unsupported region answers `AccessDeniedException` with a null message, which looks like an IAM denial, so the caller loses the real diagnostic. `microvms-cli/src/cli.rs:466-471`.

`AttachFlags` — the identifiers that address a VM this invocation did not launch: the explicit triple, or a registered name standing in for it. Carried by `exec`, `health`, `ack`, `stdin`, `cp`, and `sync`. `microvms-cli/src/cli.rs`, `AttachFlags`.

Flags:

* `--endpoint <ENDPOINT>` — the VM's endpoint, as reported by `run`. Required unless `--name` is given.
* `--agent-token <AGENT_TOKEN>` — the agent token delivered to the VM at launch. Required unless `--name` is given.
* `--microvm-id <MICROVM_ID>` — the MicroVM id, needed to mint the endpoint proxy token. Required unless `--name` is given.
* `--name <NAME>` — the name `run --keep --vm-name` registered, standing in for the whole triple. Resolved through the local name registry with zero AWS calls: the record carries the endpoint, the agent token, the MicroVM id, and the launch region (used as the region default when no `--region` flag is given). Conflicts with the explicit triple. A name this state directory never registered fails locally with `ERR_PRECONDITION`.
* `--port <PORT>` — the daemon's port inside the guest.
* `--state-dir <STATE_DIR>` — where the local state lives: the name registry, and exec's per-VM history. Defaults to `$MICROVM_STATE_DIR` or `~/.microvm/runs`.

`InfraFlags` — the three account-specific values the AWS commands need. Carried by `run`, `build`, and `doctor`. `microvms-cli/src/cli.rs:594-608`.

Flags:

* `--bucket <BUCKET>` — S3 bucket for the build artifact; defaults to `$MICROVM_BUCKET`. `microvms-cli/src/cli.rs:597-599`.
* `--build-role-arn <BUILD_ROLE_ARN>` — build role ARN; defaults to `$MICROVM_BUILD_ROLE_ARN`. `microvms-cli/src/cli.rs:601-603`.
* `--execution-role-arn <EXECUTION_ROLE_ARN>` — execution role ARN; defaults to `$MICROVM_EXECUTION_ROLE_ARN`. `microvms-cli/src/cli.rs:605-607`.

## run

```
microvm run [OPTIONS] [BINARY]
```

Builds an image, launches a VM, runs a command, reports the cost, and tears the VM down. Teardown is the default so that a closed laptop does not leave a billable VM.

`microvms-cli/src/commands/lifecycle.rs:119`

Flags:

* `[BINARY_OR_DIR]` — the aarch64 agentd binary to bake in as the image CMD (ignored when `--image` names an image to launch instead), or a directory to sync. The two readings cannot collide: a path is a directory or it is not. See "Sync mode" below. `microvms-cli/src/cli.rs`, `RunArgs::binary`.
* `--image <IDENTIFIER>` — launch this existing image instead of building one. Takes an ARN or a bare image name: a name is resolved to its ARN through the account's image listing (exact match, every page read) before the launch, with a progress line naming the resolved ARN. An identifier already shaped like an ARN passes through with zero extra calls. The envelope's `imageName` reports the launched image's own name (the ARN's last colon segment), never the per-invocation default a build would have used. A name that resolves to nothing fails locally with `ERR_PRECONDITION` naming the name and suggesting `microvm build` — the service's own answer to a bare name is HTTP 400 "Malformed ARN", which says nothing about names. `microvms-cli/src/commands/lifecycle.rs:965-979`, resolution in `microvms-core/src/control/image.rs:411-475`.
* `--image-version <VERSION>` — launch this exact image version instead of the image's latest active one. Omitted takes whatever `latestActiveImageVersion` is at the moment the call lands, which is right for the ordinary case and wrong for the two that matter: a canary wants the version it just built rather than whatever became latest while it was starting, and a rollback wants the known-good version, which "latest" cannot name once a bad version is the latest one. A version the control plane has set `INACTIVE` refuses to launch when named here — measured, the answer is HTTP 404 `No active version found for MicroVM image <arn> and version <v>`, which is what makes a retire real rather than advisory. Free text rather than a closed set, because a version's legal values are an account fact only `ListManagedMicrovmImageVersions` can answer; the constraint that *is* knowable is checked before any call, so an empty version, one over 2048 characters, or one containing whitespace anywhere fails locally with `ERR_INVALID_ARG` and the reason. A version pasted from a terminal carries a trailing newline, which is that case. `microvms-core/src/control/mod.rs`'s `require_valid_version`, wiring in `microvms-cli/src/commands/lifecycle.rs`.
* `--artifact-uri <S3_URI>` — where the build artifact already is; `microvms-core` builds the artifact bytes and takes the URI but does not upload. `microvms-cli/src/cli.rs:671-672`.
* `--exec <COMMAND>` — a shell command to run in the VM. When it is omitted, the run only launches and tears down, which is how you check that an image boots. `microvms-cli/src/cli.rs:677-678`.
* `--name <NAME>` — image name; defaults to a per-invocation name, because reusing a name can trigger a `clientToken` replay that wedges the image. `microvms-cli/src/cli.rs:682-683`.
* `--vm-name <NAME>` — register a local name for the kept VM, so later commands can say `--name <NAME>` (attached commands) or use the name as the positional (suspend, resume, terminate, history) instead of pasting identifiers. Requires `--keep`. The name is a purely local fact in the state directory's registry (`<state-dir>/names/<NAME>.json`, written owner-only because the record carries the agent token), costs zero AWS calls, and is released when a terminate is accepted. Names take ASCII letters, digits, `-` and `_`, at most 128 bytes, and never a MicroVM id prefix (`microvm-` is the service's real prefix, measured live; `mvm-` is the test fixtures') — that exclusion is what lets every identifier-taking command tell a name from a MicroVM id. A name registered to a live VM is refused locally with `ERR_NAME_TAKEN` (exit 14) before any billable call. `microvms-cli/src/cli.rs`, `RunArgs::vm_name`; registry in `microvms-cli/src/ledger.rs`, `Names`.
* `--memory <MEMORY>` — baseline MiB, selecting a documented size class; default `2048`. Closed set: `512`, `1024`, `2048`, `4096`, `8192`. `microvms-cli/src/cli.rs:690-691`.
* `--dockerfile <DOCKERFILE>` — a Dockerfile to use instead of the library's default; its `FROM` must match the base. `microvms-cli/src/cli.rs:694-695`.
* `--log-group <GROUP>` / `--log-stream <STREAM>` — build-log destination, applied when this invocation builds; same semantics as `build`'s flags (the stream is a prefix the client suffixes with `/<16 hex>` per build). Both are also `microvm.toml` keys (`log-group`, `log-stream`), with the flag winning per knob; a stream with no group from either layer is refused locally. `microvms-cli/src/cli.rs`, merge in `microvms-cli/src/commands/lifecycle.rs`'s `merge_config`.
* `--repair-identity` — request identity repair and additional OS capabilities. Inspect health for `identity_degraded`; requested capabilities do not guarantee every repair succeeds.
* `--egress` — request the managed `INTERNET_EGRESS` connector. Omission does not disable internet access. Also the `egress` config key. See [Networking](../internals/networking.md).
* `--egress-network-connector <ARN>` — attach an existing custom VPC connector; repeatable. Conflicts with `--egress`. Config key: `egress-network-connectors`; explicit flags replace the configured list. Internet isolation requires a VPC without an IGW or NAT gateway and no alternative internet path.
* `--deny-egress` — set proxy variables pointing to an unreachable local proxy. Reports `best-effort`; workloads can bypass it. This is not network enforcement and conflicts with `--egress`. Also the `deny-egress` config key.
* `--launch-env <KEY=VALUE>` — set one launch-environment variable for every exec in the VM; repeatable. Delivered in the same `runHookPayload` as the agent token, at launch, so it never touches the shared image snapshot and never touches disk. The daemon applies it as the *base* environment of every exec, with `exec --env` on the same key winning. Same parser as `exec --env`, so the first `=` splits, an empty VALUE is legal, and a missing `=` or empty KEY is refused at parse time. The whole payload shares a 4096-byte ceiling with the token, checked locally before the launch: an over-budget env fails with the byte count and the env's share of it, rather than as an AWS `ValidationException` after the call. The total must fit after serialization; large credential sets belong on `microvm cp` after bootstrap or on a role the workload assumes. `microvms-cli/src/cli.rs`, wiring in `microvms-cli/src/commands/lifecycle.rs`.
* `--keep` — retain the VM and image; the VM continues to incur charges until stopped, suspended, or terminated according to its lifecycle. `--keep` is also the only case in which the envelope's `agentToken` carries a value: a run that tears its VM down emits the key as `null`, because stdout outlives the process and the token would name a VM that no longer exists (#161). `microvms-cli/src/cli.rs:800-801`, `microvms-cli/src/render.rs`, `RunOutcome::to_data`.
* `--timeout <TIMEOUT>` — how long to wait for the exec, in seconds; default `300`. `microvms-cli/src/cli.rs:838-839`.
* `--max-idle-sec <MAX_IDLE_SEC>` — suspend the VM after this much inbound-traffic idleness; default `600`. `microvms-cli/src/cli.rs:842-843`.
* `--suspended-sec <SUSPENDED_SEC>` — terminate the VM after this long suspended; a resume attempted after this window fails because the VM no longer exists. Default `600`. `microvms-cli/src/cli.rs:846-847`.
* `--auto-resume` — let the platform resume a suspended VM on an incoming request, instead of requiring an explicit `microvm resume`; omitted by default. Sets `idlePolicy.autoResumeEnabled` on the launch. Also a `microvm.toml` key (`auto-resume`). `microvms-cli/src/cli.rs`, merge in `merge_config`.
* `--max-duration-sec <MAX_DURATION_SEC>` — hard ceiling on the VM's life; refused above 28800 before any call. Default `3600`. `microvms-cli/src/cli.rs:854-855`.
* `--port <PORT>` — the daemon's port inside the guest. `microvms-cli/src/cli.rs:858-859`.
* `--state-dir <STATE_DIR>` — where the run ledger is written; defaults to `$MICROVM_STATE_DIR` or `~/.microvm/runs`. `microvms-cli/src/cli.rs:862-863`.
* `--config <PATH>` — read this project config file instead of `./microvm.toml`. Naming a file that does not exist is refused with `ERR_CONFIG`: a typed path that is wrong must not silently become "no config". Conflicts with `--no-config`. `microvms-cli/src/cli.rs`, `ConfigFlags`.
* `--no-config` — ignore any `microvm.toml`, even a malformed one; flags and built-in defaults apply. `microvms-cli/src/cli.rs`, `ConfigFlags`.
* Plus `RegionFlags` and `InfraFlags`. `microvms-cli/src/cli.rs:875-879`.

Precedence, when a config file is in play: a typed flag beats the file, and the file beats the built-in default. "Typed" is read off the parse (`clap`'s `value_source`), not off the value, so `--memory 2048` overrides a file that says `4096` even though 2048 is also the default. The merge happens in exactly one place (`merge_config` in `microvms-cli/src/commands/lifecycle.rs`, per-knob precedence in `config::pick`) and its outcome is reported in the success envelope's `resolvedConfig` key — each knob's winning value and the source it came from (`flag`, `config`, `env`, or `default`; `env` appears only on the region, the one knob whose chain continues past the file into `$AWS_REGION`/`$AWS_DEFAULT_REGION`) — so a caller never has to re-derive which source won. `egress` and `denyEgress` are two of those knobs; the conservative assessment of internet isolation is a separate top-level envelope key, `egressPosture` (`open` | `unsealed` | `best-effort` | `sealed`), because a request flag is not an answer — see docs/TRUST.md, **Egress**. One deliberate pairing rule: a typed `BINARY` positional with no typed `--image` suppresses the file's `image`, because `run` builds exactly when the merged image is absent, and a file that silently won that pair would run the caller's tests against a stale pinned image. A positional that names a directory does not suppress it — sync mode launches, so the file's pinned image is exactly what `run .` wants. See "Project config" below for the file itself.

Sync mode (issue #72): a positional that names a directory switches `run` into a pack-run-collect round trip — `microvm run . --image ci-image --exec "make test"` is the headline spelling. The tree is packed locally (deterministic member order; `.git`, `target`, `node_modules`, and `.venv` skipped whole; sockets, fifos, and devices skipped individually; symlinks preserved as links, never followed), uploaded to `/workspace` in the guest, and the exec runs with `/workspace` as its working directory. The pack is budgeted against the daemon's own caps — 512 MiB of file bytes, 100 000 members — during the walk, so an over-budget tree is `ERR_SYNC` naming the offending subtree before any archive bytes are allocated or any AWS call is made.

Afterwards — including when the exec exited non-zero, because a failing run's report is the artifact CI most wants — the workdir comes back and the members matching the config file's `artifacts` globs are written into the directory. Only glob-matched regular-file members land, and never under `.git`: symlinks, hardlinks, specials, unmatched members, anything attempting traversal outside the directory, and any `.git` path are skipped, because the returned archive is the VM's word, the VM is where untrusted work runs, and a workload-written `.git/hooks/pre-commit` would execute on the host at the caller's next commit. With no `artifacts` globs configured the workdir is not downloaded at all, and the `sync` key says so in a `note`. A download failure does not fail the run — by then the exec's result is already in hand, and discarding a green test run because `make clean` removed the workdir would be the report lying — the error lands in `sync.error` and the exec's own exit code stands.

Sync mode launches an existing image (`--image`, or `image` pinned in microvm.toml); a directory with nothing supplying an image is refused with `ERR_PRECONDITION` before any call. A local pack or extraction failure is `ERR_SYNC` (exit 16). The envelope's `sync` key reports the workdir, uploaded bytes and member count, and each artifact brought back with its size — or `error` / `note` for the two no-artifact shapes. `microvms-cli/src/sync.rs`; wiring in `microvms-cli/src/commands/lifecycle.rs`.

## build

```
microvm build [OPTIONS] [BINARY]
```

Builds a MicroVM image and waits for it to be usable. Nothing is torn down afterward. The image is the durable artifact, and because its snapshot has a one-week minimum retention, deleting it early saves nothing.

`microvms-cli/src/commands/lifecycle.rs:1296`

Flags:

* `[BINARY]` — the aarch64 agentd binary to bake in as the image CMD. Omitted, the CLI provisions its own version's release asset and caches it under the state directory; `$MICROVM_AGENTD` names a binary without touching the command line. `microvms-cli/src/cli.rs:918-919`.
* `--artifact-uri <S3_URI>` — where the build artifact already is, as an `s3://` URI. `microvms-cli/src/cli.rs:928-929`.
* `--name <NAME>` — image name; defaults to a per-invocation name. `microvms-cli/src/cli.rs:932-933`.
* `--memory <MEMORY>` — baseline MiB, selecting a documented size class; default `2048`. `microvms-cli/src/cli.rs:936-937`.
* `--dockerfile <DOCKERFILE>` — a Dockerfile to use instead of the library's default. `microvms-cli/src/cli.rs:940-941`.
* `--project <DIR>` — bake an environment layer from the directory's dependency files (#74). Exactly one ecosystem's manifest+lockfile pair must be present — `pyproject.toml`+`uv.lock`, `package.json`+`package-lock.json`, or `Cargo.toml`+`Cargo.lock` — and only that pair enters the shared image snapshot, under names fixed by the ecosystem (nothing else in the directory can enter, which is what keeps a `.env` out of a snapshot every VM shares). The derived Dockerfile copies the pair into the working directory (`/project` when none is named) and installs from the lockfile with the lockfile-faithful spelling: `uv sync --locked`, `npm ci`, or `cargo fetch --locked`, each refusing a lockfile that disagrees with its manifest rather than quietly re-resolving. Launches from the image then start with dependencies already installed — the 31–48% env-init share of launch time `docs/STRATEGY.md` measures is paid once at build. A caller `--dockerfile` that never mentions the lockfile is refused before the upload, because it would bake no layer while building cleanly. Missing lockfile, missing manifest, and two-ecosystem directories are each refused naming their remedy. Measured launch delta, and why in-guest code should call `/project/.venv/bin/python` directly rather than `uv` (an exec sees no `PATH`): `docs/PLATFORM.md`, "A baked environment layer removes the guest's env init". `microvms-cli/src/commands/lifecycle.rs`'s `read_project_files`, the entries in `microvms-core/src/control/artifact.rs`.
* `--base-image-version <VERSION>` — pin the managed base image to one version instead of taking the service's default. Without this a build floats: the managed base's version list is not static — `al2023-1` carried one version in June and two by July — so two builds of identical inputs weeks apart can sit on different bases and neither recorded which. The build succeeds either way; the difference shows up in the guest. The legal values come from `ListManagedMicrovmImageVersions`, which `microvm doctor` prints as its `base-image-versions` check, and they are bare integers for a managed base (`0`, `1`) where a custom image's versions are `1.0`. A bogus pin is refused by the service before anything is created (HTTP 400 `No managed MicroVM Image with arn <base-arn> and version 999 is available`), but it costs the artifact upload first, so the `Version` shape's own constraints — non-empty, at most 2048 characters, no whitespace anywhere — are checked locally before the upload. Note that the value comes back **normalised**: a build pinned with `1` reads back `baseImageVersion: "1.0"` from `GetMicrovmImageVersion`, so the echoed value cannot be fed back into a request. `microvms-cli/src/commands/lifecycle.rs`'s `BuildSpec`, guard in `microvms-core/src/control/image.rs`'s `create_image`.
* `--log-group <GROUP>` — CloudWatch log group for the build's logs, instead of the service-created `/aws/lambda-microvms/<image-name>`. Letters, digits, and `_ - / . #` only, up to 512 characters, validated locally before the artifact upload. The build role must be able to write to whatever this names (`logs:CreateLogGroup`/`CreateLogStream`/`PutLogEvents`); a group outside a granted prefix builds with **no logs at all**, the same silent outcome as the wrong-prefix policy in `docs/PLATFORM.md`.
* `--log-stream <STREAM>` — log stream name **prefix** inside `--log-group`; requires it. The platform's `logging.logStream` member is an exact stream name — prefixes are unsupported — and one image build is three VMs writing three streams (docker build, Graviton 3 snapshot, Graviton 4 snapshot), so a fixed configured name would collapse every build's logs into one indistinguishable stream. The client therefore appends `/<16 hex>` of fresh randomness per build attempt, and the envelope reports the resolved exact name as `logStream` — the only place it exists. No `:` or `*` (the shape's pattern is `[^:*]*`); up to 495 characters (the platform's 512 minus the suffix's 17). See `docs/PLATFORM.md`, "An image build is three VMs and three log streams".
* `--repair-identity` — request identity repair and additional OS capabilities. Inspect health for `identity_degraded`; requested capabilities do not guarantee every repair succeeds.
* `--reuse` — reuse an existing image whose build inputs match, instead of building. Computes a sha256 over the build inputs (the daemon binary's bytes, the Dockerfile, and — with `--project` — the manifest and lockfile, names and bytes both), derives the image name `<name>-<hash12>` — where the prefix is `--name` or the stable stem `microvm-cli` — and checks the listing for that exact name. A hit skips the build entirely and reports the existing image with `reused: true` in the envelope; a miss builds under the derived name, so the next invocation with the same inputs hits. The hash is in the name because recreating an image under a previously-used fixed name can serve a stale snapshot (measured; the same hazard class as the clientToken replay in `docs/PLATFORM.md`) — content-keying gives both properties at once: unchanged inputs reuse their image, changed inputs get a fresh name and a fresh build. With `--project` that is #74's promise: two projects with identical dependency files share a layer, and a lockfile edit builds a fresh one. `--memory` is not part of the identity, so a reused image keeps the size class it was created with; the envelope's `size` is the requested class and the text says so. `microvms-cli/src/commands/lifecycle.rs:1370-1376`, the hash at `microvms-core/src/control/artifact.rs`'s `artifact_content_hash`.
* `--port <PORT>` — the daemon's port inside the guest. `microvms-cli/src/cli.rs:1010-1011`.
* Plus `RegionFlags` and `InfraFlags`. `microvms-cli/src/cli.rs:1013-1017`.

The success envelope always carries `reused` (`false` for a plain build) and `logStream` (`null` when no `--log-stream` was configured; a reused image also reports `null`, because no build ran and no stream was resolved), so a consumer never guards for either key. `microvms-cli/src/commands/mod.rs`.

## agent-up

```
microvm agent-up [OPTIONS] --vm-name <NAME> [BINARY]
```

Brings up a VM with a coding agent in it: image, launch, model credentials, non-root user. The L3 helper (`docs/AGENT-VMS.md`) composes `build --reuse`, `run --keep --egress --vm-name`, a Bedrock bearer token minted from the caller's own AWS credentials, and the file uploads and `chown` the coding-agents example performed by hand. Two paths, decided by one local registry read: a name this state directory has not registered means build (or reuse), launch, provision, register; a name it has registered means attach, mint a fresh token, re-install the same files, and report `vmReused: true`, with no build and no launch. That refresh path is how a 12-hour token is renewed on a long-lived VM. Teardown is `microvm terminate <NAME>`.

`microvms-cli/src/commands/agent.rs`

Flags:

* `[BINARY]`: the aarch64 agentd binary to bake in. Omitted provisions this CLI's own release asset, the same chain `run` and `build` use; the envelope's `agentd` key reports the path and source. `microvms-cli/src/cli.rs`, `AgentUpArgs::binary`.
* `--vm-name <NAME>`: the local name to register for the VM, and the handle every later command uses. Required, because an agent VM is kept by definition. Same grammar as `run --vm-name`. A name already registered to a live VM is not a collision here: it selects the refresh path. A name registered to a torn record (empty MicroVM id, a process died mid-register) is refused with `ERR_NAME_TAKEN` and the record's path. `microvms-cli/src/cli.rs`, `AgentUpArgs::vm_name`.
* `--agent <AGENT>`: which agent to install. Closed set: `claude-code`, `codex`. Repeatable; omitted means `claude-code`; a repeated value collapses to one row. On the refresh path, omitted keeps the agents and models the guest marker names, and a typed `--agent` is the caller changing them. `microvms-cli/src/cli.rs`, `AgentArg`.
* `--claude-model <MODEL_ID>`: the Bedrock inference-profile id Claude Code uses; default `global.anthropic.claude-opus-5`, from the profile table. `microvms-core/src/agents/profile.rs`, `CLAUDE_CODE`.
* `--codex-model <MODEL_ID>`: the Bedrock model id Codex uses; default `global.openai.gpt-5.6-sol`, from the profile table. `microvms-core/src/agents/profile.rs`, `CODEX`.
* `--claude-version <VERSION>` / `--codex-version <VERSION>`: pin the agent's npm package to this version (`@anthropic-ai/claude-code@<V>`, `@openai/codex@<V>`). Unpinned, the image carries the registry's latest at build time. A pin changes the Dockerfile text and therefore the image's reuse hash. `microvms-cli/src/cli.rs`, `AgentUpArgs::claude_version`.
* `--project <DIR>`: a local directory to upload into `/workspace` after launch, packed the way `run <DIR>` packs: same skip list (`.git`, `target`, `node_modules`, `.venv`), same budgets, packed before any AWS call so an unreadable tree costs nothing. The upload lands before the install's `chown`, so the agent owns the tree. Bring results back with `microvm cp --tar vm:/workspace <LOCAL> --name <NAME>`. Also honored on the refresh path. `microvms-cli/src/cli.rs`, `AgentUpArgs::project`.
* `--memory <MEMORY>`: baseline MiB; default `1024`, not `run`'s `2048`: a 4 GiB always-present ceiling at half the floor cost, which fits peaky agent sessions. Closed set as for `run`. `microvms-cli/src/cli.rs`, `AgentUpArgs::memory`; `microvms-core/src/agents/mod.rs`, `DEFAULT_SIZE`.
* `--token-ttl-hours <HOURS>`: how long the Bedrock bearer token lives; default and ceiling `12`. Core refuses a lifetime outside `(0, 12 h]` with `ERR_INVALID_ARG`; the service additionally caps validity at the signing credentials' own expiry. `microvms-core/src/agents/bedrock.rs`, `MAX_LIFETIME`.
* `--max-idle-sec <MAX_IDLE_SEC>`: suspend the VM after this much inbound-traffic idleness; default `600`.
* `--suspended-sec <SUSPENDED_SEC>`: terminate the VM after this long suspended; default `600`.
* `--auto-resume`: let the platform resume a suspended VM on an incoming request; omitted by default.
* `--max-duration-sec <MAX_DURATION_SEC>`: hard ceiling on the VM's life; refused above 28800 before any call. Default `3600`.
* `--port <PORT>`: the daemon's port inside the guest.
* `--state-dir <STATE_DIR>`: where the run ledger and name registry live; defaults to `$MICROVM_STATE_DIR` or `~/.microvm/runs`.
* Plus `RegionFlags` and `InfraFlags`. The fresh path requires the bucket, the build role, and the execution role (`ERR_PRECONDITION` naming the missing one); the refresh path makes no control-plane call before the attach, and takes the region from the flag when typed, else from the name record.

Egress is always requested, because neither agent reaches Bedrock without it. The image is named `agent-vm-<agents>-<hash12>`, the hash over the same inputs `build --reuse` covers, so an unchanged daemon and agent set reuse their image and a version pin builds a fresh one. Provisioning writes `/workspace/.agent-env` (mode `0600`; `HOME`, `PATH`, `AWS_REGION`, and each agent's credential and model lines), `/workspace/.codex/config.toml` when Codex is present, and the marker `/workspace/.agent-vm.json` (mode `0644`) naming the installed agents and models, then runs one root `chown -R 1000:1000 /workspace`. The token travels only as a file over the authenticated channel; it is never an argv element and never in the launch payload. `microvms-core/src/agents/mod.rs`, `provisioning_files` and `install_access`.

A failure after the launch and before the registration (token mint, project upload, credential install, or an interrupt) tears the VM down, because a VM with no name and no credentials is one nobody can use; the failure envelope carries `microvmId`, `imageIdentifier`, `leaked`, and `terminateAccepted`. The image stays, as the durable artifact. The name is registered last, over a VM every step succeeded on; a registry write failure is `ERR_PRECONDITION` with the identifier triple in `data`. No new exit row: every failure maps onto the existing table.

The success envelope's type is `microvm.agent`; its keys are `vmName`, `microvmId`, `endpoint`, `agentToken`, `imageIdentifier`, `imageName`, `imageReused`, `vmReused`, `agents` (`[{agent, model, cliVersion, headlessCommand}]`), `credentialExpiresAt` (epoch seconds), `workdir`, `project` (`{workdir, uploadedBytes, uploadedMembers}` or `null`), and `agentd`. On the refresh path the two image keys and `imageReused` are `null`, because no image was built or looked up. `agents[].headlessCommand` is the exact template `agent-prompt` runs with `<TASK>` where the quoted task goes, so a caller who wants `exec --stream --user 1000 --group 1000` over it can spell the command without knowing the profile. `microvms-cli/src/commands/mod.rs`, the `agent-up` row.

## agent-prompt

```
microvm agent-prompt [OPTIONS] <TASK>
```

Hands a coding agent in a running agent VM one task, headless, as the non-root user. Runs the agent's headless command (`claude -p <TASK> --allowedTools Bash,Read,Edit,Write,Grep,Glob` or `codex exec --skip-git-repo-check -s workspace-write <TASK>`) as uid 1000, gid 1000, in `/workspace`, with `. /workspace/.agent-env &&` in front so the credentials `agent-up` installed are the environment; the request's own `env` map is empty, so the token never rides in a request body. By default it waits and acks, exactly as `exec` does; `--detach` starts and returns the exec id.

`microvms-cli/src/commands/agent.rs`

Flags:

* `<TASK>`: the task, as prose. Passed to the headless command single-quoted for `sh`. Required. An empty or whitespace-only task is refused locally with `ERR_INVALID_ARG` before the attach, so it costs zero calls. `microvms-cli/src/cli.rs`, `AgentPromptArgs::task`.
* `--agent <AGENT>`: which installed agent to prompt; closed set `claude-code`, `codex`. Omitted reads the guest marker `/workspace/.agent-vm.json` and takes the one agent it names; a marker naming two agents is refused with `ERR_PRECONDITION` listing both and suggesting `--agent`. A typed `--agent` still reads the marker when it can, so the prompt uses the model the VM was provisioned with; a VM with no marker and a typed agent runs the profile's defaults; a VM with no marker and no flag is `ERR_PRECONDITION` naming `agent-up`. `microvms-cli/src/commands/agent.rs`, `choose_spec`; `microvms-core/src/agents/mod.rs`, `installed_agents`.
* `--timeout <TIMEOUT>`: how long this process waits for the agent, in seconds; default `900`, because agent tasks run minutes, not seconds. A client-side deadline: the exec on the daemon's side is started with no wall-clock budget of its own. `microvms-cli/src/cli.rs`, `AgentPromptArgs::timeout`.
* `--detach`: start the agent and return immediately, without waiting and without acking; the envelope reports `phase: running` and a `null` exit code. `microvm exec --poll <ID> --name <NAME>` reads it back and `microvm ack` releases it, as for `exec --detach`. `microvms-cli/src/cli.rs`, `AgentPromptArgs::detach`.
* `--exec-id <ID>`: use this exec id instead of a fresh one, making a retry idempotent. `microvms-cli/src/cli.rs`, `AgentPromptArgs::exec_id`.
* Plus `AttachFlags` and `RegionFlags`; `--name <NAME>` is the usual spelling, the name `agent-up --vm-name` registered.

The success envelope's type is `microvm.agent.prompt`, under its own discriminant because a consumer that learned `microvm.exec` is reading a command it chose and this one ran a template it did not. Its keys are `exec`'s plus which agent ran: `execId`, `agent`, `model`, `phase`, `exitCode`, `stdout`, `stderr`, `truncated`. A non-zero agent exit earns `ERR_EXEC_FAILED` on a success envelope, as `exec` does. `microvms-cli/src/commands/mod.rs`, the `agent-prompt` row; `microvms-cli/src/commands/attached.rs`, `render_exec_as`.

## exec

```
microvm exec [OPTIONS] --endpoint <ENDPOINT> --agent-token <AGENT_TOKEN> --microvm-id <MICROVM_ID> [COMMAND]
```

Runs one command in a MicroVM that is already running. The single subcommand covers four uses. It can start a command and wait for it, start one and stream its output (`--stream`), start one and feed its stdin (`--stdin`), or read an existing exec (`--poll`).

`microvms-cli/src/commands/attached.rs:178`

Flags:

* `[COMMAND]` — a shell command to run in the VM; omitted only with `--poll`. `microvms-cli/src/cli.rs:1023-1024`.
* `--timeout <TIMEOUT>` — how long to wait for the command, in seconds; default `300`. `microvms-cli/src/cli.rs:1027-1028`.
* `--cwd <CWD>` — working directory. When omitted, the command inherits the image WORKDIR, which is not the same as passing `/`. `microvms-cli/src/cli.rs:1034-1035`.
* `--env <KEY=VALUE>` — set one environment variable for the command; repeatable. These flags are the child's whole environment: the daemon starts every exec from an empty one and applies exactly this map, so there is no inherited PATH to append to. Split at the first `=`, so a value may itself contain `=`; an empty VALUE is legal (`--env EMPTY=`), and a missing `=` or an empty KEY is refused at parse time. `microvms-cli/src/cli.rs:1050-1051`.
* `--user <UID>` — numeric uid to run the command as; omitted runs as the daemon's own user. Numeric because that is the protocol's type and the daemon's mechanism (`Command::uid`, between fork and exec) — a name would need an `/etc/passwd` lookup inside a guest whose base image may not have one. `microvms-cli/src/cli.rs:1060-1061`.
* `--group <GID>` — numeric gid to run the command as; omitted keeps the daemon's own group. `microvms-cli/src/cli.rs:1064-1065`.
* `--exec-id <ID>` — use this exec id instead of a fresh one, making a retry idempotent; the daemon returns success for a known id without spawning a second child. `microvms-cli/src/cli.rs:1084-1085`.
* `--poll <ID>` — read an existing exec's status and output instead of starting anything; read-only server-side, does not ack. Conflicts with `--exec-id`, `--stream`, `--stdin`, `--cwd`, `--detach`, `--env`, `--user`, `--group`. `microvms-cli/src/cli.rs:1093-1094`.
* `--detach` — start the command and return immediately, without waiting and without acking; prints the exec id and `phase: running`. Conflicts with `--stream` and `--stdin`. `microvms-cli/src/cli.rs:1113-1114`.
* `--stream` — stream output as it arrives rather than waiting for the whole thing; under `--json` or into a pipe this writes NDJSON. `microvms-cli/src/cli.rs:1123-1124`.
* `--from-offset <BYTES>` — resume a stream at this byte offset; requires `--stream`. `microvms-cli/src/cli.rs:1131-1132`.
* `--stdin` — give the command a stdin pipe, feed it this process's stdin, then close it. `microvms-cli/src/cli.rs:1139-1140`.
* `--reap` — signal the command's whole process group once its own child exits, so nothing it backgrounded outlives it (`reap_group_on_exit: true` on the wire). Off by default, and the default is a contract: a backgrounded grandchild that inherited the output pipe keeps running and keeps writing. With the flag, `ps` afterwards shows the group empty and the exec's `writersMayBeAlive` reads false because the linger saw EOF. Conflicts with `--poll`. `microvms-cli/src/cli.rs:1151-1152`.
* `--kill-on-timeout` — on `ERR_TIMEOUT`, send one `POST /v1/exec/{id}/kill` and put the daemon's verdict in the failure envelope's `data.killed`. A plain `--timeout` is a client-side deadline that abandons the exec and leaves it running in the guest; this turns it into a stop. The exit code stays `ERR_TIMEOUT`, because the deadline is still what ended the wait. Conflicts with `--poll`, `--detach`, and `--stream`. `microvms-cli/src/cli.rs:1162-1163`.
* Plus `AttachFlags` and `RegionFlags`. `microvms-cli/src/cli.rs:1165-1169`.

A timeout is not a stop. `ERR_TIMEOUT`'s suggestions say so in as many words and name `microvm kill <exec-id>` as the remedy, beside the older fact that the exec and its output are untouched and re-pollable. `microvms-cli/src/exit.rs:402`.

## health

```
microvm health [OPTIONS] --endpoint <ENDPOINT> --agent-token <AGENT_TOKEN> --microvm-id <MICROVM_ID>
```

Asks a running MicroVM's daemon whether it is up and what its identity repair did. This is the one command that reports `identityDegraded` and `diskUnderPressure`, and either flag is a reason to drain the VM rather than keep scheduling onto it.

It also reports `busy` and `execs`, which is what makes this the command an orchestrator loops on to hold a long-running VM alive. The platform measures idleness by inbound traffic through the endpoint proxy, and that proxy terminates outside the guest, so a request sent from *inside* the VM cannot reset the idle timer — only a poll from outside counts, and this poll is that traffic. `busy` is what makes the loop informed rather than unconditional: it is true only while some exec is actually running, so an exec that exited and is waiting to be acked reads false. `execs` counts every registered entry in any phase, so `busy: false` with a non-zero count is a VM holding unacked output somebody still has to collect before terminating it. See `docs/PROTOCOL.md`, "Idle policy, and why liveness is a field rather than a route".

And it reports `hooks`, the daemon's own record of every lifecycle-hook invocation it observed — `[{hook, firedAt}]`, oldest first, with `firedAt` in epoch seconds on the daemon's clock — beside `hooksDropped`, how many invocations the daemon's capped log discarded. The platform writes no CloudWatch logs for the validate hook, so this is the only place "did my validate hook even run?" is answerable; `validate` and `ready` fire in the snapshot VM before the snapshot is taken, so a launched VM commonly reports them from the memory image it restored from. Each observation the poll returns is also appended to the VM's local history as a `hookObserved` event, deduplicated on the (hook, firedAt) pair, so re-polling appends nothing and the record survives the VM. One caveat travels with the field: the daemon's hook routes are unauthenticated and reachable over loopback from inside the guest, so a hostile workload can forge *additional* observations by posting the hook paths itself — it can never remove or alter real ones, and the capped log keeps the earliest entries, which are the platform's. Both fields are empty/zero against a daemon that predates them.

`microvms-cli/src/commands/attached.rs:635`

Flags:

* `AttachFlags` and `RegionFlags` only; this command has no arguments of its own. `microvms-cli/src/cli.rs:1172-1179`.

## ack

```
microvm ack [OPTIONS] --endpoint <ENDPOINT> --agent-token <AGENT_TOKEN> --microvm-id <MICROVM_ID> <EXEC_ID>
```

Releases a finished exec's buffered output, which starts its collection clock. A second ack returns a 409 because the first one already released the output.

`microvms-cli/src/commands/attached.rs:811`

Flags:

* `<EXEC_ID>` — the exec whose output to release. Required. `microvms-cli/src/cli.rs:1346-1347`.
* Plus `AttachFlags` and `RegionFlags`. `microvms-cli/src/cli.rs:1349-1353`.

## kill

```
microvm kill [OPTIONS] --endpoint <ENDPOINT> --agent-token <AGENT_TOKEN> --microvm-id <MICROVM_ID> <EXEC_ID>
```

Stops a running exec: SIGTERM to its whole process group, SIGKILL after the daemon's grace (`POST /v1/exec/{id}/kill`). This is the stop button `exec --timeout` is not — a client-side timeout abandons an exec and leaves it running in the guest (issue #156). The VM is addressed the way every attached command addresses it, by `--name` or the identifier triple, and the exec by its id, which `exec --detach`, `exec --exec-id`, and `ps` all print.

The envelope carries the daemon's own verdict. `killed: false` with exit 0 means the process group had already exited, which is the outcome a kill was asking for, so `microvm kill x-1 && collect` runs its second half either way. An unknown exec id is the daemon's 404, arriving as `ERR_PROTOCOL` with `data.kind: NotFound`.

`microvms-cli/src/commands/attached.rs:852`

Flags:

* `<EXEC_ID>` — the exec whose process group to signal. Required. `microvms-cli/src/cli.rs:1359-1360`.
* Plus `AttachFlags` and `RegionFlags`.

## ps

```
microvm ps [OPTIONS] --endpoint <ENDPOINT> --agent-token <AGENT_TOKEN> --microvm-id <MICROVM_ID>
```

Lists every exec's process group and its live pids (`GET /v1/procs`). The daemon reads `/proc` itself, so this works against the al2023 base image, which ships no `ps` (issue #157); nothing needs installing in the image for this command. The row worth reading is `childExited: true` with a non-empty `pids`: a command that finished while something it backgrounded did not, which `health`'s `busy` cannot show because busy is about the exec's own child. The `execId` on that row is what `kill` takes.

`data.procs` is one object per registered exec in any phase: `{execId, pgid, startedAt, childExited, reap, pids}`. `pgid` is `null` (never absent) when the daemon captured none; `startedAt` is epoch seconds on the daemon's clock; `reap` echoes `exec --reap`. Zombies are not live and are not listed. `--dense` prints one TSV row per group — exec id, pgid, childExited, pid count, startedAt — exec id first, so `cut -f1` feeds `kill`.

`microvms-cli/src/commands/attached.rs:904`

Flags:

* `AttachFlags` and `RegionFlags` only; this command has no arguments of its own. `microvms-cli/src/cli.rs:1369-1376`.

## stdin

```
microvm stdin [OPTIONS] --endpoint <ENDPOINT> --agent-token <AGENT_TOKEN> --microvm-id <MICROVM_ID> <EXEC_ID>
```

Writes to a running exec's stdin and optionally closes it. It only works on an exec started with `exec --stdin`, and it is the only way to close the pipe.

`microvms-cli/src/commands/attached.rs:995`

Flags:

* `<EXEC_ID>` — the exec to write to; must have been started with `exec --stdin`. Required. `microvms-cli/src/cli.rs:1381-1382`.
* `--data <DATA>` — what to write; `-` reads this process's stdin, and omitting the flag writes nothing. The value is raw bytes either way, and core base64-encodes them for the wire. `microvms-cli/src/cli.rs:1388-1389`.
* `--eof` — close stdin after any `--data` is written, in the same request rather than a second one. `microvms-cli/src/cli.rs:1396-1397`.
* Plus `AttachFlags` and `RegionFlags`. `microvms-cli/src/cli.rs:1399-1403`.

## cp

```
microvm cp [OPTIONS] --endpoint <ENDPOINT> --agent-token <AGENT_TOKEN> --microvm-id <MICROVM_ID> <SRC> <DST>
```

Copies a file or a tar archive between here and a running MicroVM: `cp ./local vm:/remote` writes, `cp vm:/remote ./local` reads.

`microvms-cli/src/commands/attached.rs:1162`

Flags:

* `<SRC>` — source; `vm:/path` reads from the VM, anything else is a local path. Required. `microvms-cli/src/cli.rs:1409-1410`.
* `<DST>` — destination; `vm:/path` writes to the VM, anything else is a local path. Required. `microvms-cli/src/cli.rs:1413-1414`.
* `--tar` — move a whole directory tree as an uncompressed tar archive; the `vm:` side is a directory the daemon packs or extracts, the local side is a `.tar` file. `microvms-cli/src/cli.rs:1437-1438`.
* `--mode <OCTAL>` — permissions for an uploaded file, octal as a string; conflicts with `--tar`, since a tar carries its members' own modes. `microvms-cli/src/cli.rs:1445-1446`.
* Plus `AttachFlags` and `RegionFlags`. `microvms-cli/src/cli.rs:1448-1452`.

## sync

```
microvm sync [OPTIONS] --name <NAME> <DIR>
```

Syncs a project directory into a running MicroVM's `/workspace`, incrementally: the first sync uploads the tree and writes a content manifest beside it in the guest; every later sync hashes the local tree, diffs against that manifest, and uploads only the members whose bytes differ. A second sync of an unchanged tree sends no archive — one manifest read, zero uploads. Locally deleted paths are removed in the guest by one argv `rm -rf --` exec rooted in `/workspace`, and deletion paths are validated first: the manifest is the VM's word, and it cannot order deletions outside the workspace. The incremental half of #71; `run <DIR>` is the launch-coupled batch half.

`microvms-cli/src/commands/attached.rs`, `sync`

Flags:

* `<DIR>` — the project directory; packed with `run <DIR>`'s rules (`.git`, `target`, `node_modules`, `.venv` stay home; symlinks travel as links; the daemon's budgets enforced during the walk). Required.
* `--watch` — keep syncing on debounced filesystem changes until Ctrl-C, which resolves into the summary envelope. Events only wake a re-hash; the hash compare decides whether bytes move. With `microvm port-forward` in a second terminal, an edit here is a reload there.
* `--full` — upload the whole tree even where the guest manifest claims members are unchanged; the recovery when a workload edited `/workspace` behind sync's back.
* `--timeout <SECONDS>` — budget for the in-guest deletion exec; default `60`.
* Plus `AttachFlags` and `RegionFlags`.

A disk-pressure refusal (the daemon's 507, with the byte counts in the body) surfaces as `ERR_PLATFORM` with the free-space remedy and `data.diskUnderPressure: true` — never `ERR_RETRYABLE`, because retrying an identical upload against a full disk cannot succeed.

## attach

```
microvm attach --from <FILE|-> [--name <NEW_NAME>] [--verify-identity] [--state-dir <DIR>]
microvm attach --name <NAME> --microvm-id <ID> --endpoint <URL> --agent-token <TOKEN> [--region <R>] [--identity-host-seed <BASE64> --identity-vm-public-key <BASE64>] [--verify-identity]
```

Registers a name for a running MicroVM this state directory did not launch, so every later `--name` command here can act on it. Cross-machine adoption (issue #66): same-machine adoption already exists through the registry `run --keep --vm-name` writes, and what was missing was moving that record between machines. Two spellings of the same facts, mutually exclusive: `--from` reads a name record exactly as the registry writes it (`<state-dir>/names/<name>.json` on the launching machine — the export format is the existing file, so `microvm attach --from <(ssh other cat ~/.microvm/runs/names/ci.json)` needs no export command), and the explicit triple is for a caller holding a `run` envelope from another machine. Nothing is written until the VM has answered one authenticated request with the token being registered.

`microvms-cli/src/commands/attached.rs`, `attach_vm`

Flags:

* `--from <FILE>` — a name record file, or `-` for stdin. The record's own name is registered unless `--name` renames it; its region is the default unless a region flag is given. Identity material in the record is kept: the VM pin is what a later `tunnel --name <NAME> --verify-identity` checks against, and the host seed is in the same trust domain as the agent token beside it. Conflicts with the explicit triple.
* `--name <NAME>` — the name to register. Required with the explicit triple; optional rename with `--from`. Validated like `run --vm-name`.
* `--endpoint <URL>`, `--agent-token <TOKEN>`, `--microvm-id <ID>` — the explicit triple, from a `run` envelope. Required unless `--from` is given.
* `--identity-host-seed <BASE64>`, `--identity-vm-public-key <BASE64>` — the pair from `run --identity`'s envelope, stored on the record for a later verified tunnel. Each requires the other: the Noise KK pattern proves this side too, so the pin alone cannot run a handshake.
* `--verify-identity` — prove the far end is the VM the record was created for before writing anything, with the same Noise KK handshake `tunnel --verify-identity` runs (`microvms-core/src/session/tunnel.rs`, `verify_identity` — the tunnel's open-and-handshake step with no relay after it). Refused up front when the record carries no identity pair, naming the missing field.
* `--port <PORT>` — the daemon's port inside the guest, for the probe. Not recorded.
* `--state-dir <DIR>` — the registry the record is written into; defaults to `$MICROVM_STATE_DIR` or `~/.microvm/runs`.
* Plus `RegionFlags`.

Refusals, each before anything is written:

* A name already registered here to a *different* MicroVM id is `ERR_NAME_TAKEN` (14) before any AWS call, with `data.holder` naming the VM that holds it — the same row and the same reasoning as `run --keep --vm-name`, and a torn record reads as taken. The same VM under the same name is an idempotent success with `replaced: true` and a refreshed record.
* The probe is one authenticated read (`GET /v1/fs/file?path=/dev/null`), not `/v1/health`: health is an open route on the daemon, so a probe that stopped there would accept a wrong token. A dead endpoint, a wrong token (`ERR_CREDENTIALS`, `data.kind: Unauthorized`), or an unbootstrapped daemon (`data.kind: NotBootstrapped`) surfaces with the daemon's own class and leaves the registry untouched.
* `--verify-identity` without the pair, a VM launched without `--identity` (close 4401), or a record replayed from another VM (the daemon's reply does not verify against the pin) is `ERR_PRECONDITION`, and nothing is written.

The envelope is `{name, microvmId, endpoint, region, verifiedIdentity, replaced, statePath}` — never the agent token, which is in the owner-only (0600 on Unix) record file at `statePath`. Text rendering is one line.

A `terminate` releases a name only in the state directory it ran against: a record adopted into a second machine stays there until that machine terminates by the name or deletes the file.

## suspend

```
microvm suspend [OPTIONS] <MICROVM_ID>
```

Freezes a MicroVM, which keeps its memory, filesystem, token, and endpoint. The operation is a freeze and restore rather than a stop and start.

`microvms-cli/src/commands/lifecycle.rs:1908`

Flags:

* `<MICROVM_ID>` — the MicroVM to freeze: a MicroVM id, or a name `run --keep --vm-name` registered (resolved locally, zero extra calls; an unknown name fails with `ERR_PRECONDITION` before any call). Required.
* `--timeout <TIMEOUT>` — how long to wait for the state transition, in seconds; default `300`. `microvms-cli/src/cli.rs:1582-1583`.
* Plus `RegionFlags`. `microvms-cli/src/cli.rs:1589-1590`.

## resume

```
microvm resume [OPTIONS] <MICROVM_ID>
```

Thaws a suspended MicroVM and reports its endpoint. Past the launch-time `suspendedDurationSeconds` window the VM has been terminated, so the resume fails.

`microvms-cli/src/commands/lifecycle.rs:1979`

Flags:

* `<MICROVM_ID>` — the MicroVM to thaw: a MicroVM id, or a registered name (resolved locally). Required.
* `--timeout <TIMEOUT>` — how long to wait for RUNNING, in seconds; default `300`. `microvms-cli/src/cli.rs:1600-1601`.
* Plus `RegionFlags`. `microvms-cli/src/cli.rs:1607-1608`.

## terminate

```
microvm terminate [OPTIONS] <MICROVM_ID>
```

Tears down a MicroVM and optionally its image and build log group. When part of the teardown fails, the command still exits successfully and reports the leaked identifier.

`microvms-cli/src/commands/lifecycle.rs:2050`

Flags:

* `<MICROVM_ID>` — the MicroVM to terminate: a MicroVM id, or a registered name (resolved locally). An accepted terminate releases every name this state directory registered to the VM, whichever spelling addressed it — the launch's own and any `attach` alias — so the names are reusable. Required.
* `--image-identifier <IMAGE_IDENTIFIER>` — the image to delete, if `--delete-image` is given. Omitted, the image is read off the kept run's record in the state directory: `run --keep` wrote the VM, the image, and the image's name there, so the flag is an override rather than a requirement (#160). `microvms-cli/src/cli.rs`, `TerminateArgs::image_identifier`.
* `--image-name <IMAGE_NAME>` — the image's name, needed to name its build log group; the service created that group, so `terraform destroy` never removes it. Omitted, it is read off the same record when one names the image. `microvms-cli/src/cli.rs`, `TerminateArgs::image_name`.
* `--delete-image` — also delete the image and name its build log group. With neither the flag nor a record naming an image, the command is refused locally with `ERR_INVALID_ARG` before any call. After the teardown the run record is narrowed to what is still outstanding — a kept image, a failed delete, and the build log group this CLI can only name — and removed when nothing is, so `ls` stops reporting a VM this command removed. `microvms-cli/src/cli.rs`, `TerminateArgs::delete_image`; `microvms-cli/src/commands/lifecycle.rs`, `terminate`; `microvms-cli/src/ledger.rs`, `Ledger::open_for_vm`.
* `--wait` — wait for TERMINATED rather than returning as soon as the call is accepted. `microvms-cli/src/cli.rs:1639-1640`.
* Plus `RegionFlags`. `microvms-cli/src/cli.rs:1646-1647`.

## ls

```
microvm ls [OPTIONS]
```

Lists the local ledger of a state directory: what this CLI could not confirm it deleted, not what exists in the account. It reads `~/.microvm/runs/*.json` rather than asking AWS, because the ledger still names the resources a killed process never got to delete, and an entry can outlive the resource it names — the measurement behind #159 was 68 entries with populated `leaked` lists in an account both `scripts/verify-clean.py` and the live APIs showed empty. Every `microvm.runs` envelope therefore carries `source: "local-ledger"`, and the text and TUI header reads `local ledger of <state dir>: what this CLI could not confirm it deleted, not what exists in the account`. `microvms-cli/src/commands/local.rs:32-52`.

`--remote` asks the other question. Through the same control plane every other command uses (`CoreSeam::control_plane`; one AWS service, no second client), it reads `ListMicrovms` and `ListMicrovmImages` to their last page and judges each identifier in a ledger entry's `leaked` list — the ledger's own statement of what is outstanding, which teardown narrows to what a delete did not report gone. An identifier found in either listing takes the listed state: alive is every MicroVM state but `TERMINATED` and every image state but `DELETING`/`DELETED`, the predicate `scripts/verify-clean.py` uses, so the two tools cannot disagree about a leak. An identifier absent from both is `gone` only when it is spelled as a MicroVM id (`microvm-…`) or an image ARN (`…:microvm-image:<name>`), the two things the listings could have shown; any other spelling, a service-created `/aws/lambda-microvms/…` log group above all, is something neither listing can see, and its absence says nothing. The entry is then `live` if any identifier is, else `unjudged` if any is, else `gone`; a record written for another region or one that cannot be read is `unjudged` outright. A record whose `leaked` list is empty, the shape a run that created nothing leaves, is judged by its named `microvmId`/`imageIdentifier` instead. `data.remote` is null without the flag and otherwise `{region, microvms: [{microvmId, state, imageArn}], images: [{imageArn, name, state}], entries: [{runId, microvmId, imageIdentifier, microvmState, imageState, status}], unknownToLedger: {microvms, images}}`, where `unknownToLedger` is what is alive in the account that no entry names — the sibling-client case the issue measured. `microvms-cli/src/commands/local.rs:256`.

`--prune` (requires `--remote`) removes the ledger files of `gone` entries and lists their run ids in `data.pruned`, which is `[]` on every other invocation. Only `gone`: a live record is the one the ledger exists for, and an unjudged one names something these listings cannot answer for — for a leaked log group the ledger file is the only pointer there is (`microvms-cli/src/ledger.rs:6-9`), so removing it on the strength of a `TERMINATED` VM beside it would lose the resource. The removal goes through `ledger::remove`, which checks the run id against the ledger's own grammar before it becomes a path component. `microvms-cli/src/ledger.rs:197-209`.

Flags:

* `--state-dir <STATE_DIR>` — where the ledgers live; defaults to `$MICROVM_STATE_DIR` or `~/.microvm/runs`. `microvms-cli/src/cli.rs:1653-1654`.
* `--remote` — list live MicroVMs and images from the account and mark each ledger entry `live`, `gone`, or `unjudged` by the identifiers in its `leaked` list; conflicts with `--watch`, which is ledger-only by contract. `microvms-cli/src/cli.rs:1693-1694`.
* `--prune` — remove the ledger files of `gone` entries; requires `--remote`. `microvms-cli/src/cli.rs:1700-1701`.
* `--region <REGION>` / `--unlisted-region <NAME>` — the region `--remote` lists; the shared `RegionFlags` group, ignored without `--remote`. `microvms-cli/src/cli.rs:1703-1704`.
* `--watch` — re-read the ledger on an interval until Ctrl-C, `port-forward` style: snapshots on stderr, one summary envelope at the end (`data.watch = {refreshes, intervalSeconds, interrupted, calls}`, `null` for a plain `ls`), Ctrl-C exits 0 (#78). The loop is **ledger-only**: zero platform calls per refresh — no `GetMicrovm`, no `/v1/health` — so it resets no idle timer, keeps no VM alive, and bills nothing; `data.watch.calls` states that flatly and the text carries the measurement behind it. The distinction matters because an outside `/v1/health` poll *does* reset the platform idle timer (`docs/PLATFORM.md`, idle-timer section: a polled VM stayed RUNNING through 311s of a 60s window while the unpolled control suspended at 66s), so a health-polling watcher would keep every watched VM billing. The corollary: what a watch shows is what this CLI last recorded, not the platform's live state — a VM the platform suspended at its idle window still reads as this ledger wrote it. `microvms-cli/src/commands/local.rs`, `watch`.
* `--interval-sec <SECONDS>` — seconds between ledger re-reads under `--watch`; default `2`, floor `0.1`. The default can be short *because* the loop is local-only; a health-polling watcher would instead need its interval judged against every watched VM's `maxIdleDurationSeconds`.
* `--max-refreshes <N>` — stop after N snapshots instead of on Ctrl-C, for scripts; `0` is refused.

## history

```
microvm history [OPTIONS] <VM_ID>
```

Prints what was asked of one MicroVM and what the platform reported back: `imageBuilt` (when `run`'s own invocation built the image — a standalone `build` has no VM id and writes no history), `launched`, `exec` per exec, `hookObserved` per lifecycle-hook firing the daemon reported, `suspended`, `resumed`, and `terminated` with the teardown's own verdict. The record is an append-only JSONL file at `<state-dir>/history/<vm-id>.jsonl`, one camelCase object per line with a per-VM `seq` counter and an epoch-seconds `at`, appended by `run`, `exec`, `health`, `suspend`, `resume`, and `terminate` with the same swallowed-failure discipline as the ledger — an unwritable state directory never fails a teardown.

`hookObserved` events (issue #80) carry `hook` — the route's own spelling: `ready`, `validate`, `run`, `suspend`, `resume`, `terminate` — and `firedAt`, epoch seconds on the *daemon's* clock, distinct from the line's own `at` (when this CLI learned of it). They arrive by health poll rather than push: `run`'s teardown fetches health once before terminating, `resume` polls after RUNNING (the only moment a suspend-hook firing can become visible, since a frozen VM cannot answer), and `microvm health` appends whenever it knows the VM id. Every append deduplicates on the (hook, firedAt) pair, so polling twice appends nothing. The validate hook writes no CloudWatch logs, so this record is the only local answer to "did my validate hook even run?".

Unlike the ledger `ls` reads, nothing ever deletes a history file: the record survives terminate on purpose, because a caller attesting over a run needs it precisely after the VM is gone. A VM with no history file is a clean empty result rather than an error — asking about a VM this state dir never saw is a question, not a mistake. A truncated last line (a process killed mid-append) is reported as an unreadable record rather than skipped.

What the record does not prove: it shows what was asked of the VM and what the daemon and the control plane reported back — identifiers, endpoints, exit codes, teardown verdicts, hook firings — not what a process inside the guest did between execs. Every value is the platform's or the daemon's; nothing the guest printed ever reaches a history line, because a record built from guest output would be a record the workload can forge. `hookObserved` lines carry one additive caveat on top of that: the daemon's hook routes are unauthenticated and loopback-reachable from inside the guest, so a hostile workload can make the daemon observe *extra* firings by posting the hook paths itself — it cannot remove or alter real ones, because the daemon records before responding and its capped log keeps the earliest entries, which are the platform's. Read a hook line as "the daemon saw this hook fire", never as "only the platform could have fired it".

`microvms-cli/src/commands/local.rs`, storage in `microvms-cli/src/history.rs`.

Flags:

* `<VM_ID>` — the MicroVM whose history to print: a MicroVM id, or a registered name (resolved locally; an unregistered name reads as a clean empty history, this command's usual answer for an unseen VM). Required.
* `--state-dir <STATE_DIR>` — where the histories live; defaults to `$MICROVM_STATE_DIR` or `~/.microvm/runs`.

## logs

```
microvm logs [OPTIONS] <IMAGE_NAME>
```

Names an image's build log group, `/aws/lambda-microvms/<image-name>`, and prints the working `aws logs tail` invocation that reads it — a success, exit 0. That log group holds the only evidence a failed build leaves behind.

The envelope's `data` carries `tailCommand` (`aws logs tail <group> --since 1h --format short --region <region>`, pinned to the region this invocation resolved so the printed line is runnable regardless of the shell's default) and `tailRequires`: the command needs **AWS CLI v2** — `aws logs tail` does not exist in v1 (verified present in 2.35.7). The identity running the tail also needs `logs:FilterLogEvents`, `logs:GetLogEvents`, and `logs:DescribeLogStreams` on the group; the Terraform stack's `logs_read_policy_arn` output is exactly that grant, ready to attach. The CLI itself never reads CloudWatch — `lines` is explicitly `null`, never `[]`, so a consumer cannot mistake "this client did not read the group" for "the group has no events" (the wrong-prefix signature).

`data` also carries `streams`: three objects labelling the build's log topology by role — `docker-build` (zip pull and docker image build), `snapshot-graviton3`, and `snapshot-graviton4` (the snapshot VMs are the ones that start the app, so application startup logs land there). One build is three VMs and three streams, with random service-chosen stream names by default; a configured `logStream` collapses all three into one exact stream, distinguished across builds only by the per-build `/<16 hex>` suffix this client appends, and the resolved name is on the build envelope's `logStream`. The point of returning the topology here is that an agent handed this envelope knows what to look for inside the group without measuring the platform itself. See `docs/PLATFORM.md`, "An image build is three VMs and three log streams".

`microvms-cli/src/commands/local.rs:696`

Flags:

* `<IMAGE_NAME>` — the image whose log group to name. Required. `microvms-cli/src/cli.rs:1721-1722`.
* Plus `RegionFlags`. `microvms-cli/src/cli.rs:1724-1725`.

## cost

```
microvm cost [OPTIONS]
```

Reports what a run cost or what a plan will cost, with every figure labelled. Dollar figures are estimates derived from published rates, not invoice amounts. A line item with no published rate reads `unpriced` rather than `$0.00`.

`microvms-cli/src/commands/cost.rs:27`

Flags:

* `--estimate` — treat the durations as a plan rather than as timings, so every duration is labelled projected. `microvms-cli/src/cli.rs:1734-1735`.
* `--compare` — also print running versus suspended for the same hold, with the break-even. `microvms-cli/src/cli.rs:1738-1739`.
* `--memory <MEMORY>` — baseline MiB, selecting a documented size class; default `2048`. `microvms-cli/src/cli.rs:1742-1743`.
* `--running-sec <RUNNING_SEC>` — seconds the VM spent, or will spend, RUNNING; billed at baseline whether or not anything is executing. Default `0`. `microvms-cli/src/cli.rs:1749-1750`.
* `--suspended-sec <SUSPENDED_SEC>` — seconds spent suspended; storage only, no compute line. Default `0`. `microvms-cli/src/cli.rs:1753-1754`.
* `--build-sec <BUILD_SEC>` — seconds the image build took; appears as an unpriced line. Default `0`. `microvms-cli/src/cli.rs:1760-1761`.
* `--image-gb <IMAGE_GB>` — image size in GB; adds storage with its one-week minimum retention. `microvms-cli/src/cli.rs:1764-1765`.
* `--cycles <CYCLES>` — suspend/resume cycles, each paying a snapshot write plus a read; default `1`. `microvms-cli/src/cli.rs:1768-1769`.
* `--hold-sec <HOLD_SEC>` — the hold to compare running against suspended over, in seconds; default `3600`. `microvms-cli/src/cli.rs:1772-1773`.
* `--max-cost <USD>` — a budget the report's total is checked against (#77). The comparison is against the *priced* total, which is a lower bound whenever any line is unpriced — so a detected breach has already been exceeded by an unknown margin. The verdict lands in `data.budget` (`{maxUsd, onBreach, basis, breached, overageAtLeastUsd}`, `null` with no budget), in the text, and in the dense rendering; `basis` says whether the compared total was `exact` or `lower-bound`. A breach is always a stderr warning, `--quiet` included. Requires `--on-breach`. `microvms-cli/src/commands/cost.rs`, budget gate.
* `--on-breach <warn|abort>` — what a `--max-cost` breach does, and deliberately without a default: because the compared total can be a lower bound, whether a breach warns (exit 0) or aborts is the caller's judgement. `abort` exits `ERR_PRECONDITION` (12) *after* the full report is written — the same success-envelope-then-non-zero mechanism as `run`'s workload exit — so a CI gate branches on 12 while still receiving the figures. No new exit code: 12 is an existing row, and 12-vs-2 distinguishes "over budget" from "bad flag". `microvms-cli/src/cli.rs`, `OnBreach`.

## doctor

```
microvm doctor [OPTIONS]
```

Checks every prerequisite and says which one is wrong. The checks cover the project config file, credentials, the region, the three Terraform outputs, whether the stack is applied, the managed base images AWS publishes, and whether the daemon binary is aarch64.

The config check runs first because it is the one check that is entirely local, and a malformed file fails `run` before anything else would. `doctor --config ci.toml` validates the exact file `run --config ci.toml` would read, through the same loader, so the two commands cannot disagree about which config applies. A broken file — unparseable, an unknown key, a value outside its flag's domain — is a fatal fail naming the reason; an absent `./microvm.toml` is an advisory pass, because a project configured by flags is not a finding. A valid file's pass line names which knobs it pins. `microvms-cli/src/commands/doctor.rs`'s `check_config`.

`microvms-cli/src/commands/doctor.rs:32`

Two of the checks are about the managed base rather than about this machine, and both are advisory.

`managed-bases` reads `ListManagedMicrovmImages` and answers whether AWS publishes a base this client does not know about. It reports rather than offers, and the reason is a limitation in the shape: `ManagedMicrovmImageSummary` carries an ARN and two timestamps and nothing else — no registry reference, no architecture, no working directory. A `BaseImage` in `microvms-core` pairs the ARN *with* the Dockerfile `FROM` and with whether the image declares a `WORKDIR`, because `require_matching_from` compares a caller's Dockerfile against the first and `require_workdir` refuses inheritance on the second. Neither guard has an input from this listing, and the registry ref is not derivable from the ARN — `al2023-1` pairs with `public.ecr.aws/amazonlinux/amazonlinux:2023-minimal` and nothing in the ARN says so. So a discovered base cannot be built from through this client, and the check says which one it knows.

`base-image-versions` reads `ListManagedMicrovmImageVersions` and prints the values `build --base-image-version` may take. That is the actionable half: an unpinned build takes whatever the service currently defaults to, and that default has already moved once.

Both are advisory because nothing about a build depends on either read succeeding, and a `doctor` that failed on a listing would add a way to be wrong rather than report one. `microvms-cli/src/commands/doctor.rs`'s `check_managed_bases`.

Flags:

* `--binary <BINARY>` — the agentd binary to check the architecture of. `microvms-cli/src/cli.rs:1814-1815`.
* `--infra-dir <INFRA_DIR>` — the Terraform stack directory; defaults to `./conformance/infra`. `microvms-cli/src/cli.rs:1818-1819`.
* Plus `RegionFlags` and `InfraFlags`. `microvms-cli/src/cli.rs:1821-1828`.

## manifest

```
microvm manifest [OPTIONS]
```

Emits the whole command surface, its exit codes, and its envelope schema. The manifest is generated from the registered clap tree rather than maintained by hand, so it cannot drift from what the binary accepts.

`microvms-cli/src/commands/local.rs:785`

The command takes no flags and always emits JSON, because the only consumer that asks for a manifest is one that parses it.

## constants

```
microvm constants [OPTIONS]
```

Emits every service constraint this client believes, for the drift gate that `scripts/check-model-drift.py` runs against the pinned botocore model.

`microvms-cli/src/commands/local.rs:808`

Flags:

* `--emit-json` — emit the raw constants object without an envelope. This is the one stdout write in this binary that is not an envelope. `microvms-cli/src/cli.rs:1839-1840`.

## dockerfile

```
microvm dockerfile [OPTIONS]
```

Prints the Dockerfile stanza that wraps any base image with agentd. The stanza is what `microvm build` bakes when no `--dockerfile` is given, so appending your own `RUN` layers to this output and passing the result to `build --dockerfile` is the default build plus your layers. The text comes from `microvms-core`'s own generator rather than a copy, so it cannot drift from what a default build produces. `microvms-cli/src/commands/local.rs:843`, reusing `microvms-core/src/control/artifact.rs:145`.

The output's comments name the two platform constraints a hand-written wrapper hits. The `FROM` must match the managed base's `docker_ref`, because the build runs the Dockerfile on top of the base that `baseImageArn` names and `microvms-core` refuses a Dockerfile whose `FROM` disagrees. And a `WORKDIR` is required when the base declares none — the managed al2023 base does not, so without one every relative path resolves against `/`. `microvms-core/src/control/artifact.rs:228-244`, `microvms-core/src/control/artifact.rs:196-220`.

This is a local command: no account is involved, and the stanza is built from compile-time constants.

Flags:

* `--from <IMAGE_REF>` — the image ref for the `FROM` line; defaults to the managed al2023 base's pair, `public.ecr.aws/amazonlinux/amazonlinux:2023-minimal`. Only change this when you are also changing `baseImageArn`. `microvms-cli/src/cli.rs:1845-1852`.
* `--port <PORT>` — the port agentd listens on inside the guest; default `9000`. Reaches both `ENV AGENTD_PORT` and `EXPOSE`. `microvms-cli/src/cli.rs:1854-1856`.
* `--workdir <DIR>` — a working directory to create and set, as both a `RUN mkdir -p` and a `WORKDIR`. Strongly recommended, because the managed base declares no WorkingDir. `microvms-cli/src/cli.rs:1858-1864`.

The JSON envelope carries the stanza text plus the base image pair — `baseImageName` for deriving `baseImageArn` and `baseImageDockerRef` for the `FROM` — so a consumer holds both halves of the agreement the platform enforces. `microvms-cli/src/commands/mod.rs:455-465`.

For the full recipe — appending tool layers, building, and driving the daemon from your own harness — see [docs/EMBEDDING.md](../internals/embedding.md).

## Project config: microvm.toml

`run` and `doctor` read an optional `./microvm.toml` beside the invocation (or the file `--config <PATH>` names). Every key in it already exists as a `run` flag; the file adds no capability, only persistence — `microvm run` in a configured project needs zero flags. `microvms-cli/src/config.rs`.

```toml
image = "coding-agents"       # run --image
binary = "target/agentd"     # run [BINARY]
exec = "make test"           # run --exec
memory = 4096                # run --memory; same closed set as the flag
region = "us-west-2"         # any region string, resolved where the flag's is
egress = true                # run --egress
# deny-egress = true         # run --deny-egress; advisory. Refused beside egress = true,
                             #   so this line and the one above are alternatives, never both
auto-resume = true           # run --auto-resume
max-idle-sec = 600           # run --max-idle-sec
suspended-sec = 600          # run --suspended-sec
max-duration-sec = 3600      # run --max-duration-sec
artifacts = ["dist/**"]      # run <DIR> artifact globs (issue #72); validated here
log-group = "/aws/lambda-microvms/ci-builds"  # run --log-group; the build role must be able to write here
log-stream = "ci-image"      # run --log-stream; a PREFIX — the client appends /<16 hex> per build (issue #98)
[env]                        # run --launch-env, as a table; per-key merge, flag wins its key
CI = "1"
```

Key names are the flag names with `-` for `_`, so the file reads like the command line it replaces. All keys are optional; an absent key means the flag or the built-in default decides. A relative `binary` path resolves against the config file's own directory, not the process cwd — `--config /repo/microvm.toml` exists precisely for the invoke-from-elsewhere case, and `target/agentd` must mean the same binary from anywhere. An unknown key is refused by name rather than silently ignored — `memroy = 4096` launching a 2 GB VM is the failure that closes. A value outside its flag's domain (`memory = 1500`, an artifact glob that will not compile) is refused with the same closed-set reasoning the parser enforces, so the file cannot be a side door past the flag domains.

On Windows, two `binary` shapes are refused at load because they mean two things at once (issue #87): a rooted path with no drive (`/opt/agentd` — Windows parses it as relative, so the file-directory join would silently re-anchor it onto the config file's drive) and a drive with no root (`C:agentd` — resolves against that drive's current directory at spawn time). The remedy differs per intent — write the drive letter, or write a genuinely relative path — so the loader refuses rather than guesses. On Unix neither shape exists and `/opt/agentd` is simply absolute.

`--timeout` is deliberately not a config key: it is a client-side wait, not a property of the VM the project wants to pin.

A file that cannot be used is `ERR_CONFIG` (exit 15), refused locally before any billable call. The remedy differs from `ERR_INVALID_ARG`'s — edit (or `--no-config` bypass) a file the invocation may never have named, rather than edit the command line — which is why it has its own row.

## The JSON envelope

Each invocation writes exactly one JSON object on stdout. Progress always goes to stderr. `--quiet` suppresses progress but not warnings, so a leaked resource is still reported even in quiet mode. `microvms-cli/src/envelope.rs:1-18`. `apiVersion` is `"1"`; it is bumped when a field's meaning changes, not when a command is added. `microvms-cli/src/envelope.rs:66`.

A success envelope carries `status`, `apiVersion`, `type`, and `data`. `type` is the discriminant to branch on first. `microvms-cli/src/envelope.rs:311-318`.

```
{
  "status": "ok",
  "apiVersion": "1",
  "type": "microvm.run",
  "data": { }
}
```

A failure envelope carries `status`, `apiVersion`, `error`, `code`, `exitCode`, `finding`, `suggestions`, and `data`. Every field is always present, so a consumer never has to guard against a missing key. `microvms-cli/src/envelope.rs:323-342`.

```
{
  "status": "error",
  "apiVersion": "1",
  "error": "human readable, may be reworded between releases",
  "code": "ERR_PROTOCOL",
  "exitCode": 5,
  "finding": "",
  "suggestions": [],
  "data": { "kind": "Conflict" }
}
```

Branch on `code` rather than `error`; the `error` text may be reworded between releases while `code` is stable. `microvms-cli/src/manifest.rs:120-121`.

### data.kind

`data.kind` carries the daemon's own status name when the exit code is coarser than the failure. Five `WireKind`s (`Conflict`, `NotFound`, `ProtocolError`, `StdinClosed`, `TooLarge`) all map to `ERR_PROTOCOL`. Collapsing them is deliberate. A shell branching on `$?` cannot act differently on a 400 than on a 409, and a consumer that can act differently reads `data.kind` instead. `microvms-cli/src/exit.rs:39-44`, inserted at `microvms-cli/src/envelope.rs:326-328`, and pinned at `microvms-cli/src/exit.rs:594-627`.

A request rejected locally reports no `data.kind`, because nothing reached the daemon. `microvms-cli/src/envelope.rs:445-450`.

### Response types

Each command declares its `type` discriminant and the `data` keys its success envelope carries. `microvms-cli/src/commands/mod.rs:102-466`.

| Command | `type` |
| --- | --- |
| `run` | `microvm.run` |
| `quickstart` | `microvm.run` |
| `build` | `microvm.image` |
| `agent-up` | `microvm.agent` |
| `agent-prompt` | `microvm.agent.prompt` |
| `exec` | `microvm.exec` |
| `health` | `microvm.health` |
| `ack` | `microvm.exec` |
| `kill` | `microvm.kill` |
| `ps` | `microvm.procs` |
| `stdin` | `microvm.stdin` |
| `cp` | `microvm.copy` |
| `sync` | `microvm.sync` |
| `attach` | `microvm.attach` |
| `tunnel` | `microvm.tunnel` |
| `port-forward` | `microvm.port-forward` |
| `shell` | `microvm.shell` |
| `suspend` | `microvm.state` |
| `resume` | `microvm.state` |
| `terminate` | `microvm.teardown` |
| `ls` | `microvm.runs` |
| `history` | `microvm.history` |
| `logs` | `microvm.logs` |
| `cost` | `microvm.cost` |
| `doctor` | `microvm.doctor` |
| `manifest` | `microvm.manifest` |
| `constants` | `microvm.constants` |
| `dockerfile` | `microvm.dockerfile` |

## The NDJSON stream exception

`exec --stream` is the one invocation that writes more than one object to stdout. Under `--json` (or into a pipe asking for it) it emits NDJSON, meaning one JSON object per event and then the envelope as the final line. This is a second, narrower contract that sits alongside the one-envelope rule. Three things keep the two contracts distinguishable. `microvms-cli/src/envelope.rs:35-54`.

First, the discriminant differs. A streamed exec's final envelope has `type` `microvm.exec.stream`, while a non-streamed exec has `microvm.exec`. A consumer branching on `type` learns which parse applies from the field it already reads first. `microvms-cli/src/commands/mod.rs:468-494`.

Second, the manifest publishes it. `exec`'s entry carries an `alternateResponse` object naming `when: "--stream"`, the `responseType`, the `responseKeys`, and a `stdout` description of the NDJSON shape. The entry is generated from the flag's presence in the command tree, so removing `--stream` from `exec` removes the entry too. `microvms-cli/src/manifest.rs:50-71`.

Third, the envelope is written compact once a stream has started, because "the last line is the envelope" is only true if the envelope is one line. A pretty-printed envelope at the end of an NDJSON stream would parse as several broken records. `microvms-cli/src/envelope.rs:171-175`.

```
{"event":"output","stream":"stdout","offset":0,"bytes":12,"text":"hello world\n","lossy":false}
{"event":"exit","exitCode":0,"signal":null,"truncated":false,"writersMayBeAlive":false,"offset":12}
{"status":"ok","apiVersion":"1","type":"microvm.exec.stream","data":{"execId":"x-1","events":2,"bytes":12,"nextOffset":12,"gaps":0,"exitCode":0,"truncated":false}}
```

Three event kinds reach a line: `output` (with `stream`, `offset`, `bytes`, `text`, `lossy`), `gap` (with `from` and `to`), and `exit` (with `exitCode`, `signal`, `truncated`, `writersMayBeAlive`, `offset`). `microvms-cli/src/commands/attached.rs:492-531`.

Output arrives as lossy text beside the true byte count rather than as base64. `lossy` is set when the conversion actually replaced anything, so a consumer can tell when the text differs from the original bytes. The non-JSON path carries the exact bytes. `microvms-cli/src/commands/attached.rs:480-511`.

The stream envelope's keys summarize the stream. The output itself was the NDJSON events, and repeating it in the envelope would double a stream's memory cost for a consumer that has already seen every byte. `events` and `bytes` let a caller assert it read everything, and `nextOffset` is where a resume with `--from-offset` would continue. `microvms-cli/src/commands/mod.rs:468-494`.

The events are the command's output, not progress about it, so they cannot go to stderr even though that would preserve the simpler one-envelope rule. Sending a workload's stdout to the caller's stderr would make `microvm exec --stream build.sh > log` write an empty log. Buffering the events to keep stdout a single document would remove the only reason to stream. `microvms-cli/src/envelope.rs:51-54`.

The non-JSON formats emit no NDJSON at all. The raw child bytes go to stdout untouched, with no lossy string conversion applied. `microvms-cli/src/envelope.rs:232-247`.

A stream that fails part-way through has already written events and no envelope. On the JSON path the failure envelope becomes the stream's compact last line, because an NDJSON consumer reading line by line needs a terminating record saying why the events stopped. On the human paths the same failure goes to stderr instead, because appending an error message to the child's raw output would corrupt the file a caller was redirecting into. `microvms-cli/src/main.rs:353-367`.

A stream that ended without an exit event was cut. `exitCode` is reported as `null` rather than `0`, because reporting zero would turn a truncated stream into a passing build. The command exits `ERR_EXEC_FAILED`. `microvms-cli/src/commands/attached.rs:455-474`.

## Exit codes

The table has seventeen rows, 0 through 16, and is append-only because consumers branch on the values. The rows are split by what the caller should do next; a distinction that does not change the caller's next action does not get its own integer. `microvms-cli/src/exit.rs:196-309`, with the enum's explicit discriminants at `microvms-cli/src/exit.rs:76-133`.

| Exit | Code | Meaning | `docs/PLATFORM.md` finding |
| --- | --- | --- | --- |
| 0 | — | the command did what it said | |
| 1 | `ERR_UNEXPECTED` | an exception no handler claimed — a bug in this CLI, not the platform | |
| 2 | `ERR_INVALID_ARG` | the request was refused locally, before any AWS call | |
| 3 | `ERR_RETRYABLE` | a transient condition; run the identical command again | Endpoint authentication |
| 4 | `ERR_CREDENTIALS` | an identity is wrong or absent; waiting will not fix it | |
| 5 | `ERR_PROTOCOL` | the daemon rejected the request on its merits | |
| 6 | `ERR_BUILD_WEDGED` | the image build was never scheduled — the clientToken replay signature | `clientToken` is a permanent idempotency key |
| 7 | `ERR_LAUNCH_DIED` | the MicroVM reached a terminal state before RUNNING; read stateReason | `runHookPayload` arrives wrapped, not as the body |
| 8 | `ERR_WINDOW_CLOSED` | the launch-time suspended window passed, so there is nothing to resume | `idlePolicy` |
| 9 | `ERR_PLATFORM` | a control-plane failure with no more specific class | |
| 10 | `ERR_TIMEOUT` | a client-side deadline elapsed; the VM and the exec are untouched | |
| 11 | `ERR_INTERRUPTED` | interrupted after launch; teardown ran and any leak is named in the payload | The build log group survives Terraform |
| 12 | `ERR_PRECONDITION` | a prerequisite is missing — run `microvm doctor` | |
| 13 | `ERR_EXEC_FAILED` | the sandbox worked and the command in it exited non-zero | |
| 14 | `ERR_NAME_TAKEN` | the VM name is registered to a live VM; refused locally, before any AWS call | |
| 15 | `ERR_CONFIG` | the project config file is missing, malformed, or out of domain; refused locally — fix the file, or pass `--no-config` | |
| 16 | `ERR_SYNC` | `run <DIR>` could not pack the directory or write an artifact back; the failure is on this machine's filesystem, not the platform's | |

Row 0 is the only one with no `ERR_*` string, because a success envelope has no `code` field to put one in. `microvms-cli/src/exit.rs:52-59`.

`ERR_NAME_TAKEN`, `ERR_CONFIG`, and `ERR_SYNC` are the rows with no core `ErrorKind` behind them: the name registry, the config file, and `run <DIR>`'s pack/extract are the CLI's own filesystem work, so these refusals can only arise locally. Each is distinct from `ERR_INVALID_ARG` because the next actions differ — an invalid argument is fixed by editing the flag, a taken name by terminating the VM that holds it or picking another name, a broken config by editing (or `--no-config` bypassing) a file the invocation may never have named, and a sync failure by fixing the local file or disk the pack or extraction named. `ERR_SYNC` is also distinct from `ERR_EXEC_FAILED`: a CI caller needs to tell "the sync plumbing failed here" from "your tests failed" from "the platform failed".

`ERR_EXEC_FAILED` has its own code because it is the one non-zero exit that means nothing is wrong with the platform, the credentials, or the CLI. A CI caller needs to tell "your tests failed" apart from "we never got a VM". `microvms-cli/src/exit.rs:96-101`.

A clap parse failure maps to exit 2 / `ERR_INVALID_ARG`, forwarding clap's own message verbatim including its did-you-mean line. This deliberately matches clap's own convention, so a caller who reads `$?` sees the same number either way. `microvms-cli/src/exit.rs:434-442`, `microvms-cli/src/cli.rs:56-59`.

`--help` and `--version` are successes that print themselves and exit 0, never becoming envelopes. `microvms-cli/src/main.rs:95-108`.

### Suggestions

The exit code comes from the failure class and nothing else. The CLI adds the suggestion on top. The library reports what went wrong, and the CLI names the flag or command that addresses it. Two failures sharing `ERR_CREDENTIALS` therefore get different remedies. A 401 names the agent token. An unresolvable credential chain names `microvm doctor` and the unsupported-region null-message signature. `microvms-cli/src/exit.rs:369-426`.

## Conventions

`microvm manifest` publishes six conventions alongside the command tree. `microvms-cli/src/manifest.rs:129-146`.

* Exactly one envelope object on stdout per invocation; progress is on stderr.
* Branch on `code`, never on `error`.
* Dollar figures are estimates derived from published rates, never an invoice.
* An unpriced line item omits `usd` rather than reporting zero.
* `data.kind` carries the daemon's own status name when the exit code is coarser than the failure (`ERR_PROTOCOL` covers five).
* `exec --stream` is the one exception to the first line: it writes NDJSON with the discriminant `microvm.exec.stream`. Every other invocation writes exactly one object.

## Closed option domains

Two options carry a closed set rather than free text, and the parser refuses everything else before any handler runs. Refusing an off-table value in the parser reports the error immediately, while refusing it in `microvms-core` would cost a build cycle first. `microvms-cli/src/cli.rs:4-19`.

`--memory` accepts exactly `512`, `1024`, `2048`, `4096`, `8192`, the five documented size-class baselines. `microvms-cli/src/cli.rs:361-373`.

`--region` accepts exactly `us-east-1`, `us-east-2`, `us-west-2`, `eu-west-1`, `ap-northeast-1`, the five regions measured to carry MicroVMs. `eu-central-1` is excluded on measurement. `microvms-cli/src/cli.rs:412-424`.

The escape hatch is a separate flag rather than a permissive parser. `--unlisted-region <NAME>` conflicts with `--region` and carries its cost in its help text, so a reader of a command line can see that someone opted in. `microvms-cli/src/cli.rs:31-37`.

Four option names — `--client-token`, `--capabilities`, `--connector`, and `--architecture` — are deliberately absent. Three of them because `microvms-core` has no parameter for the values they would carry; `--connector` because the intent is spelled `--egress` (the managed connector) or `--egress-network-connector <ARN>` (an existing VPC connector, see above). A test asserts the absence of all four names over every argument of every subcommand. `microvms-cli/src/cli.rs:21-29`, `microvms-cli/src/cli.rs:2911-2935`.

In the manifest, a boolean flag reports `type: "boolean"` and `choices: null` even though clap gives a `SetTrue` flag the possible values `["true", "false"]`. Publishing those would put a `choices` array on every flag and make the closed-domain field unreadable. `microvms-cli/src/manifest.rs:151-170`.

## See also

* [microvms-agentd · Processes](../internals/behavior/processes.md)
* [microvms-agentd · Contract map](../internals/insights/contract-map.md)
* [microvms-agentd · Debugging guide](../internals/insights/debugging-guide.md)
* [microvms-agentd · Data flow](../internals/architecture/data-flow.md)
* [microvms-agentd · Impact analysis](../internals/insights/impact-analysis.md)