Install: set up smolvm and prove the host boots

Installs smolvm from a published release and proves the host can actually boot a microVM before any other work starts. Use when setting smolvm up on a new machine, a CI runner or an agent’s own environment; when a first boot fails with krun_start_enter -22, KVM_DENIED or “agent did not become ready”; when checking whether a host meets smolvm’s requirements at all; or when an install has to be isolated from an existing one and then removed. Do not use it to remove an existing install (see the teardown packet) or for anything after the first boot has succeeded.

Verified on smolvm v1.18.2 on macOS arm64 and Linux aarch64, 2026-09-24. Done means smolvm --version prints the release version and a throwaway VM has run one command and exited. A version number alone proves nothing: on every platform here there is at least one way for the install to succeed and every VM start to fail.

scripts/preflight.sh reports the host as key=value lines and ends with result=ready, result=not_installed or result=blocked. Run it first, and run it again after the install if the first boot fails.

Procedure

1. Preflight. Read-only. It starts no VM and writes no smolvm state.

scripts/preflight.sh

result=not_installed on a fresh host means the host is fit and smolvm is simply missing: go on to step 2 and run this again afterwards. Stop on result=blocked and read the note= lines. The two that block a fresh host are accel_access=denied on Linux (your user cannot open /dev/kvm) and socket_path_status=too_long on macOS (the install path is too deep for a VM’s Unix socket). Both have a fix in references/traps.md, and neither announces itself later: the installer warns about KVM and continues, and the path limit surfaces as an error about disks.

2. Install from the published release.

curl -sSL https://smolmachines.com/install.sh | bash
export PATH="$HOME/.local/bin:$PATH"
smolvm --version

Install the newest release. The installer with no --version takes the latest published release, and a later release is expected to work with this packet. The version in the banner above is what the packet was last verified on, not what you should install. Pin only to reproduce a recorded run:

curl -sSL https://smolmachines.com/install.sh | bash -s -- --version 1.18.2   # a recorded run

On macOS two warning: lines about notarization appear on every install and are not a problem. On Linux info: KVM access verified appears only when your user can already open /dev/kvm.

To install without touching an existing one, point HOME at a scratch directory: every path smolvm uses moves with it on macOS and Linux. Keep that directory shallow on macOS. This does not work on Windows, where state cannot be relocated at all. See references/layout.md.

Windows does not use this installer. See references/windows.md.

3. Prove it boots. This is the step that decides whether smolvm works here.

scripts/verify-boot.sh

It runs one ephemeral alpine VM and asserts two values: a marker the guest printed, and that the guest kernel is not the host’s. On failure it prints the three misreadings that cost the most time, before you clean up and lose the evidence.

It boots a 2048 MiB guest, and a machine you create without --mem asks for 8192. A host can pass this step and still fail every default-size machine with agent did not become ready within 30 seconds; if your work uses the default, boot one at that size too. references/traps.md has the measurement.

4. Clean up.

scripts/cleanup.sh

It waits before asserting an empty machine list, because a successful machine run returns before its entry retires and an immediate assertion fails on a healthy host. It then reports VM processes an interrupt left behind, and kills them only with --reap.

What the preflight reports

keymeaning
smolvm_installed, smolvm_versionwhether the binary is on PATH and what it says
verified_version, version_statusmatch, newer, older or unknown against the version this packet was last verified on. newer is the expected state on a current host and is not a failure
platformdarwin-aarch64, linux-aarch64, linux-x86_64
accel, accel_accesshvf and kern.hv_support, or kvm and whether /dev/kvm is readable and writable
macos_version, hardware_verifiedhardware_verified=no on an Intel Mac: the installer accepts it and nothing here was run on one
socket_path_bytes, socket_path_statusmacOS only, and the single most common cause of “macOS is broken”
unsupportedfeatures this platform does not have
resultready, not_installed (the host is fit and smolvm is missing) or blocked

version_status=newer is a warning, not a failure. smolvm’s flags and messages move every release, so on a newer binary check each step’s output against the binary before trusting the text here.

