Child Processes
Two ways to run processes in Secure Exec:
- Guest
node:child_process: guest code spawns commands inside the VM. Every child is a kernel-managed process, never a real host process. rt.spawn(code): the host starts a long-running guest program and gets a live handle (pid,writeStdin,kill,wait,exitCode).
Guest child_process
Section titled “Guest child_process”Guest code uses the standard node:child_process module to spawn commands available in the VM (sh, node, and the mounted coreutils):
import { NodeRuntime } from "secure-exec";
const rt = await NodeRuntime.create();
try { const { stdout, stderr, exitCode } = await rt.exec(` import { execFileSync } from "node:child_process";
// Spawn a shell command and capture its stdout. const shellOut = execFileSync("sh", ["-c", "echo hello from a child process"], { encoding: "utf8", }); console.log("sh output:", shellOut.trim());
// Spawn node as a child process and read its version. const nodeVersion = execFileSync("node", ["--version"], { encoding: "utf8", }); console.log("child node version:", nodeVersion.trim()); `);
console.log("exitCode:", exitCode); if (stderr.trim()) console.log("guest stderr:", stderr.trim()); console.log("guest stdout:"); console.log(stdout.trim());} finally { await rt.dispose();}Output:
exitCode: 0guest stdout:sh output: hello from a child processchild node version: v22.0.0execFileSyncis used for brevity; the async/streaming APIs (spawn,exec,execFile) also work for incremental stdout/stderr or writing to a child’s stdin.- Children run any command provided by the mounted runtimes. By default that is WASM-backed
sh+ coreutils and V8-backednode. - Point at a different set of WASM command binaries with
commandsDir:
const rt = await NodeRuntime.create({ commandsDir: "/path/to/wasm/commands",});Where the commands come from
Section titled “Where the commands come from”The guest sh and the coreutils it drives ship as WASM binaries. The kernel cannot spawn any guest process without them, so they are mounted through the WASM runtime at boot. This is how node:child_process and the shell work inside the VM with no host processes ever involved.
The commandsDir create option overrides where those WASM command binaries are loaded from. When unset, the runtime resolves a directory using the first match in this order:
- an explicit
commandsDiroption, - the
SECURE_EXEC_WASM_COMMANDS_DIRenvironment variable, - the in-repo build output (
registry/native/target/wasm32-wasip1/release/commands), present only in developer checkouts, - the commands vendored into the installed
@secure-exec/corepackage (published installs).
The in-repo build output wins over the bundled copy so local edits are picked up without re-vendoring; a fresh npm install has no in-repo path and falls through to the vendored commands. See the TypeScript SDK reference for the full create-option shape.
Long-running guests with rt.spawn
Section titled “Long-running guests with rt.spawn”rt.exec runs to completion and returns captured output. rt.spawn(code) returns a live handle immediately while the guest keeps running. It is the building block for dev servers and other long-lived guests.
const proc = await rt.spawn(` process.stdin.on("data", (chunk) => { process.stdout.write("got: " + chunk.toString()); });`);
proc.writeStdin("hello\n"); // feed stdinproc.closeStdin(); // signal end-of-input
const exitCode = await proc.wait();console.log(proc.pid, exitCode);See the TypeScript SDK reference for the full NodeRuntimeProcess and NodeRuntimeSpawnOptions shapes. Stream output by passing onStdout / onStderr, which receive raw Uint8Array chunks:
const proc = await rt.spawn("setInterval(() => console.log('tick'), 100)", { onStdout: (chunk) => process.stdout.write(new TextDecoder().decode(chunk)),});// ... laterproc.kill(); // SIGTERMawait proc.wait();Driving a guest server
Section titled “Driving a guest server”Spawn a server, wait for it to listen, then drive requests into it with rt.fetch, entirely inside the VM, even when guest network egress is denied:
const server = await rt.spawn(` import http from "node:http"; http.createServer((_, res) => res.end("ok")).listen(3000);`);
const listener = await rt.waitForListener({ port: 3000 });const res = await rt.fetch(listener.port ?? 3000, { path: "/" });console.log(res.status, res.body); // 200 ok
server.kill();await server.wait();Underlying process model
Section titled “Underlying process model”- The kernel process table, signals, and shell that back
node:child_processandrt.spawnare documented in agentOS: Processes & Shell.