Skip to content

CLI

The @oxidevm/cli package provides the oxidevm command. It runs on Node.js 24 or newer, on your development machine or in CI. Install it as a development dependency at the same version as @oxidevm/sdk, and run it through your package manager:

Terminal window
npx oxidevm --help

Paths in arguments and flags resolve from the current directory.

Command Purpose
init <directory> Create a new browser app with the SDK set up.
assets install Stage the pinned engine release and an image or a prepared start into your app.
doctor [url] Check a running or deployed app in a real browser.
image init <directory> Create an image recipe.
image keygen Create a signing key and its public trust file.
image build <recipe> Build and sign an image bundle.
image test <bundle> Test a built image in a real browser.
image inspect <bundle> Check a bundle’s integrity without running it.
image pack <bundle> Add download packs to a signed Userspace bundle.
snapshot init [directory] Create a prepared-start recipe.
snapshot build [recipe] Prepare Linux state in a browser and publish it as a signed bundle.
snapshot pack <backup> Publish an existing .oxvm backup as a signed bundle.
snapshot test <bundle> Restore a bundle in a browser and verify it.
snapshot inspect <artifact> Check a bundle or a backup without running it.
Flag Meaning
--json Accepted by every command: print one JSON report on stdout. See Output and JSON.
--help Print usage. oxidevm snapshot <command> --help prints the usage of the snapshot commands.
--version Print the CLI and SDK versions and the ID of the pinned engine release.

--help and --version cannot be combined with each other or with --json. An unknown flag, a flag the command does not take, a repeated flag (other than --secret) or a missing argument fails with INVALID_ARGUMENT and exit code 2.

Terminal window
oxidevm init <directory> --mode userspace|system [--skip-assets | --engine-dir <release>]
Flag Meaning
--mode Required. userspace for mode: "userspace", system for mode: "system".
--skip-assets Only create the files; run oxidevm assets install later.
--engine-dir <release> A local copy of the pinned engine release. Cannot be combined with --skip-assets.

Creates <directory>, which must not exist yet, with a Vite and TypeScript app, including package.json with the SDK and CLI at this CLI’s version, oxidevm.config.ts, oxidevm.lock.json, a vite.config.ts that sends the isolation headers, and src/main.ts with a button that runs a Linux command. Unless --skip-assets is given, it then runs assets install. It does not install dependencies.

Result: { directory, mode, assets: "installed" | "skipped" }.

Terminal window
oxidevm assets install [--config <file>] [--image <bundle> | --snapshot <bundle> --trust <file>] [--reference <file>] [--engine-dir <release>]
Flag Meaning
--config <file> The project config, default oxidevm.config.ts. A .json file also works.
--image <bundle> Stage this image bundle, from image build, instead of the default image.
--snapshot <bundle> Stage this prepared-start bundle, from snapshot build or snapshot pack. Cannot be combined with --image.
--trust <file> The prepared start author’s public trust file. Only with --snapshot, which needs it here or in OXIDEVM_SNAPSHOT_TRUST_FILE.
--reference <file> Where to write the generated TypeScript module. Requires --image or --snapshot.
--engine-dir <release> A local copy of the pinned engine release.
oxidevm.config.ts
import type { ProjectConfig } from "@oxidevm/sdk/image-builder";
export default {
mode: "userspace", // or "system"
publicDir: "public", // default "public"
assetBaseUrl: "/oxidevm/", // default "/oxidevm/", must end with "/"
} satisfies ProjectConfig;

What it does:

  • On its first run, it writes oxidevm.lock.json beside the config. The lock binds the exact SDK, CLI, engine release, image and staging layout; commit it. Later runs validate the lock and never replace it. After an SDK upgrade, delete the lock and run the command again: a lock written for another SDK version fails with INVALID_ARGUMENT.
  • It stages the verified engine release under <publicDir><assetBaseUrl>runtime/. With assetBaseUrl: "/oxidevm/", that is public/oxidevm/runtime/, the default runtime.baseUrl of createVM.
  • Without --image or --snapshot, it stages the SDK’s pinned default image for the project’s mode.
  • With --image, it verifies the bundle and stages it under images/<manifest sha256>/ in the asset base. --reference then writes a module whose default export is the defineImage(...) reference to pass as image.
  • With --snapshot, it checks the bundle’s signature with the trust file and its mode, SDK and engine against the project lock, and stages it under snapshots/<bundle digest>/. It writes an options module, by default src/generated/<name>.snapshot.ts, whose default export you spread into createVM options.
  • It also stages the doctor/ files that oxidevm doctor loads, and a project.json.
  • Staged image and prepared-start files are immutable: an existing file with other bytes fails with PROTOCOL_ERROR. Output never follows symbolic links, and a generated module never overwrites a file the CLI did not generate (PERMISSION_DENIED).