Security defaults, and why they are the defaults

  • The installer is a user-level install and needs no root. Everything lands under $HOME, which is what lets an agent or a CI job install a private copy and remove it without a privileged step. Nothing in this packet escalates privilege.
  • sudo usermod -aG kvm is the one privileged step, and it is yours to run. Group membership on /dev/kvm is the host’s boundary between users who can start VMs and users who cannot, so a script should report accel_access=denied and stop rather than widen it for you. sg kvm -c then applies the group to a single command instead of your whole session.
  • The uninstaller leaves ~/.config/smolvm and your PATH line on purpose. Those hold registry credentials and a change you made to your own shell profile, so removing them is a separate, deliberate act. references/layout.md has both commands.

Platform arms

  • macOS arm64: verified. The path-length rule in references/traps.md applies to every install.
  • Linux aarch64 and x86_64: verified. The kvm group check applies to every fresh host.
  • Intel Mac: unverified. The installer accepts macOS 11 or later on Intel and nothing in the material behind this packet was run on one. preflight.sh reports hardware_verified=no there rather than implying it works.
  • Windows x86_64: references/windows.md, re-run on 2026-09-11 against v1.14.6 on Windows 11 Home build 10.0.26200.0 UBR 9445, where it confirmed. The three facts that break a Unix-shaped script are there: the zip unpacks into a nested versioned folder, state lives in %LOCALAPPDATA%\smolvm and cannot be moved, and a script must never capture machine start output because it never returns.

Eval prompts, and what they produced

Run against this packet on 2026-09-07 PT, on smolvm v1.14.2 installed from the published release into an isolated HOME. Output is verbatim.

1. “Install smolvm on this machine and tell me whether it can actually run a VM.”

macOS 26.6.2 arm64:

smolvm_installed=yes
smolvm_version=1.14.2
version_status=match
platform=darwin-aarch64
accel=hvf
accel_access=ok
socket_path_bytes=62
socket_path_status=ok
result=ready

  BOOTED_OK
  Linux 6.12.95 aarch64
guest_ran=yes
guest_kernel=Linux 6.12.95
host_kernel=Darwin 25.6.0
is_a_vm=yes
result=boot_ok

Lima linux-kvm, Ubuntu 24.04 aarch64:

platform=linux-aarch64
accel=kvm
accel_access=ok
result=ready

  BOOTED_OK
  Linux 6.12.95 aarch64
guest_kernel=Linux 6.12.95
host_kernel=Linux 6.8.0-139-generic
is_a_vm=yes
result=boot_ok

Boot plus image pull took 8.9 s on macOS and 22.4 s on the nested-virt Linux box.

2. “smolvm is installed but every machine run fails with krun_start_enter returned: -22. What is wrong?”

Reproduced deliberately on macOS by installing into a 49-character HOME. The preflight names the cause before any VM is started:

socket_path_bytes=104
socket_path_status=too_long
note=HOME is too deep: every VM start will fail with krun_start_enter -22, whose text blames disks and device options. Install under a shorter HOME.
result=blocked

and the boot then fails exactly as reported, with the misleading text:

Error: agent operation failed: start machine: agent operation failed: monitor agent:
agent operation failed: start vm: krun_start_enter returned: -22 (EINVAL ... libkrun
rejected the VM configuration; usually a disk/overlay that could not be opened ... or an
unsupported device option) (boot process exited (code 1) before the agent was ready)
guest_ran=no
is_a_vm=no
result=boot_failed

3. “Set up smolvm somewhere throwaway so it does not touch my existing install, then remove it.”

Both isolated installs in this session ran under a scratch HOME and the uninstaller then reported every path removed, with find "$HOME" -iname '*smolvm*' empty afterwards:

success: Removed <HOME>/.smolvm
success: Removed symlink <HOME>/.local/bin/smolvm
success: Removed data directory <HOME>/Library/Application Support/smolvm
success: Removed cache directory <HOME>/Library/Caches/smolvm
warning: You may want to remove the PATH entry from your shell profile.
success: smolvm has been uninstalled

Re-verified on v1.14.6

Run 2026-09-10 PT against v1.14.6 from the published release, into a fresh isolated HOME on macOS 26.6.2 arm64 and Lima linux-kvm (Ubuntu 24.04 aarch64). Both hosts: result=ready, then guest_ran=yes, guest_kernel=Linux 6.12.95, is_a_vm=yes, result=boot_ok, and a clean cleanup. The guest kernel is unchanged across 1.14.2, 1.14.3 and 1.14.6.

Re-verified on v1.18.2

