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.
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
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.
runtimeClassName: rc on k3s and microk8s: every pod gets its own kernel and the CNI IP, with kubectl exec, logs, Services and cluster DNS. 19 of 19 checks on both.
dockerd on a real ext4, nested container, docker.sock on the host.
Vulkan (Venus) all the way to Metal: 56.7 tokens/s on the GPU versus 10.3 on the CPU.
🖥️Screen in the browserThe machine's screen in the browser, with keyboard and mouse, from 127.0.0.1.
Machines find each other by db.lab.internal; another network doesn't even resolve.
AWS, R2 or MinIO mounted by the guest, read on demand.
🔑SSH agent and secretsgit push with the host's keys, which never enter the VM.
--expose-socket and --mount-socket: Unix sockets in both directions.
Standard library only. Jobs, machines, exec and streaming. Go: go get runner.codes/pkg/client.
Screenshot, click, keyboard and scroll from the CLI, the API and the SDKs, with nothing installed in the VM.
🤖An agent operates the VMrc mcp: Claude Code takes screenshots, clicks and types on the machine's screen.
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.1only 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
shellandeventostabs. - Your SSH keys through the host's agent:
git pushworks, the private key never gets in. - Log in once: the session lives on the machine's disk and survives a stop.
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
▐▛███▛█ 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)
A machine of your own, right now, in the browser.
At claude-in-chrome.runner.codes each visitor gets a machine of their own with Claude Code open and no permission prompts.
- Your account: the
/loginis yours and stays on your machine only. - Any port you start answers at
https://<port>-<id>.runner.codes/and becomes a tab. - Isolated: open internet, the provider's private network closed.
- Temporary: stops after 30 min idle and is deleted after 24 h without use; at most 3 at a time.
https://8000-<id>.runner.codes/.Run your users' code.
Exercises, auto-grading, documentation sandboxes: each submission runs in a fresh MicroVM, with no network, and is gone at the end.
- 15 languages through the same route.
- Pre-warmed pool: 26–43 ms per request.
- Live output over SSE, and a timeout that kills the whole VM.
rc serve --listen 127.0.0.1:8080 --pool-size 2
curl -s -X POST http://127.0.0.1:8080/api/v1/run \
-H 'content-type: application/json' \
-d '{"language":"python","stdin":"oi","files":[{"name":"main.py","content":"print(input().upper())"}]}'
{"status":"success","stdout":"OI\n","stderr":null,"exception":null,"executionTime":40}A local, isolated LLM on the Mac's GPU.
llama.cpp with Vulkan inside the VM, talking to the host GPU through virtio-gpu (Venus → MoltenVK → Metal).
- 56.7 tokens/s on the GPU versus 10.3 on the CPU (Qwen2.5 0.5B).
- No access to your disk: no host directory is mounted unless you declare a volume.
rc image build --stack llama rc run --stack llama --gpu --code 'vulkaninfo --summary | grep deviceName'
A desktop with Claude Code in a tab.
XFCE, Firefox and Claude Code in a runner.code, with the screen in the browser. The screen follows the window, and an agent operates the VM through rc mcp: screenshot, click and keyboard.
- On 127.0.0.1 only, with no VNC or extra client.
- Starts in 5.6 s from an
rc packof the ready desktop.
cd examples/desktop-claude && rc machine create --name desk
CLAUDE_CODE_OAUTH_TOKEN=... rc machine start --name desk
claude mcp add rc -- rc mcp --name desk
# the screen is at http://127.0.0.1:6080You 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.
runner.code
the same from the command line
what the code inside the machine can reach
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.
| Container | runner.codes | |
|---|---|---|
| Kernel | same as the host | its own, one per machine |
| Network | on by default | off; opened host by host, with every denial logged |
| Credentials | a variable the process can read | placeholder; the real value is added on the host, only for the allowed host |
| Disk | writable | job: read-only root; machine: its own disk with copy-on-write branching |
| End of job | the process dies | the whole MicroVM disappears |
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 shellopens 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
Persistent machine
A writable disk that survives from one start to the next, from a stack or any OCI image. Declared in a runner.code file, versioned alongside the code.
create,start,exec,shell,cp,logs,stop,deletebranch,pause,resume,sync,update(network while the machine is running)rc pack create --from-vmturns it into an executable
rc machine create --name dev --stack python --allow-host pypi.org rc machine start --name dev rc machine exec --name dev -- pip install --user requests rc machine branch --from dev --name tentativa-1 rc machine shell --name tentativa-1 rc machine stop --name dev
HTTP API
A pool of already-started MicroVMs per language, each used once: a request takes 26–43 ms. Streaming output (SSE) and full machines through the API.
- no authentication: that's why it listens only on
127.0.0.1by default - an unknown JSON field is rejected, not ignored
rc serve --listen 127.0.0.1:8080 --pool-size 2
curl -s -X POST http://127.0.0.1:8080/api/v1/run \
-H 'content-type: application/json' \
-d '{"language":"python","stdin":"oi","files":[{"name":"main.py","content":"print(input().upper())"}]}'
{"status":"success","stdout":"OI\n","stderr":null,"exception":null,"executionTime":40}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.
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 run | Host | Result |
|---|---|---|
| 20/10/2026 | macOS 27, Apple M1 Pro | task runbook: 0 failures in 116 checks (up to 8u: user, secrets, socket bridges, ping, staged, refusals) |
| 19/10/2026 | macOS 27, Apple M1 Pro | task 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;testewithrunninginls.
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:
createwithout-fsaysfrom runner.code, and the status shows"cpus": 1: the flag wins over the file;logs --source workloadshowsup hello in /app: the workdir was created and env and workdir reached the workload;- the first
execprints1,hello,/app; - the second prints
1andsaved: init did not run again on the second start, and the stop'sSIGINTreached 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 systemandro=1::rois enforced by libkrun on the device, and the guest's root cannot undo it;- on the host,
catshowsdo-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=0forexample.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.orggoes through without restarting the machine; curlon the host receivesdo-guestthrough 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 lsExpected:
olaandrc=3: the exit code is the command's;leu:dados:-iforwards stdin (without-istdin is not read);- the
uname -aof the libkrunfw kernel on an image outside the catalog (built on first use); lswith norun-…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 -fExpected: 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 -fExpected:
- the guest lists
runner.code keyboardandrunner.code tablet; - the page shows the
modetestcolor 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 -fExpected:
900 600in/tmp/resize.txt: the size the page requests reaches the guest's/etc/rc/display-resizehook (on the desktop, it runsxrandr; without the hook, nothing happens);screen 1024x768 -> tela.jpg, a JPEG of the screen;- in
evtest:KEY_LEFTCTRLandKEY_Cpressed,ABS_Xat32767(the right edge) andBTN_LEFT; - the viewer page draws an orange arrow where the CLI clicked (
GET /pointerrecords theclick); - the screen page shows
runner.codeson its bar (display_titlein therunner.codechanges the name); key hyperexits with an error (unknown key "hyper"), without sending anything to the machine;rc mcpanswersinitializeand lists 9 tools:screenshot,click,move,drag,type,key,scroll,actionsandexec.
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:
statusshowspaused, and anexecin that state is refused with the reason;- the child shows
antes: the branch carries what the parent wrote, even while it is paused (rcresumes it for an instant for thesync); - 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 (
dbis100.98.0.10,appis100.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
dbdisappears from the network's DNS:nslookupno 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:
startmounts the bucket (with a wrong key, it fails withs3: 403and the reason);- the guest reads
ola do bucket, andnovo.txtappears in the server's folder withdo-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 -fExpected:
guest: RC_PLACEHOLDER_TOK_...: the guest only has the placeholder;server: Bearer valor-do-runbook: the server received the real value, which came from thestartenvironment.
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/onealone showsusage: 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, androotwith--user root(the exec's--userwins 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 thedocker.sockin 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 -llists the key from/tmp/rb-id(the private key stays in the host's agent);grepfinds 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.1comes 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.1goes 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, noold.txt(sync mirrors: what disappeared from the copy disappears from the host) andrun.shwith-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 anddid not become ready); on a host that nests,/dev/kvminside 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-openonly 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 andrcexits with its code; /streamwithout the token answers 403, and so does a non-loopbackHost(DNS rebinding).- tabs
claude(the command),shell(a separate terminal, on first click),eventosand, 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's127.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/sessionwith 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, anddocs.runner.codesstays 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.shmeasures 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-hellogets Ready and its log showsRC_K8S_OK, the VM kernel (6.12.91, not the node's) and the pod IP oneth0;kubectl execwithout 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/podsand norc vmmprocess 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 anllvmpipeline (software Vulkan);- the
llama-benchtable withVulkan0,pp128in the 900–1,365 t/s range andtg32at ~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; echoExpected:
{"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/syncExpected:
400: an unknown field (allowedHost, with a typo) is refused, not ignored with egress left open;"allow_hosts":["api.example.com"],"enabled":trueand"memory_mib":512: the alternative names are accepted;400onnestedVirt(only allowed withrc serve --allow-nested-virt);event: stdout/data: a, thendata: b: one line per event, raw text, andevent: exitat the end;204onsync(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-... runningandexec: ola do sdk go(orpython): 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:
curlreturns<meta name="go-import" content="runner.codes git https://github.com/andrebassi/runner-codes">;go getdownloadsrunner.codes v1.1.0(or newer);- the
go runoutput is the same as in 10b:job: 42,exec: ola do sdk goanderror: 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)andnavegador 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
| Symptom | Where to look |
|---|---|
image for X not built | ./bin/rc image build --stack X |
read stream: the guest in this image does not support streaming | image from before streaming: rebuild |
INVALID_STATE: machine X is not running | ./bin/rc machine start --name X |
CONFLICT: machine X already exists | another name, or delete first |
start fails with the end of the console | ./bin/rc machine logs --name X |
GPU: ERROR_OUT_OF_HOST_MEMORY | GPU › 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
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.
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.
Fast enough to skip the container.
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.
15 in the catalog, plus an LLM.
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…).
- 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.
Mac and Linux, no Docker.
runner.codes uses the hypervisor the system already has. No Docker, no cloud machine.
Apple Silicon, through Hypervisor.framework. This is where the numbers on this page were measured.
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.
libkrun has no Hyper-V backend. The way is WSL2 with nested virtualization, where the Linux rc runs.
Ready to run, tested on a VM.
Every example comes up on a real VM, and every guide has a run.sh that proves what it promises.
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")'
Docs an agent reads in one go.
Every command, flag, route and error, on a single page.
runner-code.mdThe machine file, key by key.
network.mdEgress policy, ports, volumes, S3, named network.
credentials.mdCredentials the machine uses without reading.
pack.mdThe machine in an executable.
architecture.mdHow the host, the VMM and the guest talk to each other.
runbook.mdThe step-by-step guide to test by hand.
languages.yamlThe language catalog, with image and limits.