Result: { publicDir, files, referencePath }.

Terminal window
oxidevm doctor [url] [--browser chromium|firefox|webkit] [--preview-origin <origin>]
Argument or flag Meaning
url The app page to check, default http://localhost:5173/. An http: or https: URL without credentials.
--browser chromium (default), firefox or webkit.
--preview-origin <origin> Also check this preview origin. Pass the bare origin without a trailing slash, such as https://preview.example.com: https:, or http: on localhost or a loopback address.

Doctor opens the page in a headless browser with a fresh profile, finds the asset base in <meta name="oxidevm-assets" content="..."> (default /oxidevm/) and reports every check:

Check Passes when
secure-context The page is a secure context.
cross-origin-isolation The response carries Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp (or credentialless), and the page is cross-origin isolated.
wasm-features The browser has the WebAssembly and JavaScript features the engine needs.
asset-integrity The engine modules under runtime/ match the pinned release, and the staged doctor/ files and project.json match the installed SDK.
wasm-mime The engine modules are served as application/wasm.
worker-load The SDK’s module workers load and answer.
content-security-policy The engine modules compile and a worker starts under the page’s Content Security Policy.
storage The browser reports a storage quota and can write and read an OPFS file.
preview-origin With --preview-origin: the preview bootstrap loads, registers its Service Worker and requests a transport. Without it: not-configured.

Result: { result: "passed" | "failed", url, checks }, each check being { check, status: "passed" }, { check, status: "failed", message } or { check: "preview-origin", status: "not-configured" }. A failed check exits with code 3.

Terminal window
oxidevm image init <directory> --mode userspace|system

Creates <directory>, which must not exist yet, with a Dockerfile on a digest-pinned Alpine base, a typed recipe oxidevm.image.ts, image.lock.json, .gitignore, .dockerignore and README.md. A System recipe also selects a kernel and a disk size. Result: { directory, recipePath, mode }. See Custom images.

Terminal window
oxidevm image keygen --out <author.pem>

Writes an Ed25519 private key in PKCS#8 PEM to --out (file mode 0600) and its public trust file beside it: author.pem gives author.public.json, containing { algorithm: "Ed25519", keyId, publicKey } (file mode 0644). Neither file may exist yet. Keep the private key out of your repository and out of browser code; the public trust file is what --trust flags and defineImage take. Result: { privateKeyPath, publicKeyPath, keyId }.

Terminal window
oxidevm image build <recipe.ts> --signing-key <author.pem> [--out <directory>] [--base-url <url>] [--distribution] [--secret <id>=<file>]
Argument or flag Meaning
<recipe.ts> The recipe module. Its default export is a UserspaceImageRecipe or SystemImageRecipe.
--signing-key <file> The author’s private key. Required here or in OXIDEVM_SIGNING_KEY_FILE.
--out <directory> The bundle directory, default dist beside the recipe.
--base-url <url> The directory URL the bundle will be served from, ending in /. The bundle’s generated reference points at <url>manifest.json. Default /oxidevm/images/<id>/<manifest sha256>/.
--distribution Build for distribution. The recipe must name its evidence inputs and a sourceDateEpoch; the image is built twice, both builds must be identical, and the evidence is checked before the bundle is signed.
--secret <id>=<file> Passes a BuildKit secret to a Dockerfile build. id is 1 to 64 letters, digits, _ or -. Repeatable.
  • dockerfile and oci sources need Docker; dockerfile sources also need a running Buildx (BuildKit) builder. System images also need mke2fs and debugfs from e2fsprogs.
  • The bundle is published atomically; a failed build keeps the previous bundle.
  • Build phases (resolve, build-rootfs, validate-files, package, sign, reproduce, done) are printed to stderr, one per line.

Result: { mode, manifestSHA256, bundleDir, referencePath, lockPath, distribution }. referencePath is a generated <id>.image.ts in the bundle. distribution is { kind: "development", missing }, listing the evidence a distribution build would still need, or { kind: "candidate", browserTestsRequired: true }.

Terminal window
oxidevm image test <bundle> --trust <author.public.json> [--browser chromium|firefox|webkit] [--distribution] [--engine-dir <release>]
Flag Meaning
--trust <file> The author’s public trust file. Required here or in OXIDEVM_IMAGE_TRUST_FILE.
--browser chromium (default), firefox or webkit.
--distribution Also run the license-evidence and reproducible-build checks.
--engine-dir <release> A local copy of the pinned engine release.