Run 2026-09-24 PT against v1.18.2 from the published release, into a fresh isolated HOME on macOS 26.6.2 arm64 and Lima linux-kvm (Ubuntu 24.04 aarch64), with --version 1.18.2. Both hosts: result=ready, then guest_ran=yes, guest_kernel=Linux 6.12.95, is_a_vm=yes, result=boot_ok, and machines=clean, vm_processes=none from the cleanup. The boot took 9.8 s on macOS and 30.7 s on Linux, the image pull included. The smolvm-bin the installer laid down hashed identical to the one in the release tarball (34b13aae2e126998444b85cd78553f047a2376eeab48715399eaa3443d0e726f on macOS), so the installer serves the published release.

The Linux box passed this packet and could not boot a default-size machine. On the same day it booted 512, 1024 and 2048 MiB guests and timed out at 2560 MiB and above, with v1.16.1 on the same box doing the same, so the cause is the host and not the release. verify-boot.sh asks for 2048 MiB and passed. See the second cause under the readiness trap in references/traps.md.

What was not run

  • Intel Mac. Nothing.
  • Windows. references/windows.md records a run on Windows 11 Home build 26200 that was not repeated here. No PowerShell script ships with this packet for that reason.
  • The unprivileged Windows symlink path. The Windows preflight check for Developer Mode or SeCreateSymbolicLinkPrivilege is written from reading the release’s extraction path and from a session that already held the privilege. The failure it guards against has been reported from outside and reproduced by forcing the state, but the unprivileged install itself has not been run by anyone here.
  • Linux x86_64 was verified for install and boot in the material behind this packet, on a cloud GPU instance that no longer exists. The scripts here were re-run on macOS arm64 and Linux aarch64 only.
  • teardown for the full removal sequence, and for what a leak check must exclude.
  • throwaway-machine for running untrusted work in a throwaway machine, which assumes this boot.
  • dev-env, local-api, docker-in-machine, gpu-cuda and pack all assume it too.

Scripts

The files the procedure runs, in the order it runs them. It calls each one by the path in its heading, relative to the directory the procedure is saved in.

scripts/preflight.sh

#!/usr/bin/env bash
# Report whether this host can install and boot smolvm. Read-only: starts no VM,
# writes no smolvm state, changes no group membership.
#
# Output is one key=value per line so a caller can parse it. The last line is
# always result=ready, result=not_installed or result=blocked.

set -uo pipefail

VERIFIED_VERSION="1.18.2"

emit() { printf '%s=%s\n' "$1" "$2"; }

blocked=0
note() { printf 'note=%s\n' "$1"; }

# --- the binary --------------------------------------------------------------

SMOLVM="${SMOLVM:-$(command -v smolvm 2>/dev/null)}"
missing=0
if [ -z "$SMOLVM" ]; then
    emit smolvm_installed no
    emit smolvm_version ""
    missing=1
else
    emit smolvm_installed yes
    version="$("$SMOLVM" --version 2>/dev/null | awk '{print $NF}')"
    emit smolvm_version "${version:-unknown}"
fi

emit verified_version "$VERIFIED_VERSION"
if [ -n "${version:-}" ] && [ "$version" != "unknown" ]; then
    if [ "$version" = "$VERIFIED_VERSION" ]; then
        emit version_status match
    else
        newest="$(printf '%s\n%s\n' "$version" "$VERIFIED_VERSION" | sort -V | tail -1)"
        if [ "$newest" = "$version" ]; then
            emit version_status newer
            note "this packet was verified on $VERIFIED_VERSION and the binary is $version; flags and messages move every release, so check the output against the binary before trusting a step here"
        else
            emit version_status older
            note "this packet was verified on $VERIFIED_VERSION and the binary is $version"
        fi
    fi
else
    emit version_status unknown
fi

# --- platform ----------------------------------------------------------------

kernel="$(uname -s)"
arch="$(uname -m)"
case "$arch" in aarch64|arm64) arch=aarch64 ;; esac

