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:
npx oxidevm --helpPaths in arguments and flags resolve from the current directory.
Commands
Section titled “Commands”| 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. |
Global flags
Section titled “Global flags”| 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.
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" }.
assets install
Section titled “assets install”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. |
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.jsonbeside 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 withINVALID_ARGUMENT. - It stages the verified engine release under
<publicDir><assetBaseUrl>runtime/. WithassetBaseUrl: "/oxidevm/", that ispublic/oxidevm/runtime/, the defaultruntime.baseUrlofcreateVM. - Without
--imageor--snapshot, it stages the SDK’s pinned default image for the project’s mode. - With
--image, it verifies the bundle and stages it underimages/<manifest sha256>/in the asset base.--referencethen writes a module whose default export is thedefineImage(...)reference to pass asimage. - 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 undersnapshots/<bundle digest>/. It writes an options module, by defaultsrc/generated/<name>.snapshot.ts, whose default export you spread intocreateVMoptions. - It also stages the
doctor/files thatoxidevm doctorloads, and aproject.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 }.
doctor
Section titled “doctor”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.
image init
Section titled “image init”oxidevm image init <directory> --mode userspace|systemCreates <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.
image keygen
Section titled “image keygen”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 }.
image build
Section titled “image build”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. |
dockerfileandocisources need Docker;dockerfilesources also need a running Buildx (BuildKit) builder. System images also needmke2fsanddebugfsfrom 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 }.
image test
Section titled “image test”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.
image inspect
Section titled “image inspect”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 }.
image pack
Section titled “image pack”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.
snapshot commands
Section titled “snapshot commands”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? }.
snapshot init
Section titled “snapshot init”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 }.
snapshot build
Section titled “snapshot build”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.
snapshot pack
Section titled “snapshot pack”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.
snapshot test
Section titled “snapshot test”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.
snapshot inspect
Section titled “snapshot inspect”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".
Snapshot failures
Section titled “Snapshot failures”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.
Engine release
Section titled “Engine release”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:
--engine-dir <release>;- the
OXIDEVM_ENGINE_DIRenvironment variable; - 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.
Output and JSON
Section titled “Output and JSON”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 printsCODE: message, also on stdout. - Progress always goes to stderr.
commandisnullwhen 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_ERRORwith a generic message, so raw tool output, which can contain secrets, never reaches the report. @oxidevm/cliexportsdecodeCliReport(value)and theCliReporttypes, to read reports in scripts.
The code values are the SDK’s error codes.
Exit codes
Section titled “Exit 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. |
Environment variables
Section titled “Environment variables”| 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. |