Runs the image in a real browser, with commandInterceptors: "off" for Userspace, and reports the checks asset-integrity, manifest-signature, browser-features, runtime-ready, workspace-read-write, smoke-command (the recipe’s smokeTest), terminal-interrupt, persistence-reopen, preview, license-evidence and reproducible-build. A check that does not apply is not-applicable, with reason "not-declared" or "mode-unsupported".

Result: { schemaVersion, result, mode, manifestSHA256, engineRelease, sdkVersion, browser, checks }. A failed check exits with code 3. A successful Docker build alone does not show that the image’s programs run in the browser engine; the smoke-command check runs the recipe’s command there.

Terminal window
oxidevm image inspect <bundle> [--trust <author.public.json>]

Checks the bundle’s files against its manifest without running anything. Without --trust, authentication is { kind: "unverified", keyId }. With it, the signature and every referenced file are verified and it is { kind: "verified", keyId }. Result: { mode, id, version, manifestSHA256, artifactCount, artifactBytes, authentication }.

Terminal window
oxidevm image pack <bundle> --trust <author.public.json>

For an existing signed Userspace bundle, writes deterministic download packs and transport/<manifest sha256>/packs-v1.json beside the unchanged manifest and lock. Publish the additions in the image’s directory and keep the original files reachable. A System bundle fails with INVALID_ARGUMENT. --trust is required.

A prepared start has a recipe that runs in Node.js and a preparation and a verification module that run in the browser through the public SDK. Sign prepared starts with their own key, separate from the image signing key. Applications open them with shippedSnapshot.

Build, pack and test accept --browser chromium|firefox|webkit, --timeout-ms <ms> (a time limit for the command, a positive integer) and --engine-dir <release>. Their progress goes to stderr: the phase name per line, or with --json one JSON object per line, { buildId, sequence, phase, completedBytes?, totalBytes? }.

Terminal window
oxidevm snapshot init [directory] [--name <name>] [--mode userspace|system] [--image <bundle>] [--image-trust <file>]
Argument or flag Meaning
[directory] Default snapshots/ready. Must not exist yet.
--name <name> The prepared start’s name, default ready: a lowercase letter followed by up to 63 lowercase letters, digits or -.
--mode Default userspace. With --image, the image’s mode applies, and a different --mode fails.
--image <bundle> Prepare on this image instead of the default image. Needs the image’s trust in --image-trust or OXIDEVM_IMAGE_TRUST_FILE.
--image-trust <file> The image author’s public trust file.

Writes oxidevm.snapshot.ts (the recipe), prepare.ts, verify.ts, tsconfig.snapshot.json, README.md and .gitignore. Result: { directory, recipePath, mode, name, files }.

Terminal window
oxidevm snapshot build [recipe] [--signing-key <file>] [--image-trust <file>] [--out <directory>] [--base-url <url>]

Runs the preparation in a real browser, captures the result, verifies that it restores, and publishes a signed bundle.

Argument or flag Meaning
[recipe] Default oxidevm.snapshot.ts.
--signing-key <file> The prepared start author’s private key. Required here or in OXIDEVM_SIGNING_KEY_FILE.
--image-trust <file> Trust for a custom image. When absent, the recipe’s imageTrust and then OXIDEVM_IMAGE_TRUST_FILE apply.
--out <directory> The bundle directory; default the recipe’s outDir, else dist beside the recipe.
--base-url <url> Where the bundle will be served; default the recipe’s publicBaseUrl, else /.

--browser defaults to the recipe’s browser, else chromium, and --timeout-ms replaces the recipe’s timeouts.buildMs.

Terminal window
oxidevm snapshot pack <backup.oxvm> [--recipe <recipe>] [--signing-key <file>] [--image-trust <file>] [--out <directory>] [--base-url <url>]

Publishes an existing .oxvm backup, such as one from vm.snapshots.export, as a signed bundle. It uses the recipe (default oxidevm.snapshot.ts) for the image and the verification module, verifies the restore in a browser, then publishes. The other flags work as for snapshot build.

Terminal window
oxidevm snapshot test <bundle> [--trust <file>] [--verify <module>]

Restores the bundle in a fresh browser profile and runs its bundled verification module, or --verify <module> instead. The author’s trust is required, in --trust or OXIDEVM_SNAPSHOT_TRUST_FILE. The checks are bundle-signature, image-integrity, snapshot-integrity, browser-features, shipped-resume, authoring-receipt, original-processes and custom-verification. A failed check exits with code 3. --browser defaults to chromium.