case "$kernel" in
    Darwin)
        emit platform "darwin-$arch"
        emit accel hvf
        emit macos_version "$(sw_vers -productVersion)"
        hv="$(sysctl -n kern.hv_support 2>/dev/null)"
        if [ "$hv" = "1" ]; then emit accel_access ok; else emit accel_access denied; blocked=1; fi
        if [ "$arch" != "aarch64" ]; then
            emit hardware_verified no
            note "Intel Mac is not verified by this packet; the installer accepts it and nothing here was run on one"
        else
            emit hardware_verified yes
        fi
        # A VM's agent socket lives under the cache directory. macOS sockaddr_un
        # holds 104 bytes including the terminator.
        sock="$HOME/Library/Caches/smolvm/vms/0123456789abcdef/agent.sock"
        len=${#sock}
        emit socket_path_bytes "$len"
        if [ "$len" -gt 100 ]; then
            emit socket_path_status too_long
            blocked=1
            note "HOME is too deep: every VM start will fail with krun_start_enter -22, whose text blames disks and device options. Install under a shorter HOME."
        else
            emit socket_path_status ok
        fi
        emit unsupported "cuda"
        ;;
    Linux)
        emit platform "linux-$arch"
        emit accel kvm
        emit socket_path_status n_a
        if [ ! -e /dev/kvm ]; then
            emit accel_access missing
            blocked=1
            note "/dev/kvm does not exist; this host has no KVM"
        elif [ -r /dev/kvm ] && [ -w /dev/kvm ]; then
            emit accel_access ok
        else
            emit accel_access denied
            blocked=1
            note "your user cannot open /dev/kvm. The installer warns and continues, so a successful install says nothing about whether a VM will start. Fix: sudo usermod -aG kvm \$USER, then run the next command through sg kvm -c '...' rather than logging out."
        fi
        emit unsupported "vulkan"
        ;;
    *)
        emit platform "unsupported-$kernel"
        emit accel unknown
        emit accel_access unknown
        blocked=1
        note "this script covers macOS and Linux. On Windows use references/windows.md, which is written from a run and not re-run by this packet."
        ;;
esac

# Not installed yet is not a fault of the host, and reading it as one stopped a
# first install that had nothing wrong with it.
if [ "$blocked" -ne 0 ]; then
    emit result blocked
elif [ "$missing" -ne 0 ]; then
    note "smolvm is not installed yet and nothing else blocks this host: install it (step 2), then run this again"
    emit result not_installed
else
    emit result ready
fi

scripts/verify-boot.sh

#!/usr/bin/env bash
# Prove the install can actually boot a VM. This is the step that decides
# whether smolvm works here; `smolvm --version` printing a number does not.
#
# Runs one ephemeral alpine VM, asserts a marker the guest printed and that the
# guest kernel is not the host's, then hands off to cleanup.sh.

set -uo pipefail

SMOLVM="${SMOLVM:-$(command -v smolvm 2>/dev/null)}"
if [ -z "$SMOLVM" ]; then
    printf 'smolvm not found; set SMOLVM to its path\n' >&2
    exit 2
fi

host_kernel="$(uname -sr)"
marker="BOOTED_OK"

out="$("$SMOLVM" machine run --mem 2048 --net --image alpine -- \
      sh -c "echo $marker && uname -srm" 2>&1)"
printf '%s\n' "$out" | sed 's/^/  /'

fail=0

# Assert the marker, not the exit code. smolvm exits zero on paths where the
# guest never ran the command.
if printf '%s' "$out" | grep -q "^$marker$"; then
    printf 'guest_ran=yes\n'
else
    printf 'guest_ran=no\n'
    fail=1
fi

guest_kernel="$(printf '%s' "$out" | grep -m1 '^Linux ' | awk '{print $1" "$2}')"
printf 'guest_kernel=%s\n' "${guest_kernel:-none}"
printf 'host_kernel=%s\n' "$host_kernel"
if [ -n "$guest_kernel" ] && [ "$guest_kernel" != "$host_kernel" ]; then
    printf 'is_a_vm=yes\n'
else
    printf 'is_a_vm=no\n'
    fail=1
fi

if [ "$fail" -eq 0 ]; then
    printf 'result=boot_ok\n'
else
    printf 'result=boot_failed\n'
    printf 'next: read the failure before cleaning up, because the evidence is deleted with the VM directory.\n'
    printf '  - "agent did not become ready within 30 seconds" is usually host load or a guest too large for this host, not the install. The 30s limit is fixed and no flag raises it for machine run or machine start; retry with a smaller --mem to tell the two apart.\n'
    printf '  - "krun_start_enter returned: -22" on macOS is almost always the socket path length, not the disks its text names. Run preflight.sh and read socket_path_status.\n'
    printf '  - the boot child sends its own output to /dev/null. To see the real failure, copy <vm-dir>/boot-config.json while a start is in flight and run: smolvm-bin _boot-vm <copy>\n'
