MicroVMs · libkrun · open source

Run any code.
Nothing leaves the box.

Every run gets its own MicroVM: separate kernel, no network, read-only disk, up in ~260 ms and destroyed at the end. Need more? A persistent machine with per-host network filtering, credentials the code uses without reading, instant branching and a portable executable of the whole machine.

macOS Apple SiliconLinux x86_64 / aarch6415 languages + LLM on the GPUHTTP API + Go and Python SDKsKubernetes: k3s · microk8sMPL-2.0
rc run
outputs and timings from a real run · cold boot
How it works

A new machine per run. Nothing left over.

A container shares the kernel with your computer. Here every run gets a whole virtual computer that exists only for as long as the run.

Under the hood: libkrun as the MicroVM monitorHypervisor.framework on macOS, KVM on Linuxlanguage images from the official OCI images, no Docker

Features, really running

Pick a feature and watch the session.

Each scene below is the output of a real VM, captured on 2026-10-20 on an M1 Pro Mac by task site:demos. The time to the right of each command is the measured one. No line was written by hand.

rc
real session · measured timings

Use cases

What you can build with it.

Each case below was run on a real VM, with the commands shown next to it.

An agent with no brakes, in a box.

Claude Code runs inside a MicroVM with --dangerously-skip-permissions: it edits, installs and executes without asking, because whatever goes wrong stays in the VM.

  • Your project mounted at /workspace: what the agent writes shows up in your folder.
  • Terminal in the browser, on 127.0.0.1 only and with a token in the URL.
  • Any port it opens (uvicorn, Vite, a database) becomes a tab with the app inside, next to the shell and eventos tabs.
  • Your SSH keys through the host's agent: git push works, the private key never gets in.
  • Log in once: the session lives on the machine's disk and survives a stop.

Try it now ↗

rc machine create --name agent --image node:22 --net -e IS_SANDBOX=1 \
  --init "npm install -g @anthropic-ai/claude-code"
rc machine start  --name agent
rc machine web-terminal --name agent -- claude --dangerously-skip-permissions
web terminal · runner.codes
 ▐▛███▛█   Claude Code v2.1.304
▝▜██████▀  Opus 5.5 · Claude Max
 ▝▝   ▝▝   /tmp/demo


❯ Write a tiny Python web server that answers "hello from the playground" on port 8000, start it in
  the background and check it with curl.

  Ran 2 shell commands

● curl isn't installed here, so I'll check the server with Python's HTTP client instead.

  Ran 1 shell command

● The server is running in the background on port 8000 and answers hello from the playground. I
  couldn't check it with curl because curl isn't installed in this environment. Instead I fetched
  the page with Python's built-in HTTP client and got 200 text/plain with that text.

  - Code: /tmp/demo/server.py uses only Python's standard library and answers any GET request.
  - Log: /tmp/demo/server.log records each request.

  To check it with curl yourself, install it first (for example ! apt-get install -y curl), then run
  curl localhost:8000. To stop the server, run pkill -f server.py, or kill its process ID if pkill
  isn't available either (pgrep wasn't).

✻ Churned for 15s · done 4:18 PM

────────────────────────────────────────────────────────────────────────────────────────────────────
❯ install curl and check it
────────────────────────────────────────────────────────────────────────────────────────────────────
  ⏵⏵ bypass permissions on (shift+tab to cycle)
Real session: Claude Code wrote and started a server on port 8000, and the port became a tab.
Build your machine

You decide what the code can reach.

A machine is born with no network and nothing from your disk. Every opening is a declared line. Tick what you need and see the runner.code, the equivalent command and what changes for the code inside.

Base
Outbound network
Your computer
Secrets

runner.code

the same from the command line

