Sandboxed Exec
mvmctl exec is the “send off a single task in a microVM” workflow.
It boots a fresh transient Firecracker microVM, runs one command via
the guest agent, streams stdout/stderr back to your terminal, propagates
the exit code, and tears the VM down — success, failure, or Ctrl-C.
Think docker run --rm, but with a microVM as the isolation boundary.
mvmctl exec -- uname -amvmctl exec --add-dir .:/work -- ls /workmvmctl exec --manifest my-tpl -- /bin/truemvmctl exec --launch-plan ./launch.json
mvmctl execis dev-mode only — the guest agent’s Exec handler is compiled in only when thedev-shellCargo feature is enabled. Production guest builds omit the feature, so the handler is not present in the binary at all andexecis physically unavailable. It is not meant for production workloads; usemvmctl up(ormvmd) for those.
When to use it
Section titled “When to use it”- Reach for
mvmctl execwhen you want to run an untrusted binary, a build script, an LLM-generated command, or any one-shot task that benefits from a strong isolation boundary but doesn’t justify standing up a long-running VM. - Reach for
mvmctl upwhen you want a long-running VM you can re-enter, share state with, or expose ports from. - Reach for
mvmctl console <vm> --command "..."when you already have a VM running and want to run something inside it without a fresh boot.
The bundled default image
Section titled “The bundled default image”If you don’t pass --manifest or --flake, mvmctl exec boots the
bundled default image:
- A minimal
mkGuestrootfs (busybox-as-PID-1 + the supervised guest agent) shipped with mvm itself. Internally it lives atnix/profiles/minimal.nixin the workspace, but you should treat it as opaque infrastructure — to customize the image you write your own flake usingmvm.lib.<system>.mkGuest(see Building MicroVM Images + Dev Image) instead of editing mvm internals. - Built via Nix on first use, cached at
~/.cache/mvm/default-microvm/(kernel + rootfs). - Identical for every invocation that doesn’t pass
--manifest.
If your host has no working Nix builder, mvmctl exec will fail with a
clear error. Pass --manifest <name> (a registered template you’ve
already built) to skip the Nix path.
Sharing host directories: --add-dir
Section titled “Sharing host directories: --add-dir”--add-dir HOST:GUEST[:MODE] materializes the host directory into a
small ext4 image, attaches it as an extra Firecracker drive, and mounts
it at GUEST inside the microVM. MODE is ro (default) or rw. The
flag is repeatable.
Read-only (default)
Section titled “Read-only (default)”echo "hello" > /tmp/foomvmctl exec --add-dir /tmp:/host -- cat /host/foo # prints "hello"The guest sees the contents at boot. Writes inside the guest are discarded when the microVM is torn down.
Writable: :rw
Section titled “Writable: :rw”mvmctl exec --add-dir .:/work:rw -- sh -c 'echo result > /work/output.txt'cat ./output.txt # "result" — written by the guestThe mount is read-write inside the guest. Once the command exits and
the VM stops, the ext4 image is mounted host-side and rsync -aH --delete-ed back over the host directory. New files appear, modified
files are updated, and files removed inside the guest are removed on
the host.
This is the equivalent of cco’s writable project directory — exactly what you want for a coding agent that needs to edit your repo.
Trade-offs
Section titled “Trade-offs”See ADR-002 for the full design rationale. Highlights for v1:
- No in-flight host visibility. Guest writes only land on the host
after the command exits. Host-side
tail -f-style tooling won’t see partial output. - Last-writer-wins on concurrent host writes. If you modify a file on the host while the guest is also modifying it, the guest’s version overwrites the host’s at teardown. v1 is for agentic flows where the host isn’t editing the same directory in parallel.
- No incremental durability. A 30-minute task that crashes at
minute 29 loses all guest writes — the rsync only runs after a clean
exit. Keep
mvmctl execfor short-lived invocations; long-lived workloads belong inmvmd. - Guest deletes propagate. The rsync uses
--delete, so files removed inside the guest are removed on the host.
For a live two-way mount (visible during the run, no clobber semantics),
virtio-fs is on the v2 roadmap once Firecracker ships upstream
vhost-user-fs support.
Multiple shares
Section titled “Multiple shares”Modes are independent per directory:
mvmctl exec \ --add-dir ./src:/work:rw \ --add-dir ~/.cargo:/root/.cargo:ro \ -- cargo build --manifest-path /work/Cargo.tomlInjecting environment variables: --env
Section titled “Injecting environment variables: --env”mvmctl exec --env FOO=bar --env BAZ=qux -- env | grep -E '^(FOO|BAZ)='--env (or -e) is repeatable. When used together with --launch-plan,
CLI --env overrides any env vars the launch plan carries (see below).
Snapshot restore (registered templates)
Section titled “Snapshot restore (registered templates)”When you pass --manifest <name> and that template has a captured
snapshot, mvmctl exec restores from the snapshot instead of cold-booting.
This skips the kernel boot and service-start cost — typically sub-second
on Linux/KVM.
The snapshot path activates only when:
- the image source is a registered template (the bundled default has no template snapshot to restore from), AND
- the request has no
--add-dirextras (extra drives would mismatch the snapshot’s recorded layout), AND - the active backend reports snapshot support.
On macOS backends without Firecracker (Apple Container, libkrun), vsock snapshots return os error 95 (EOPNOTSUPP);
restore failures fall back to cold boot with a warning rather than
aborting. The harder branch — parameterized snapshots that allow
--add-dir — is tracked in issue #7.
Resource controls
Section titled “Resource controls”mvmctl exec --cpus 4 --memory 1G -- ./benchmark.shmvmctl exec --timeout 300 -- ./long-running-task.shDefaults: 2 vCPUs, 512 MiB, 60-second timeout per command.
Driving from a launch plan
Section titled “Driving from a launch plan”mvmctl exec --launch-plan <path> accepts either of two JSON
shapes — a launch.json artifact (top-level entrypoint) or a
Workload IR manifest (top-level apps[]) — and auto-detects which
one it is given. Both shapes were historically produced by the
mvmforge toolchain
(see the migration guide);
the canonical producer today is mvmctl compile in the mvm SDK.
mvmctl compile manifest.json --out ./buildmvmctl exec --launch-plan ./build/launch.jsonOnly the entrypoint is consumed in v1; image selection still comes from
--manifest or the bundled default.
LaunchPlan artifact (top-level entrypoint):
{ "artifact_format_version": "1.0", "workload_id": "hello", "entrypoint": { "command": ["python", "main.py"], "working_dir": "/app", "env": { "PORT": "8080" } }, "env": { "LOG_LEVEL": "info" }}Workload IR manifest (top-level apps[]):
{ "apps": [ { "name": "hello", "entrypoint": { "command": ["python", "main.py"], "working_dir": "/app", "env": { "PORT": "8080" } }, "env": { "LOG_LEVEL": "info" } } ]}For long-running workloads, prefer mvmctl up --flake <artifact-dir>:
the SDK bakes the entrypoint into the generated flake’s
services.<id>.command, and mvm’s PID-1 init supervises it across
reboots.
Multi-app launch plans are rejected — that’s an orchestration concern
that belongs in mvmd, not in mvmctl exec. Env precedence (lowest →
highest):
apps[].envapps[].entrypoint.env- CLI
--env(always wins)
--launch-plan is mutually exclusive with a trailing argv.
Teardown semantics
Section titled “Teardown semantics”- Normal exit: VM is stopped and the staging dir for
--add-dirimages is cleaned up. - Non-zero exit: same as normal exit;
mvmctl execpropagates the guest’s exit code. - Ctrl-C: a SIGINT handler triggers teardown so the Firecracker process and any tap interface don’t get orphaned.
- Hard kill (
kill -9onmvmctl execitself): teardown is best-effort; you may needmvmctl lsandmvmctl down <name>to clean up. Each transient VM is namedexec-<pid>-<rand>so they’re easy to spot.
Limits
Section titled “Limits”- Dev-mode only.
mvmctl execrequires a guest agent built with thedev-shellCargo feature, which is the default for the dev imagesmvmctlships with. Production guest images omit the feature and the Exec handler is physically absent from the binary. - Network access. The guest gets the same network configuration
any other transient VM gets — if your
--manifestexposes outbound internet, so doesmvmctl execfrom that template. - Stdin is currently not forwarded to the guest. Pipe data via a
--add-dir-shared file instead. Streaming stdin is a future improvement. - Persistent state doesn’t survive teardown beyond what
:rw--add-dirrsyncs back. For larger or longer-lived state, usemvmctl upwith a persistent volume.
See also
Section titled “See also”- CLI reference: One-shot Exec
- Manifests guide — build a reusable base image
via
mvm.toml;mvmctl exec [PATH]accepts the manifest path directly - Quick Start