fi

exit "$fail"

scripts/cleanup.sh

#!/usr/bin/env bash
# Delete the machines this packet's scripts created, then prove the host is clean.
#
# Only machines recorded in the state file are deleted, so a machine you or
# another session created by hand is never touched. Scripts record a name by
# calling: cleanup.sh --record <name>
#
# usage: cleanup.sh [--record <name>] [--reap] [--purge]
#   --record <name>  add a machine name to the state file and exit
#   --reap           kill leftover VM processes (see the warning it prints)
#   --purge          also remove the state file once the list is empty

set -uo pipefail

PACKET="install"
PREFIX="smolskill-"

SMOLVM="${SMOLVM:-$(command -v smolvm 2>/dev/null)}"
STATE_DIR="${SMOLVM_SKILL_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/smolvm-skills}"
STATE_FILE="$STATE_DIR/$PACKET.machines"

reap=0
purge=0
while [ $# -gt 0 ]; do
    case "$1" in
        --record)
            mkdir -p "$STATE_DIR"
            printf '%s\n' "$2" >> "$STATE_FILE"
            exit 0
            ;;
        --reap)  reap=1 ;;
        --purge) purge=1 ;;
        *) printf 'unknown argument: %s\n' "$1" >&2; exit 2 ;;
    esac
    shift
done

if [ -z "$SMOLVM" ]; then
    printf 'smolvm not found; set SMOLVM to its path\n' >&2
    exit 2
fi

case "$(uname -s)" in
    Darwin) VMS_DIR="$HOME/Library/Caches/smolvm/vms" ;;
    *)      VMS_DIR="${SMOLVM_DATA_DIR:-$HOME/.cache/smolvm}/vms" ;;
esac
VMS_DIR="${SMOLVM_VMS_DIR:-$VMS_DIR}"
SMOLVM_PREFIX="${SMOLVM_PREFIX:-$HOME/.smolvm}"