what the code inside the machine can reach

    What it's for

    For running code you didn't write.

    Code generated by AI, sent by a user or downloaded from the internet runs without access to your files, your network or your system.

    🤖

    AI-generated code

    Run what the LLM wrote without reviewing it line by line first. If it goes wrong, it goes wrong inside the MicroVM.

    🧑‍💻

    A machine for agents

    Claude Code or opencode with the repository mounted, network only to the model's API, the key as a secret and the host's SSH. Branch before every attempt.

    🎓

    Playgrounds and evaluations

    Run user code in 15 languages through the API, with a pre-warmed pool: 26–43 ms per request.

    🛡️

    Stronger than a container

    A container only separates processes inside the same system. The MicroVM is another computer: whatever goes wrong there can't reach yours.

    When not to use it: if the code is yours, trusted and needs network, a container does the job and is simpler.

    Containerrunner.codes
    Kernelsame as the hostits own, one per machine
    Networkon by defaultoff; opened host by host, with every denial logged
    Credentialsa variable the process can readplaceholder; the real value is added on the host, only for the allowed host
    Diskwritablejob: read-only root; machine: its own disk with copy-on-write branching
    End of jobthe process diesthe whole MicroVM disappears
    Three ways to use it

    A job, a machine or an API.

    Ephemeral job

    A new MicroVM runs it, returns stdout and stderr, and disappears. The most direct way to run a piece of code.

    • stdout and stderr show up while the program runs
    • rc shell opens an interactive shell in a disposable MicroVM
    # runs and returns the output
    rc run --stack python --code 'print(2**10)'
    1024
    
    # interactive shell in a MicroVM that disappears on exit
    rc shell --stack python
    Runbook

    Test it yourself, step by step.

    The same script that task runbook runs and checks on its own: 146 checks, 0 failures in the last run. Copy each step's commands, compare with the expected result and tick it. Progress is saved in this browser.

    0 of 0

    A sequence to check, in a fresh terminal, one command at a time, that rc does what it promises. Each step says what you should see.

    This runbook is executable. task runbook (scripts/runbook.sh) runs exactly these steps and checks each expected result. It fails if any of them comes out different. Project rule: every new feature gets a section here and the matching step in scripts/runbook.sh, in the same delivery.

    Last runHostResult
    20/10/2026macOS 27, Apple M1 Protask runbook: 0 failures in 116 checks (up to 8u: user, secrets, socket bridges, ping, staged, refusals)
    19/10/2026macOS 27, Apple M1 Protask runbook: 0 failures in 50 checks (with volumes and network, steps 8c and 8d). Failure proof: with the gateway DNS unfiltered, 8d failed 2 checks

    Prerequisites: task krun:install, task build and the images (./bin/rc image build --all; step 9 needs the llama image).

    Run with the e2e stopped: the tests check with pgrep that no VM was left behind, and a machine of yours that is running makes them fail.

    0Prepare
    cd ~/works/labs/github/runner.codes
    ./bin/rc doctor

    Expected: ok on platform, libkrun, gpu and guest, and one ok image <stack> line per built image.

    1Ephemeral job with streaming output
    ./bin/rc run --stack python --code $'import time\nprint("early")\ntime.sleep(1.5)\nprint("late")'

    Expected: early shows up right away (~0.3 s, the boot) and late 1.5 s later, while the program is still running. At the end, [status=success exit=0 ...] on stderr. The VM is destroyed when it finishes.

    2Create and start a machine
    ./bin/rc machine create --name teste --stack python
    ./bin/rc machine start --name teste
    ./bin/rc machine ls

    Expected:

    • created teste (stack=python cpus=4 memory=8192MiB disk=10GiB gpu=false);
    • started teste (pid N) in ~200ms;
    • teste with running in ls.

    The machine stays running after the command exits.

    3Write things in three places
    ./bin/rc machine exec --name teste -- sh -c 'echo persistiu > /etc/minha-config; echo dados > /workspace/arquivo.txt; echo some > /tmp/temporario.txt'

    Expected: no output, and rc 0.

    4Stop and start again
    ./bin/rc machine stop --name teste
    ./bin/rc machine start --name teste

    Expected: stopped teste and started teste (pid N) in ~200ms. The stop does a sync of the disk before shutting down.

    5Check what was kept
    ./bin/rc machine exec --name teste -- sh -c 'cat /etc/minha-config /workspace/arquivo.txt; cat /tmp/temporario.txt'

    Expected:

    persistiu
    dados
    cat: can't open '/tmp/temporario.txt': No such file or directory

    /etc (writable layer over the image) and /workspace (the machine's disk) persist; /tmp is tmpfs and comes back empty.

    6Copy a file to the machine and back
    head -c 20000000 /dev/urandom > /tmp/rc-up.bin
    ./bin/rc machine cp /tmp/rc-up.bin teste:/workspace/up.bin
    ./bin/rc machine exec --name teste -- sha256sum /workspace/up.bin
    shasum -a 256 /tmp/rc-up.bin
    ./bin/rc machine cp teste:/workspace/up.bin /tmp/rc-down.bin
    cmp /tmp/rc-up.bin /tmp/rc-down.bin && echo "idênticos"

    Expected:

    • uploaded 20000000 bytes ... in ~230ms;
    • the same sha256 inside and outside the machine;
    • downloaded ... in ~230ms;
    • idênticos.
    7Exit code and streaming inside the machine
    ./bin/rc machine exec --name teste -- sh -c 'echo a; sleep 1; echo b; exit 7'; echo "rc=$?"

    Expected: a, b one second later and rc=7. The exit code of rc is the command's.

    7bInteractive terminal on the machine
    ./bin/rc machine shell --name teste

    Inside the shell that opens, type:

    test -t 0 && echo IS-TTY; stty size     # IS-TTY e o tamanho do seu terminal
    sleep 30                                 # aperte Ctrl+C: interrompe só o sleep
    echo ALIVE                               # o shell continua vivo
    exit 4

    Outside, echo $? shows 4. The machine stays running: the shell is a session, not the VM.

    Expected: a real terminal, with Ctrl+C, arrow keys, history, vi and window resizing. It is bash if the image has it; on python (Alpine) it is sh.

    8Machine logs
    ./bin/rc machine logs --name teste --tail 10

    Expected: the last lines of the console (the guest logs every operation). Also works with the machine stopped.

    8bMachine declared in a runner.code

    The runner.code is the machine declared in a file: stack or image, resources, env, init and workload in a versionable TOML.

    mkdir -p /tmp/rc-demo && cd /tmp/rc-demo
    cat > runner.code <<'TOML'
    stack = "python"
    cpus = 2
    env = ["GREETING=hello"]
    workdir = "/app"
    init = ["echo $(( $(cat count 2>/dev/null || echo 0) + 1 )) > count"]
    entrypoint = ["sh", "-c"]
    cmd = ["trap 'echo saved > saved; exit 0' INT; echo \"up $GREETING in $(pwd)\"; while :; do sleep 0.2; done"]
    stop_signal = "SIGINT"
    stop_timeout = 5
    TOML
    ~/works/labs/github/runner.codes/bin/rc machine create --name web --cpus 1
    cd ~/works/labs/github/runner.codes
    ./bin/rc machine status --name web --json
    ./bin/rc machine start  --name web
    ./bin/rc machine logs   --name web --source workload
    ./bin/rc machine exec   --name web -- sh -c 'cat count; echo $GREETING; pwd'
    ./bin/rc machine stop   --name web
    ./bin/rc machine start  --name web
    ./bin/rc machine exec   --name web -- sh -c 'cat count saved'
    ./bin/rc machine stop   --name web
    ./bin/rc machine delete --name web

    Expected:

    • create without -f says from runner.code, and the status shows "cpus": 1: the flag wins over the file;
    • logs --source workload shows up hello in /app: the workdir was created and env and workdir reached the workload;
    • the first exec prints 1, hello, /app;
    • the second prints 1 and saved: init did not run again on the second start, and the stop's SIGINT reached the workload, which wrote the file before the sync.

    A key that runner.code does not implement yet is refused, never ignored:

    printf 'stack = "python"\n[health]\nexec = ["true"]\n' > /tmp/rc-demo/bad.code
    ./bin/rc machine create --name x -f /tmp/rc-demo/bad.code

    Expected: key "health" is not supported yet: health checks, and nothing created.

    8cVolumes: a host directory inside the machine
    mkdir -p /tmp/rc-vol/ro && echo "do host" > /tmp/rc-vol/hello.txt
    ./bin/rc machine create --name vol --stack python -v /tmp/rc-vol:/data -v /tmp/rc-vol/ro:/ro:ro -w /data
    ./bin/rc machine start  --name vol
    ./bin/rc machine exec   --name vol -- sh -c 'cat hello.txt; echo do-guest > from-guest.txt; echo y > /ro/y; echo ro=$?'
    cat /tmp/rc-vol/from-guest.txt
    ./bin/rc machine stop   --name vol
    ./bin/rc machine delete --name vol

    Expected:

    • do host: the guest reads the host directory;
    • Read-only file system and ro=1: :ro is enforced by libkrun on the device, and the guest's root cannot undo it;
    • on the host, cat shows do-guest: the guest's write shows up on the host immediately.
    8dNetwork: egress, policy and port

    A machine is born without network. --net turns egress on; --allow-host turns it on and restricts it; -p publishes a port on the host's 127.0.0.1.

    ./bin/rc machine create --name net --stack python --allow-host example.com -p 18080:8000
    ./bin/rc machine start  --name net
    ./bin/rc machine exec   --name net -- sh -c 'wget -qO /dev/null http://example.com; echo ok=$?'
    ./bin/rc machine exec   --name net -- sh -c 'wget -qO /dev/null http://iana.org'
    ./bin/rc machine exec   --name net -- sh -c 'wget -qO- -T 3 http://169.254.169.254; echo meta=$?'
    ./bin/rc machine update --name net --allow-host iana.org
    ./bin/rc machine exec   --name net -- sh -c 'wget -qO /dev/null http://iana.org; echo ok=$?'
    ./bin/rc machine exec   --name net -- sh -c 'cd /tmp && echo do-guest > i.html && (python3 -m http.server 8000 >/dev/null 2>&1 &)'
    curl -s http://127.0.0.1:18080/i.html
    cat ~/Library/Caches/runner-codes/machines/net/egress-denials.log
    ./bin/rc machine stop   --name net
    ./bin/rc machine delete --name net

    Expected:

    • ok=0 for example.com;
    • wget: bad address 'iana.org' immediately: the gateway DNS answers NXDOMAIN, with no waiting for a timeout;
    • meta=1: cloud metadata (169.254.169.254) is refused even with the network open;
    • after the update, iana.org goes through without restarting the machine;
    • curl on the host receives do-guest through the published port;
    • the denial log has deny dns iana.org: name not in the allow list.
    8eEphemeral machine: machine run
    ./bin/rc machine run --stack python -- python3 -c 'print("ola"); raise SystemExit(3)'; echo "rc=$?"
    echo dados | ./bin/rc machine run -i --stack python -- sh -c 'read x; echo leu:$x'
    ./bin/rc machine run --net --image alpine -- uname -a
    ./bin/rc machine run -it --image alpine -- /bin/sh        # terminal; `exit` sai e apaga a máquina
    ./bin/rc machine ls

    Expected:

    • ola and rc=3: the exit code is the command's;
    • leu:dados: -i forwards stdin (without -i stdin is not read);
    • the uname -a of the libkrunfw kernel on an image outside the catalog (built on first use);
    • ls with no run-… at all: the ephemeral machine disappears at the end, including on Ctrl+C.
    8fA published port reaches a server that only listens on localhost
    ./bin/rc machine run -d --name web --stack python -p 18080:9000 -- python3 -m http.server 9000 --bind 127.0.0.1
    curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18080/
    ./bin/rc machine delete --name web -f

    Expected: 200. The published port enters through the guest (dial op over vsock) and dials 127.0.0.1 from there, so a server that only listens on localhost, like vite or openclaw, answers.

    8gScreen in the browser: display, keyboard and mouse
    ./bin/rc machine create --name tela --image alpine:latest --gpu --net --display 1024x768
    ./bin/rc machine start --name tela
    ./bin/rc machine exec --name tela -- sh -c 'apk add -q --no-cache libdrm-tests evtest; cat /sys/class/input/event*/device/name'
    ./bin/rc machine exec --name tela -- sh -c 'c=$(modetest -M virtio_gpu -c | awk "/connected/ {print \$1; exit}"); (modetest -M virtio_gpu -s $c:1024x768 >/dev/null 2>&1 </dev/null &)'
    open http://localhost:6080
    ./bin/rc machine exec --name tela -- sh -c '(timeout 25 evtest /dev/input/event0 > /tmp/k.log 2>&1 </dev/null &)'
    # click the page and type a few letters; then:
    ./bin/rc machine exec --name tela -- grep 'value 1' /tmp/k.log
    ./bin/rc machine delete --name tela -f

    Expected:

    • the guest lists runner.code keyboard and runner.code tablet;
    • the page shows the modetest color bars, live, at 1024x768;
    • each key typed on the page shows up in the guest's evtest (KEY_R, KEY_C...).

    task runbook checks the same path without needing a human eye: the 1024x768 frame reaches the viewer and a KEY_A sent via POST /input shows up in evtest.

    8hResponsive screen and screen control

    The same machine with a screen from 8g, now controlled by the CLI, the API and MCP, and with the resolution requested by the page:

    ./bin/rc machine create --name tela --image alpine:latest --gpu --net --display 1024x768
    ./bin/rc machine start --name tela
    ./bin/rc machine exec --name tela -- sh -c 'apk add -q --no-cache libdrm-tests evtest; mkdir -p /etc/rc; printf "#!/bin/sh\necho \"\$1 \$2\" > /tmp/resize.txt\n" > /etc/rc/display-resize; chmod 755 /etc/rc/display-resize'
    curl -s -X POST localhost:6080/resize -d '{"width":900,"height":600}'
    ./bin/rc machine exec --name tela -- cat /tmp/resize.txt
    ./bin/rc machine exec --name tela -- sh -c 'c=$(modetest -M virtio_gpu -c | awk "/connected/ {print \$1; exit}"); (sleep 90 | modetest -M virtio_gpu -s $c:1024x768 >/dev/null 2>&1 &)'
    ./bin/rc machine screen --name tela screenshot -o tela.jpg
    ./bin/rc machine exec --name tela -- sh -c '(timeout 8 evtest /dev/input/event0 > /tmp/k.log &); (timeout 8 evtest /dev/input/event1 > /tmp/t.log &)'
    ./bin/rc machine screen --name tela key ctrl+c
    ./bin/rc machine screen --name tela click 1023 767
    ./bin/rc machine exec --name tela -- sh -c 'sleep 8; grep -h "value 1" /tmp/k.log /tmp/t.log | head'
    ./bin/rc machine screen --name tela key hyper
    printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | ./bin/rc mcp --name tela
    ./bin/rc machine delete --name tela -f

    Expected:

    • 900 600 in /tmp/resize.txt: the size the page requests reaches the guest's /etc/rc/display-resize hook (on the desktop, it runs xrandr; without the hook, nothing happens);
    • screen 1024x768 -> tela.jpg, a JPEG of the screen;
    • in evtest: KEY_LEFTCTRL and KEY_C pressed, ABS_X at 32767 (the right edge) and BTN_LEFT;
    • the viewer page draws an orange arrow where the CLI clicked (GET /pointer records the click);
    • the screen page shows runner.codes on its bar (display_title in the runner.code changes the name);
    • key hyper exits with an error (unknown key "hyper"), without sending anything to the machine;
    • rc mcp answers initialize and lists 9 tools: screenshot, click, move, drag, type, key, scroll, actions and exec.

    task runbook does the same and also checks the API: GET /api/v1/machines/:name/screen with X-Screen-Width: 1024, 400 for an invalid action and 204 for a valid one in POST .../screen/actions.

    8iBranch and pause
    ./bin/rc machine create --name mom --stack python && ./bin/rc machine start --name mom
    ./bin/rc machine exec   --name mom -- sh -c 'echo antes > /root/marca'
    ./bin/rc machine pause  --name mom && ./bin/rc machine status --name mom
    ./bin/rc machine branch --from mom --name kid
    ./bin/rc machine resume --name mom
    ./bin/rc machine start  --name kid && ./bin/rc machine exec --name kid -- cat /root/marca
    ./bin/rc machine delete --name kid -f && ./bin/rc machine delete --name mom -f

    Expected:

    • status shows paused, and an exec in that state is refused with the reason;
    • the child shows antes: the branch carries what the parent wrote, even while it is paused (rc resumes it for an instant for the sync);
    • after that, parent and child are independent.
    8jNamed network
    ./bin/rc machine run -d --name db --stack python --network lab -- python3 -m http.server 8080
    ./bin/rc machine create --name app --stack python --network lab && ./bin/rc machine start --name app
    ./bin/rc machine exec --name app -- wget -qO- -T 3 http://100.98.0.10:8080/
    ./bin/rc machine exec --name app -- wget -qO- -T 3 http://db.lab.internal:8080/
    ./bin/rc machine delete --name db -f
    ./bin/rc machine exec --name app -- nslookup db.lab.internal
    ./bin/rc machine delete --name app -f

    Expected:

    • the two machines get consecutive IPs from the network (db is 100.98.0.10, app is 100.98.0.11) and reach each other by IP and by the name <máquina>.<rede>.internal, without --net: traffic inside the network is not egress;
    • a machine on another network does not reach these, nor resolve their names;
    • a deleted db disappears from the network's DNS: nslookup no longer returns its IP.
    8kS3 volume

    A local S3, serving a folder (MinIO left nixpkgs on 11/10/2026):

    mkdir -p /tmp/s3/bkt/data && echo 'ola do bucket' > /tmp/s3/bkt/data/hello.txt
    nix shell nixpkgs#rclone -c rclone serve s3 /tmp/s3 --addr 127.0.0.1:19000 --auth-key chave,segredo123 &
    ./bin/rc machine create --name s3 --stack python --net --allow-cidr 127.0.0.1/32 \
      -e AWS_ACCESS_KEY_ID=chave -e AWS_SECRET_ACCESS_KEY=segredo123 -e AWS_ENDPOINT_URL=http://100.96.0.1:19000 \
      -v s3://bkt/data:/data
    ./bin/rc machine start --name s3
    ./bin/rc machine exec --name s3 -- sh -c 'cat /data/hello.txt; echo do-guest > /data/novo.txt'
    cat /tmp/s3/bkt/data/novo.txt
    ./bin/rc machine delete --name s3 -f

    Expected:

    • start mounts the bucket (with a wrong key, it fails with s3: 403 and the reason);
    • the guest reads ola do bucket, and novo.txt appears in the server's folder with do-guest.
    8lA credential the machine uses without reading it
    ./bin/rc machine create --name cred --stack python --credential tok=RB_TOKEN@postman-echo.com
    RB_TOKEN=valor-do-runbook ./bin/rc machine start --name cred
    ./bin/rc machine exec --name cred -- python3 -c '
    import json, os, urllib.request
    tok = os.environ["RB_TOKEN"]
    print("guest:", tok)
    r = urllib.request.Request("https://postman-echo.com/headers", headers={"Authorization": "Bearer " + tok, "User-Agent": "runbook/1"})
    print("server:", json.load(urllib.request.urlopen(r, timeout=20))["headers"]["authorization"])'
    ./bin/rc machine delete --name cred -f

    Expected:

    • guest: RC_PLACEHOLDER_TOK_...: the guest only has the placeholder;
    • server: Bearer valor-do-runbook: the server received the real value, which came from the start environment.

    The User-Agent is custom because postman-echo's Cloudflare refuses Python-urllib (403, error code 1010).

    8mPack: the machine in a file
    ./bin/rc machine create --name pk --stack python && ./bin/rc machine start --name pk
    ./bin/rc machine exec --name pk -- sh -c 'echo marca-do-runbook > /root/marca'
    ./bin/rc machine stop --name pk
    ./bin/rc pack create --from-vm pk --output /tmp/one --single-file
    ./bin/rc machine delete --name pk
    /tmp/one run -- cat /root/marca
    /tmp/one

    Expected:

    • marca-do-runbook: the artifact carried the machine's disk, even with the source deleted (a pack that lost the disk would still boot and exit 0; that is why a value is checked);
    • /tmp/one alone shows usage: one run ....
    8nUser, disk volume and docker.sock
    ./bin/rc machine create --name usr --stack python --user nobody
    ./bin/rc machine start --name usr
    ./bin/rc machine exec --name usr -- id -un
    ./bin/rc machine exec --name usr --user root -- id -un
    ./bin/rc machine create --name dk --stack python --docker-socket -v /var/lib/dados -- python3 -c '
    import os, socket
    s = socket.socket(socket.AF_UNIX); s.bind("/var/run/docker.sock"); s.listen()
    while True:
        c, _ = s.accept(); c.sendall(b"do-guest:" + c.recv(64)); c.close()'
    ./bin/rc machine start --name dk
    ./bin/rc machine exec --name dk -- stat -f -c %T /var/lib/dados
    python3 -c 'import socket,sys; s=socket.socket(socket.AF_UNIX); s.connect(sys.argv[1]); s.sendall(b"ping"); print(s.recv(64).decode())' "$(./bin/rc machine data-dir --name dk)/docker.sock"

    Expected:

    • nobody, and root with --user root (the exec's --user wins over the machine's);
    • ext2/ext3: the disk volume is real ext4, outside the overlay (which Docker's overlay2 needs);
    • do-guest:ping: the host talks to the guest's server through the docker.sock in the machine directory.
    8oSecrets and SSH agent
    ssh-keygen -q -t ed25519 -N "" -f /tmp/rb-id && ssh-agent -D -a /tmp/rb-agent.sock & sleep 1
    SSH_AUTH_SOCK=/tmp/rb-agent.sock ssh-add -q /tmp/rb-id
    echo do-arquivo > /tmp/rb-sec.txt
    ./bin/rc machine create --name sec --image alpine --net --init 'apk add -q --no-cache openssh-client' \
      --secret-env API=RB_SECRET --secret-file ARQ=/tmp/rb-sec.txt --ssh-agent
    RB_SECRET=valor-secreto SSH_AUTH_SOCK=/tmp/rb-agent.sock ./bin/rc machine start --name sec
    ./bin/rc machine exec --name sec -- sh -c 'echo $API $ARQ; ssh-add -l'
    grep -rl valor-secreto --exclude='*.ext4' "$(./bin/rc machine data-dir --name sec)"

    Expected:

    • valor-secreto do-arquivo: both secrets in the environment of every command;
    • ssh-add -l lists the key from /tmp/rb-id (the private key stays in the host's agent);
    • grep finds nothing: the value is not kept in the machine's record.

    The agent and the key are disposable, specific to this step: nothing touches the agent of whoever runs it.

    8pSocket bridges in both directions
    python3 -c 'import socket; s=socket.socket(socket.AF_UNIX); s.bind("/tmp/rb-h.sock"); s.listen()
    while True:
        c,_=s.accept(); c.sendall(b"do-host:"+c.recv(64)); c.close()' &
    ./bin/rc machine create --name skt --stack python --expose-socket /run/app.sock --mount-socket /tmp/rb-h.sock:/run/h/h.sock -- python3 -c '
    import socket
    s = socket.socket(socket.AF_UNIX); s.bind("/run/app.sock"); s.listen()
    while True:
        c, _ = s.accept(); c.sendall(b"do-guest:" + c.recv(64)); c.close()'
    ./bin/rc machine start --name skt
    python3 -c 'import socket,sys; s=socket.socket(socket.AF_UNIX); s.connect(sys.argv[1]); s.sendall(b"ping"); print(s.recv(64).decode())' "$(./bin/rc machine data-dir --name skt)/app.sock"
    ./bin/rc machine exec --name skt -- python3 -c 'import socket; s=socket.socket(socket.AF_UNIX); s.connect("/run/h/h.sock"); s.sendall(b"pong"); print(s.recv(64).decode())'

    Expected: do-guest:ping (the host reaches the socket the workload listens on) and do-host:pong (the guest reaches the host's socket, in a directory the image did not have).

    8qPing and port ranges
    ./bin/rc machine create --name png --stack python --net -p 18301-18303:18301-18303 -- python3 -m http.server 18302
    ./bin/rc machine start --name png
    curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18302/
    ./bin/rc machine exec --name png -- ping -c1 -W3 1.1.1.1
    ./bin/rc machine create --name pngd --stack python --allow-cidr 198.51.100.0/24
    ./bin/rc machine start --name pngd
    ./bin/rc machine exec --name pngd -- ping -c1 -W2 1.1.1.1
    grep 'deny icmp' "$(./bin/rc machine data-dir --name pngd)/egress-denials.log"

    Expected:

    • 200: the range publishes one port per number (up to 64), and the middle one answers;
    • the ping to 1.1.1.1 comes back with the network open, and only if the host received the reply (no reply is made up);
    • outside the allowlist the ping does not come back, and the denial deny icmp 1.1.1.1 goes to the log.
    8rStaged volume and sync
    mkdir -p /tmp/rb-repo && echo original > /tmp/rb-repo/a.txt && echo x > /tmp/rb-repo/old.txt
    ./bin/rc machine create --name stg --stack python -v /tmp/rb-repo:/work:staged
    ./bin/rc machine start --name stg
    ./bin/rc machine exec --name stg -- sh -c 'stat -f -c %T /work; echo mudou > /work/a.txt; rm /work/old.txt; printf "#!/bin/sh\n" > /work/run.sh; chmod 755 /work/run.sh'
    cat /tmp/rb-repo/a.txt
    ./bin/rc machine sync --name stg
    cat /tmp/rb-repo/a.txt; ls /tmp/rb-repo; ls -l /tmp/rb-repo/run.sh

    Expected:

    • ext2/ext3: the copy lives on the machine's disk, with no virtio-fs round trips;
    • before the sync the host still has original;
    • after: mudou, no old.txt (sync mirrors: what disappeared from the copy disappears from the host) and run.sh with -rwxr-xr-x.

    A graceful stop and the end of machine run also sync; --timeout 0 does not. The child of a branch returns nothing to the host.

    8sRefusals that state the reason
    ./bin/rc doctor | grep -E 'cuda|nested'
    ./bin/rc machine create --name x --image alpine --cuda
    time ./bin/rc machine run --nested --image alpine -- ls -l /dev/kvm

    Expected:

    • info cuda: not supported (no CUDA remoting; --gpu is Vulkan) and this host's nested line;
    • --cuda is not supported: rc has no CUDA remoting ..., without creating a machine;
    • on a host that does not nest (M1/M2), failed to start: Error: nested virtualization is not supported on this host ... in under 1 s (before: 30 s and did not become ready); on a host that nests, /dev/kvm inside the VM.
    8tMachine directory and cache cleanup
    ls "$(./bin/rc machine data-dir --name usr)"
    ./bin/rc image prune
    ./bin/rc pack prune

    Expected: machine.json in the machine directory; both prunes exit 0 (without --all, image prune only deletes the ext4 no name points to anymore, the previous build of an image, and pack prune keeps the 5 most recently used).

    8uThe guides
    task guides DIRS=gpu-cuda      # uma pasta; sem DIRS, todas (cada uma liga as suas VMs)

    Expected: falhas: 0 in each folder. What each folder proves is in examples/guides/README.md.

    8vWeb terminal
    ./bin/rc machine web-terminal --name demo                 # opens the browser with the machine's shell
    ./bin/rc machine web-terminal --name demo --no-open -- sh -c 'echo wt-ok:$((6*7))'

    Expected:

    • web terminal: http://127.0.0.1:PORT/?t=<32 hex>: loopback only, with a token; the browser opens by itself (--no-open only prints);
    • in the browser, a real terminal (xterm.js): Ctrl+C, arrow keys, full-screen editors and the window size reach the guest, and a reload shows the recent screen again;
    • with the command, the screen shows wt-ok:42, the page says the program exited and rc exits with its code;
    • /stream without the token answers 403, and so does a non-loopback Host (DNS rebinding).
    • tabs claude (the command), shell (a separate terminal, on first click), eventos and, last, one tab per port: a server the program starts in the VM (any port) shows up with the app in an iframe and answers on the host's 127.0.0.1.

    To open Claude Code in the browser this way: rc machine web-terminal --name agent -- claude --dangerously-skip-permissions.

    8wPublic playground (claude-in-chrome.runner.codes)

    Each visitor gets a Fly Machine of their own (Fly exposes no /dev/kvm, so the microVM is Fly's), with Claude Code in the web terminal and ports at https://<port>-<id>.runner.codes/. At most 3 machines; 30 min idle stops one, 24 h without use deletes it.

    scripts/playground-cloudflare.sh     # claude-in-chrome tunnel, ingress and CNAMEs (idempotent)
    scripts/playground-fly.sh setup      # runner-codes-playground and runner-codes-visitors apps, own network, secrets
    scripts/playground-image.sh all      # visitor and front images built with Nix on the Fly builder
    scripts/playground-fly.sh deploy     # publishes the front
    scripts/playground-token-check.sh    # the front token only reaches the visitors app
    curl -s https://claude-in-chrome.runner.codes/ | grep -c FingerprintJS.load

    Expected:

    • the page opens, computes the browser visitorId and takes you to the terminal /v/<id>/?t=<token> with Claude Code;
    • /api/session with an invalid visitor answers 400; with 3 machines in use, 503 "playground is full";
    • https://3000-0000000000.runner.codes/ (a machine that does not exist) answers 502, and docs.runner.codes stays 200;
    • scripts/playground-token-check.sh: 200 on the visitors app, 4xx on the site and on the front itself;
    • inside the visitor machine, opening a connection to the Fly private network (_api.internal) fails, while DNS and the internet work (scripts/fly-firewall-probe.sh measures the same firewall on a throwaway machine).
    8xKubernetes runtime (k3s and microk8s)

    Every pod with runtimeClassName: rc becomes a libkrun microVM with its own kernel: containerd calls containerd-shim-rc-v2, the pause container boots the VM and the app containers run inside it. The VM takes the IP the CNI gave the pod (a TAP in the pod netns with tc mirred). It needs a Linux node with /dev/kvm; the test nodes are DigitalOcean droplets (task k8s:kvm-probe measures it).

    K8S_NODE=k3s task k8s:node -- create && K8S_NODE=k3s task k8s:node -- provision   # same with K8S_NODE=microk8s
    # on the node: curl -sfL https://get.k3s.io | sh -   or   snap install microk8s --classic
    task k8s:e2e -- k3s        # ships the tree, builds, runs deploy/k3s/install.sh and deploy/k8s/e2e.sh k3s
    task k8s:e2e -- microk8s   # the same with deploy/microk8s/install.sh
    task k8s:defects           # plants 20 defects in the runtime code and checks the tests catch them

    Expected (19 checks in deploy/k8s/e2e.sh, falhas: 0 on both):

    • rc-hello gets Ready and its log shows RC_K8S_OK, the VM kernel (6.12.91, not the node's) and the pod IP on eth0;
    • kubectl exec without a TTY, with a TTY and with stdin; the exec exit code (7) and the container one (3) come back right;
    • cluster DNS, rc pod ↔ runc pod both ways and a ClusterIP Service to the rc pod;
    • two containers in one pod: same VM (same boot_id) and the same emptyDir;
    • the kubelet stats summary answers (the VM usage goes to the first container);
    • after the delete, no directory in /var/lib/rc-shim/pods and no rc vmm process is left.

    Declared limits: port-forward, hostNetwork, hostIPC and mount propagation do not cross the VM; volumes are copied into the pod share when the container is created (what the kubelet changes later, such as a serviceaccount token, does not arrive); CPU/memory limits apply to the whole VM (pod annotations runner.codes/cpus and runner.codes/memory-mib); the pod vmm runs without Landlock/seccomp.

    9GPU

    Only if the llama image is built (rc doctor shows ok image llama).

    ./bin/rc run --stack llama --gpu --code 'vulkaninfo --summary | grep deviceName'
    ./bin/rc run --stack llama --gpu --timeout 120s --code 'llama-bench -m /opt/models/qwen2.5-0.5b-q4_k_m.gguf -p 128 -n 32 -ngl 99 -dev Vulkan0'

    Expected:

    • deviceName = Virtio-GPU Venus (Apple M1 Pro), and also an llvmpipe line (software Vulkan);
    • the llama-bench table with Vulkan0, pp128 in the 900–1,365 t/s range and tg32 at ~57 t/s.

    Details in GPU.

    10HTTP API

    In one terminal:

    ./bin/rc serve --pool-size 0

    In another:

    curl -s -X POST localhost:8080/api/v1/machines/teste/exec -H 'content-type: application/json' -d '{"command":["python3","-c","print(6*7)"]}'
    curl -s localhost:8080/api/v1/machines | head -c 300; echo

    Expected:

    • {"exitCode":0,"stdout":"42\n","stderr":"",...};
    • the list with "name":"teste" and "state":"running".

    The alternative bodies and the per-line stream, on the same server:

    curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8080/api/v1/machines -H 'content-type: application/json' -d '{"name":"t2","image":"alpine","allowedHost":["a.com"]}'
    curl -s -X POST localhost:8080/api/v1/machines -H 'content-type: application/json' -d '{"name":"t3","image":"alpine","allowedHosts":["api.example.com"],"memoryMb":512}'
    curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8080/api/v1/machines -H 'content-type: application/json' -d '{"name":"t4","image":"alpine","nestedVirt":true}'
    curl -sN -X POST localhost:8080/api/v1/machines/teste/exec/stream -H 'content-type: application/json' -d '{"command":["sh","-c","echo a; echo b"]}'
    curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8080/api/v1/machines/teste/sync

    Expected:

    • 400: an unknown field (allowedHost, with a typo) is refused, not ignored with egress left open;
    • "allow_hosts":["api.example.com"], "enabled":true and "memory_mib":512: the alternative names are accepted;
    • 400 on nestedVirt (only allowed with rc serve --allow-nested-virt);
    • event: stdout / data: a, then data: b: one line per event, raw text, and event: exit at the end;
    • 204 on sync (machine without a staged volume: nothing to do).

    Ctrl+C stops serve. The machine stays running, because it is detached.

    10bGo and Python SDKs

    With serve still up:

    go run ./examples/sdk-go http://127.0.0.1:8080
    PYTHONPATH=sdk/python python3 examples/sdk-python/main.py http://127.0.0.1:8080

    Expected, in both:

    • job: 42 (ephemeral job);
    • machine: sdk-... running and exec: ola do sdk go (or python): machine created, file written and executed;
    • error: NOT_FOUND: the API refusal arrives as a typed error, with the stable code;
    • no sdk-* machine is left in ./bin/rc machine ls.
    10cGo SDK in an outside project (go get)

    The Go module is named runner.codes, and https://runner.codes answers go get with a go-import meta tag pointing to GitHub. In a directory outside the repository, with serve still up:

    curl -s 'https://runner.codes/pkg/client?go-get=1'
    mkdir /tmp/sdkcheck && cp examples/sdk-go/main.go /tmp/sdkcheck/ && cd /tmp/sdkcheck
    go mod init sdkcheck
    go get runner.codes/pkg/client@latest
    go run . http://127.0.0.1:8080

    Expected:

    • curl returns <meta name="go-import" content="runner.codes git https://github.com/andrebassi/runner-codes">;
    • go get downloads runner.codes v1.1.0 (or newer);
    • the go run output is the same as in 10b: job: 42, exec: ola do sdk go and error: NOT_FOUND.

    The same for Python, in a clean venv, installing from GitHub:

    python3 -m venv /tmp/sdkcheck/venv
    /tmp/sdkcheck/venv/bin/pip install "git+https://github.com/andrebassi/runner-codes@v1.2.0#subdirectory=sdk/python"
    /tmp/sdkcheck/venv/bin/python -I <repository>/examples/sdk-python/main.py http://127.0.0.1:8080

    Expected: pip installs runner-code 1.1.1, and the output is the same as in 10b (job: 42, exec: ola do sdk python, error: NOT_FOUND). -I ensures the package comes from the venv, not from the repository.

    10dSDK recipes

    The seven recipes from the SDKs page, in Go and in Python, with serve still up:

    go run ./examples/sdk-go-recipes http://127.0.0.1:8080
    PYTHONPATH=sdk/python python3 examples/sdk-python-recipes/main.py http://127.0.0.1:8080

    Expected, in both (the time varies):

    untrusted: stdout=45 stderr=aviso tempo=58ms
    files: 42
    timeout: exception=Timeout
    compile: exception=Compilation failed stderr_tem_erro=true
    machine: sdk-go pypi=200 example=bloqueado
    branch: feito antes do branch
    errors: 404 NOT_FOUND

    On the machine line, sdk-python instead of sdk-go in the second program. No recipe-* machine is left in ./bin/rc machine ls.

    10eDesktop with Claude Code via the Go SDK

    With serve still up, the machine from examples/desktop-claude/runner.code (XFCE, Firefox, Claude Code), created and checked by the Go SDK:

    go run ./examples/desktop-claude -server http://127.0.0.1:8080 -name desk -display-port 6081
    open http://127.0.0.1:6081

    Expected, in the output:

    • claude 2.1.x (Claude Code) and navegador padrão /usr/lib/firefox-esr/firefox-esr;
    • atalhos Arquivos.desktop Claude.desktop Navegador.desktop Terminal.desktop;
    • tela responde ... current 1200 x 700: after 200 mouse movements, the screen is still up and at the requested size;
    • terminal maximizado _NET_WM_STATE_MAXIMIZED_HORZ;
    • sessão de volta em 1200x700: the program ends the XFCE session (like a logout) and it comes back on its own.

    In the browser: black background, the four shortcuts, and the screen at the size of the window.

    10fTasks for the machine's Claude Code

    Requires Claude logged in on the machine: the token enters through the runner.code credential (the guest only sees a placeholder), from the environment of whoever ran the start. With serve started with the token in the environment:

    CLAUDE_CODE_OAUTH_TOKEN="$(pass show bassi/anthropic/claude-code-token)" ./bin/rc serve --listen 127.0.0.1:8080 &
    go run ./examples/desktop-claude -server http://127.0.0.1:8080 -name desk -display-port 6081
    go run ./examples/desktop-claude-agent -server http://127.0.0.1:8080 -name desk

    Expected: ✓ /root/agent/headless.txt, ✓ /root/agent/gui.txt and ✓ /root/agent/keys.txt, each with feito pelo modo <modo>. These are three ways of sending the task: claude -p via exec, typed into the window with xdotool, and typed through the screen API. The screens of the last two are saved in agent-gui.jpg and agent-keys.jpg.

    task runbook only runs this step with RB_CLAUDE=1 and the token in the environment, because it uses the Claude subscription.

    11Clean up
    ./bin/rc machine stop --name teste
    ./bin/rc machine delete --name teste
    rm -rf /tmp/rc-up.bin /tmp/rc-down.bin /tmp/rc-demo /tmp/rc-vol

    Expected: stopped teste and deleted teste. ./bin/rc machine ls comes back empty.

    extraWhen something comes out different
    SymptomWhere to look
    image for X not built./bin/rc image build --stack X
    read stream: the guest in this image does not support streamingimage from before streaming: rebuild
    INVALID_STATE: machine X is not running./bin/rc machine start --name X
    CONFLICT: machine X already existsanother name, or delete first
    start fails with the end of the console./bin/rc machine logs --name X
    GPU: ERROR_OUT_OF_HOST_MEMORYGPU › Why Fedora
    machine that does not stop./bin/rc machine stop --name X --timeout 0 kills it immediately

    Source: docs/runbook.md · executable: scripts/runbook.sh

    Tested on a real VM

    Nothing here is a promise.

    Every feature has unit tests, a proof on a real MicroVM, and defects planted on purpose to check that the tests fail.

    42/42end-to-end tests with real libkrun, in two consecutive runs
    146runbook checks, 0 failures
    175checks across the 9 guides, each on a real VM
    95.2%coverage with a gate that fails the build; domain at 100%

    I planted 28 defects in the new features (network, ICMP, staged, sockets, nested, SSE…); the tests caught all 28. Run of 2026-10-20, macOS 27, M1 Pro.

    Measured numbers

    Fast enough to skip the container.

    260msfrom boot to the first response of a Python job (p50; p95 266 ms)
    26–43msper API request, with the pool already started
    71msfor three copy-on-write branches of a warmed-up machine
    44MBof memory for an idle MicroVM in the pool

    Boot measured on 2026-10-19 (30 boots) and branching on 2026-10-20, on an Apple Silicon Mac. The full report is in the documentation.

    Languages

    15 in the catalog, plus an LLM.

    pythonnodejsgorustjavaccppruby phpperlbashshawkjqsqlitellama · GPU

    Each with its own image, CPU and RAM ceiling and warm-up, in a YAML file. Machines accept any OCI image (redis:7-alpine, postgres:16-alpine…).

    Already works
    • Branch, pause and disk checkpoint ready
    • Per-host, CIDR and named networks, ping included ready
    • Unreadable credentials, secrets and SSH agent ready
    • Portable pack, staged volume and S3 bucket ready
    • --nested (KVM inside the VM) only the refusal measured

    --nested requires an M3 Mac or newer, or Linux with nested KVM. On this M1 it refuses right away, with the reason.

    Where it runs

    Mac and Linux, no Docker.

    runner.codes uses the hypervisor the system already has. No Docker, no cloud machine.

    macOS

    Apple Silicon, through Hypervisor.framework. This is where the numbers on this page were measured.

    Linux

    x86_64 and aarch64, through KVM (your user needs access to /dev/kvm). Each MicroVM's process runs under Landlock and seccomp. Boot time on Linux with real KVM has not been measured yet.

    Windows

    libkrun has no Hyper-V backend. The way is WSL2 with nested virtualization, where the Linux rc runs.

    Get started

    Three commands, all through Nix.

    No Homebrew and no Docker. The first install on macOS compiles libkrun (about 10 min); on Linux it comes prebuilt from nixpkgs and your user needs access to /dev/kvm.

    ./bin/rc doctor checks whether the host is ready.

    git clone https://github.com/andrebassi/runner-codes
    cd runner-codes
    
    task krun:install                    # libkrun + libkrunfw
    task build                           # host + guest
    ./bin/rc image build --stack python  # 8–10 s
    
    ./bin/rc run --stack python --code 'print("olá da MicroVM")'