Hand off a running Linux VM between hosts. Freeze it on your laptop, thaw it on a server, resume it next week. The program picks up exactly where it left off — like waking a laptop from sleep, except on a different computer.
A native microVM runtime under the hood: arm64 on Apple Silicon/Linux and amd64 on Linux/KVM. Node.js is the first-class target; Python, bash, and anything else that boots in a Linux VM works too.
Note: the source code isn't published yet — it'll be available here soon.
npm i @machinen/cli @machinen/runtimeThen run the CLI with npx machinen … (or the shorter npx mn … — both
names install). Prefer it on your PATH? npm i -g @machinen/cli is fine
too.
The right native package is pulled automatically via optional dependencies:
@machinen/native-arm64-darwin on Apple Silicon Macs,
@machinen/native-arm64-linux on arm64 Linux, and
@machinen/native-x64-linux on amd64 Linux. No system dependencies.
First run fetches the matching kernel + rootfs from a GitHub release on the companion repo over plain HTTPS — no auth required.
Bake an image, boot it, accumulate some state, then move the running process to another host.
A tiny HTTP server that counts hits in memory:
// counter.mjs
import { createServer } from "node:http";
let count = 0;
createServer((_, res) => {
res.end(JSON.stringify({ count: ++count }) + "\n");
}).listen(3000);Bake it into a rootfs tarball with provision():
// bake.ts
import { readFileSync } from "node:fs";
import { provision } from "@machinen/runtime";
await provision({
install: async (vm) => {
await vm.exec("apt-get update && apt-get install -y nodejs");
await vm.writeFile("/opt/counter.mjs", readFileSync("./counter.mjs"));
},
cmd: ["/usr/bin/node", "/opt/counter.mjs"],
out: "./counter.tar.gz",
});node bake.tsnpx machinen boot --name counter -p 3000:3000 --detached ./counter.tar.gz
curl localhost:3000 # { count: 1 }
curl localhost:3000 # { count: 2 }The process is now sitting on host A with count = 2 in its heap.
Freeze it, copy the bundle to host B, thaw it:
npx machinen snapshot counter ./counter.snap
scp ./counter.tar.gz ./counter.snap host-b:
ssh host-b npx machinen restore ./counter.snap -p 3000:3000 &
curl host-b:3000 # { count: 3 } ← same processSame guest architecture only (arm64 ↔ arm64, amd64 ↔ amd64). Cross-ISA restore is not supported. Memory, file descriptors, and timers come back exactly as they were.
The bundle remembers the absolute path of the rootfs tarball you booted
from. On the same host that's all you need — restore reuses the same
tarball so CRIU can re-open file-backed VMAs (executable, shared
libraries) at the paths they were dumped from. Across hosts, copy the
tarball to the same path or pass --image <tarball> to override.
fork is snapshot + restore without killing the source. The original keeps
running; you get a sibling VM with the same heap, same open files, and a
copy-on-write disk. Both processes diverge from the same instant.
Pick up from Step 2 above — counter is running with count = 2:
npx machinen fork counter --new-name counter-b --detach
npx machinen exec counter -- curl -s localhost:3000 # { count: 3 }
npx machinen exec counter-b -- curl -s localhost:3000 # { count: 3 }
npx machinen exec counter-b -- curl -s localhost:3000 # { count: 4 }
npx machinen exec counter -- curl -s localhost:3000 # { count: 4 }Both VMs branched from the same count = 2 heap and now count
independently. Use it to clone a warmed-up process: a database with caches
loaded, a test fixture in exactly the right state, a long-running compute
job branched into N parallel explorations.
The fork doesn't inherit the source's -p host forwards — host ports are
global, only one process can bind each one. Two ways to reach a fork:
# A) exec over vsock — works for any guest port, no host forward needed.
npx machinen exec counter-b -- curl -s localhost:3000
# B) -p with non-conflicting host ports — forwards on the host.
npx machinen fork counter --new-name counter-b -p 3001:3000 --detach
curl localhost:3001 # the fork
curl localhost:3000 # still the sourcePass -p multiple times for multiple ports. If you pick a host port the
source is already forwarding, fork errors with
BOOT_PORT_FORWARD_IN_USE and names the VM that's holding it.
From Node, same shape:
const fork = await vm.fork({ name: "counter-b" });Same arc, driven from TypeScript:
import { readFileSync } from "node:fs";
import { boot, provision, restore } from "@machinen/runtime";
await provision({
install: async (vm) => {
await vm.exec("apt-get install -y nodejs");
await vm.writeFile("/opt/counter.mjs", readFileSync("./counter.mjs"));
},
cmd: ["/usr/bin/node", "/opt/counter.mjs"],
out: "./counter.tar.gz",
});
const vm = await boot({ image: "./counter.tar.gz", name: "counter" });
// ... let it run, serve traffic, accumulate state ...
await vm.snapshot({ outDir: "./counter.snap" });
// elsewhere (possibly on another host):
const restored = await restore({ snapDir: "./counter.snap" });- Quickstart — the same three-step walkthrough with more colour
- Guides — recipes for creating VMs, snapshots and forks, mounts, and networking
@machinen/clireference — every command and flag@machinen/runtimereference — every exported function, type, and error class (typedoc-generated)
Three runnable demos live in examples/:
quickstart— the counter walkthrough above as a runnable repo.fork-pi— snapshot a VM with thepicoding agent installed, then fork three siblings that each answer a different prompt in parallel.live-mount— host directory mounted into the guest over an in-VMM virtio-fs device; bidirectional, no rebuild on edit.
npx machinen boot -- /bin/sh # ad-hoc: boot base + run a cmd
npx machinen boot ./my-image.tar.gz # boot a provisioned rootfs tarball
npx machinen install # pre-fetch base assets (CI / airgap)
npx machinen install --version <tag> # pin to a specific release tagFSL-1.1-MIT — Functional Source License. Converts to MIT two years after each release.