# List this HOME's smolvm VM processes, as "pid marker".
#
# Two process shapes exist and a reaper has to catch both. The plain
# `machine run` path EXECS a child whose argv[1] is `_boot-vm` and whose argv[2]
# is its boot-config path. The pack-run path, which is `--oci-cache` or any
# `init`, FORKS without execing, so the child inherits the parent's argv and
# carries no boot-config at all. Matching `_boot-vm` alone is therefore blind to
# exactly the path whose child survives an interrupt
# (smol-machines/smolvm#1193): measured on v1.14.6, it reported "none" while two
# orphaned VMs held 234 MB each.
#
# On Linux both shapes rename themselves to `libkrun VM`, the one marker that
# covers both and that no shell can hold. macOS exposes no rename, so there the
# executable path scopes the search to this HOME and the parent chain separates
# a VM from the CLI that started it.
#
# `pgrep -f _boot-vm` is not an alternative: it matches any shell whose text
# contains that string, including this script.
list_vm_processes() {
    case "$(uname -s)" in
        Linux)
            for p in /proc/[0-9]*; do
                [ "$(cat "$p/comm" 2>/dev/null)" = "libkrun VM" ] || continue
                pid="${p#/proc/}"
                cfg="$(tr '\0' '\n' < "$p/cmdline" 2>/dev/null | sed -n '3p')"
                case "$cfg" in
                    "$VMS_DIR"/*) printf '%s %s\n' "$pid" "$cfg"; continue ;;
                esac
                # Forked shape: nothing in argv identifies it, so scope by the
                # binary it is running.
                case "$(readlink "$p/exe" 2>/dev/null)" in
                    "$SMOLVM_PREFIX"/*) printf '%s forked-under %s\n' "$pid" "$SMOLVM_PREFIX" ;;
                esac
            done
            ;;
        Darwin)
            # shellcheck disable=SC2009  # pgrep cannot return ppid and the full
            # command together, and pgrep -f matches this script's own text.
            own=" $(ps -axo pid=,command= 2>/dev/null | grep -F "$SMOLVM_PREFIX/smolvm-bin" | awk '{print $1}' | tr '\n' ' ') "
            ps -axo pid=,ppid=,command= 2>/dev/null | while read -r pid ppid rest; do
                case "$rest" in "$SMOLVM_PREFIX"/smolvm-bin*) ;; *) continue ;; esac
                case "$rest" in
                    *" _boot-vm "*) printf '%s %s\n' "$pid" "${rest#* _boot-vm }"; continue ;;
                esac
                # Forked shape: its parent is the CLI that started it, or init
                # once that CLI is gone.
                if [ "$ppid" = 1 ]; then
                    printf '%s orphaned-under %s\n' "$pid" "$SMOLVM_PREFIX"
                else
                    case "$own" in *" $ppid "*) printf '%s forked-under %s\n' "$pid" "$SMOLVM_PREFIX" ;; esac
                fi
            done
            ;;
    esac
}

# 1. Delete recorded machines. --force is not optional: without it the command
# prompts, defaults to No, and leaves the machine in place while the script
# carries on. --cascade removes branch children, which otherwise block the
# delete.
if [ -s "$STATE_FILE" ]; then
    while read -r name; do
        [ -n "$name" ] || continue
        case "$name" in "$PREFIX"*) ;; *)
            printf 'skipping %s: not created by this packet (no %s prefix)\n' "$name" "$PREFIX"
            continue ;;
        esac
        "$SMOLVM" machine stop   --name "$name" >/dev/null 2>&1
        "$SMOLVM" machine delete --name "$name" --force --cascade 2>&1 | sed 's/^/  /'
    done < "$STATE_FILE"
fi

# 2. An ephemeral machine's entry retires after the run returns, not with it.
# Asserting an empty list immediately fails on a healthy host.
sleep 20

# 3. Assert the value, not the exit code.
listing="$("$SMOLVM" machine list 2>&1)"
if printf '%s' "$listing" | grep -q 'No machines found'; then
    printf 'machines=clean\n'
    [ "$purge" -eq 1 ] && rm -f "$STATE_FILE"
else
    printf 'machines=remaining\n'
    printf '%s\n' "$listing" | sed 's/^/  /'
    # These were not created by this packet, so nothing here will remove them.
    # Say what does, rather than leaving the reader to guess: delete prompts and
    # defaults to No without --force, and a branched machine also needs --cascade.
    printf 'note=this packet did not create these, so it will not delete them. By name:\n'
    printf '  smolvm machine stop --name <NAME> && smolvm machine delete --name <NAME> --force\n'
    printf '  add --cascade for a machine that was branched from another\n'
fi

# 4. Report VM processes an interrupt left behind. Ctrl-C does not stop a
# machine: the VM outlives the CLI and `machine list` cannot see it, so this is
# the only route to it. Only processes whose boot config lives under this HOME's
# smolvm state are listed, so a VM another session started is left alone.
found=0
while read -r pid cfg; do
    [ -n "$pid" ] || continue
    found=1
    printf 'vm_process=%s config=%s\n' "$pid" "$cfg"
    if [ "$reap" -eq 1 ]; then
        kill -9 "$pid" 2>/dev/null && printf '  killed %s\n' "$pid"
    fi
done <<EOF
$(list_vm_processes)
EOF

if [ "$found" -eq 0 ]; then
    printf 'vm_processes=none\n'
elif [ "$reap" -eq 0 ]; then
    printf 'rerun with --reap to kill them\n'
fi

Install traps, and what each misleading message actually means

krun_start_enter returned: -22 (EINVAL ...) on macOS

It means your install path is too long. The error text blames disks and device options and is wholly misleading.

A VM’s agent socket is $HOME/Library/Caches/smolvm/vms/<16 hex>/agent.sock. macOS sockaddr_un.sun_path holds 104 bytes including the terminator, so once $HOME is deep enough the socket path no longer fits and every VM start fails immediately. Measured by installing into $HOME directories of increasing length:

socket path bytesresult
100boots
102-22
104-22
106-22

scripts/preflight.sh computes that path and reports socket_path_bytes and socket_path_status before you install anything.

This is the single most expensive trap in the material behind this packet: it cost most of a session and produced a false “macOS is broken” conclusion. Everything else was ruled out by running it. The same release boots from a short $HOME on the same machine; v1.14.1, v1.13.0 and v1.11.0 all fail identically at a long path, so it is not a regression; the installed binary matches the tarball byte for byte; kern.hv_support is 1; and an ad-hoc-signed C program linking the release’s own libkrun.dylib runs a VM to completion and accepts smolvm’s own storage.raw and overlay.raw.

A CI job or an agent harness that installs under a deep temporary directory will hit this and will not be able to tell why. Nothing in the README or the installer mentions a path-length limit.

KVM_DENIED on a fresh Linux box

It means your user is not in the kvm group, and the install said nothing about it. The installer warns and then continues when /dev/kvm is inaccessible, so a successful install says nothing about whether a VM will start. This was the out-of-the-box state on a fresh cloud GPU instance.

The installer tells you to log out and back in. You do not have to:

sudo usermod -aG kvm "$USER"
sg kvm -c 'smolvm machine run --mem 2048 --net --image alpine -- echo OK'

sg kvm -c '<command>' (or newgrp kvm) applies the new group to a single command immediately, which is what you want over SSH or inside a script.

agent did not become ready within 30 seconds

Suspect host load before you suspect the install. This was reproduced on both macOS and a nested-virt Linux box purely by running other VMs at the same time, and the identical command passed on a quiet host seconds later.

Then suspect the guest’s size. A host that is slow to fault in guest memory can boot a small guest and time out on a large one, and the message is the same. Measured on Lima linux-kvm (Ubuntu 24.04 aarch64, nested virtualisation on a 16 GiB Mac that was paging), 2026-09-24:

--memv1.18.2v1.16.1
512, 1024booted, about 14 snot run
2048booted, 33 sbooted, 36 s
2560 to 8192agent did not become ready within 30 secondsthe same at 4096 and 8192

The default is 8192, so on such a host machine create without --mem gives a machine that never starts, while scripts/verify-boot.sh at 2048 passes. Bisect on --mem before designing around it: the same box booted 8192 on v1.14.6 on 2026-09-10, when its host was not paging.

The 30 s limit is a hard-coded constant (src/agent/manager.rs, AGENT_READY_TIMEOUT) and there is no flag or environment variable that raises it for machine run or machine start. SMOLVM_AGENT_READY_TIMEOUT_SECS exists but is read only by pack run.

machine list shows your just-finished VM as unreachable (eph)

Nothing is wrong. The entry retires asynchronously after the run returns; observed gone by 20 s. A cleanup assertion that runs immediately after machine run fails on a healthy system, which is why scripts/cleanup.sh waits before asserting.

The boot subprocess hides its own errors

The child’s stdout and stderr go to /dev/null unless SMOLVM_BOOT_DEBUG=1, and even that showed nothing useful. To see the real failure, run the child yourself:

smolvm-bin _boot-vm <vm-dir>/boot-config.json

That is how the vCPU panics behind the macOS -22 were finally read. The child deletes its own boot-config.json on exit, so copy it while a start is in flight if you want to re-run it. RUST_LOG=debug on the CLI shows the disk-template and boot timeline, which separates a template problem from a hypervisor problem.

DYLD_PRINT_LIBRARIES prints nothing on macOS

Do not conclude anything from it. smolvm-bin carries entitlements, so dyld strips DYLD_* from its environment. The binary finds its libraries through @executable_path/lib regardless, so the stripping is harmless and tells you nothing about a library problem.

Two assertion habits that apply beyond install

  • Assert values, never exit codes. A pack that lost its rootfs still boots and exits zero; a guest command that fails still returns HTTP 200 from the local API; a CUDA program can link and exit zero without ever reaching a GPU. scripts/verify-boot.sh asserts a marker the guest printed and that the guest kernel differs from the host’s, for this reason.
  • Find leftover VMs by argv, not by pgrep -f _boot-vm and not by readlink /proc/<pid>/exe. The pattern form matches any shell whose text contains that string, including the cleanup script itself, and it produced a phantom “1 orphan survived” result in the runs behind this packet. The /proc/<pid>/exe route then fails for a different reason: the VM process is not dumpable, so its /proc/<pid>/exe is root-owned and readlink returns Permission denied to the very user who started it, leaving a reaper that reports “no orphans” while an orphan runs. Both were observed on Ubuntu 24.04 aarch64 on 2026-09-07. scripts/cleanup.sh reads /proc/<pid>/cmdline and requires argv[1] to be exactly _boot-vm, which a shell cannot match, then keeps only the processes whose boot config lives under this HOME’s smolvm state so another session’s VM is left alone. On macOS the same test runs over ps -axo pid=,command=.