Terminal window
oxidevm snapshot inspect <bundle-or-backup> [--trust <file>]

Checks a bundle or a .oxvm backup without running author code or restoring anything. With trust, from --trust or OXIDEVM_SNAPSHOT_TRUST_FILE, a bundle’s signature is verified. The result’s kind is "bundle" or "backup".

A failed snapshot command reports message: "Snapshot command failed; check its phase and reason" and a detail with whichever of these apply:

Field Values
phase validate, compile, start, stage, prepare, ready, capture, export, package, verify, publish, done
reason snapshot-recipe-invalid, snapshot-input-unsafe, snapshot-process-exited, snapshot-command-failed, snapshot-readiness-timeout, snapshot-command-exit, snapshot-command-timeout, snapshot-ready-timeout, snapshot-capture-timeout, snapshot-build-timeout, snapshot-verify-failed, snapshot-unexpected-cold-boot, snapshot-browser-unavailable, snapshot-preparation-failed, snapshot-publication-incomplete, snapshot-initial-conflict
check The failed check of the verification.
cause ENOSPC, EDQUOT, EIO or EACCES.
process The name of the prepared process involved.
checkpoint { phase, domain, owner?, causes } when the engine refused the capture.
browserMessage The SDK error message from the browser, one printable line of at most 500 characters.

Text output prints them on one line: CODE: Snapshot command failed (phase ..., reason ...): <browser message>. snapshot-verify-failed exits with code 3.

The commands that stage or run the engine (init without --skip-assets, assets install, image test, snapshot build, snapshot pack and snapshot test) need the exact engine release the installed SDK pins. They take it from, in order:

  1. --engine-dir <release>;
  2. the OXIDEVM_ENGINE_DIR environment variable;
  3. the engine cache, $XDG_CACHE_HOME/oxidevm/engine/<release id>, by default ~/.cache/oxidevm/engine/<release id>.

Every source is verified: release.json must hash to the release ID, and each module and the engine LICENSE file must have their pinned size and SHA-256. An explicit directory that is not the pinned release fails; nothing falls back past it. When no verified copy is available, the command fails with CORE_LOAD_FAILED and names the release ID.

CliReport
type CliReport =
| { [K in CliCommand]: { schemaVersion: 1; kind: "result"; command: K; result: CliResultMap[K] } }[CliCommand]
| {
schemaVersion: 1;
kind: "error";
command: CliCommand | null;
error: { code: VMErrorCode; message: string; detail?: Record<string, unknown> };
};
  • With --json, every run prints exactly one report on one line of stdout, for success and failure alike.
  • Without --json, a successful command prints the same report as indented JSON, except the snapshot commands, which print one sentence. A failure prints CODE: message, also on stdout.
  • Progress always goes to stderr.
  • command is null when the arguments could not be parsed.
  • Errors that do not come from the SDK, such as raw file system or tool errors, are reported as STORAGE_ERROR with a generic message, so raw tool output, which can contain secrets, never reaches the report.
  • @oxidevm/cli exports decodeCliReport(value) and the CliReport types, to read reports in scripts.

The code values are the SDK’s error codes.

Code When
0 The command succeeded, or --help or --version printed.
1 The command failed with any error not listed below.
2 INVALID_ARGUMENT, including invalid usage, or FEATURE_NOT_AVAILABLE. For snapshot commands also CORE_LOAD_FAILED.
3 A check ran and failed: doctor, image test or snapshot test reported "result": "failed", or a snapshot build or pack failed its restore verification (snapshot-verify-failed).
130 Interrupted by SIGINT, for example Ctrl+C. The command cleans up what it started and reports ABORTED.
143 Interrupted by SIGTERM, with the same cleanup.
Variable Used by Meaning
OXIDEVM_ENGINE_DIR commands that need the engine release A local copy of the pinned engine release, used when --engine-dir is absent. An empty value is ignored.
XDG_CACHE_HOME engine and image caches Base directory of the caches: oxidevm/engine/<release id> and oxidevm/images/<manifest sha256> below it. For the engine cache it must be an absolute path. Default ~/.cache.
OXIDEVM_SIGNING_KEY_FILE image build, snapshot build, snapshot pack The private key, when --signing-key is absent.
OXIDEVM_IMAGE_TRUST_FILE image test, snapshot init, snapshot build, snapshot pack The image author’s public trust file, when --trust (image test) or --image-trust is absent. For build and pack, a recipe’s imageTrust takes precedence.
OXIDEVM_SNAPSHOT_TRUST_FILE snapshot test, snapshot inspect, assets install --snapshot The prepared start author’s public trust file, when --trust is absent.