mininux
v0.1.3
Published
A lightweight, embeddable Linux sandbox running in WASM. Execute shell commands, Python scripts, and create isolated sessions—perfect for sandboxed code execution, LLM backends, and educational projects.
Maintainers
Readme
MiniNux 🐧
A lightweight, embeddable Linux sandbox running in WASM. Execute shell commands, Python scripts, and create isolated sessions—all in your browser or Node.js backend.
🎉 This is a fun experimental project — it's not for production critical workloads, but perfect for learning, demos, LLM agents, CTF challenges, and creative hacks.
Features
- ⚡ Tiny & Fast — 32 MiB WASM VM with BusyBox, MicroPython, and v86 emulation
- 🔒 Isolated Sessions — Separate workspaces with environment state and RAM filesystems
- 🐍 Python Ready — MicroPython built-in (
python/python3) for data processing and scripts - 🟢 Node.js & npm — opt-in
profile: "node"boots real Node.js 22 with a workingnpm install - 🌐 Opt-in Networking —
network: truegives the guest real outbound TCP (DNS, HTTPS) via an in-process Wisp relay - 📦 Easy Embedding — One
Linuxinstance = one VM, multiple instances = parallel execution - 🎯 Session State — Environment variables and working directory persist across commands
- ⏱️ Timeouts & Controls — Per-command
timeoutMs, kill long-running processes - 🔍 Structured Output —
stdout,stderr,exitCode,timedOut, anddurationMsin every response
Quick Start
Installation
npm install mininuxBasic Usage
Works out of the box in Node.js and Bun — the Linux assets ship inside the package:
import { Linux } from "mininux";
const linux = new Linux();
await linux.start(); // optional — execute() boots the VM on first use
// Execute a command
const result = await linux.execute('echo "Hello from MiniNux!"');
console.log(result.stdout); // "Hello from MiniNux!"
console.log(result.exitCode); // 0
await linux.destroy(); // shut the VM down when you're doneYour project needs
"type": "module"in package.json (or use an.mjsfile).
Browser Usage
In the browser, serve the four asset files (v86.wasm, seabios.bin, vgabios.bin, bzImage from node_modules/mininux/dist/assets) under /mini-linux/ — that's the default path — and the same zero-config code works. To serve them somewhere else, pass assets:
const linux = new Linux({
assets: {
wasm: '/my-assets/v86.wasm',
bios: '/my-assets/seabios.bin',
vgaBios: '/my-assets/vgabios.bin',
kernel: '/my-assets/bzImage',
},
});Working with Sessions
Sessions maintain state across commands—environment variables, working directory, and files. session() is synchronous and returns a LinuxSession:
const session = linux.session('my-workspace');
// Commands run in the same context
await session.execute('export NAME=MiniNux');
await session.execute('cd /tmp');
const result = await session.execute('echo $NAME && pwd');
console.log(result.stdout);
// Output:
// MiniNux
// /tmp
// Wipe the session's files, cwd, and environment
await session.reset();Each session starts in its own workspace at /work/sessions/<name>.
Python Scripts
MicroPython is available as both python and python3:
const result = await linux.execute(`python3 << 'EOF'
import json
data = {"status": "running", "version": "1.0"}
print(json.dumps(data))
EOF
`);
console.log(JSON.parse(result.stdout));Note: this is MicroPython, not CPython — most of the core language and common modules (
json,os,sys, …) work, but not the full standard library or pip packages.
Node.js & npm
The node profile boots a bigger rootfs (rootfs-node.cpio.gz) with real Node.js 22 and npm. Combine it with network: true and npm install talks to the real registry — the guest does its own DNS and TLS through a local Wisp relay the SDK starts on 127.0.0.1:
const linux = new Linux({
profile: "node", // Node.js 22 + npm image, 256 MiB RAM default
network: true, // real outbound TCP (Node.js 22+ host required)
});
const session = linux.session("app");
await session.execute("node --version");
await session.execute("npm install left-pad --no-audit --no-fund", {
timeoutMs: 600_000,
});
const result = await session.execute(
`node -e "console.log(require('left-pad')('42', 6, '0'))"`,
);
console.log(result.stdout); // "000042"Expectations to set:
- It is slow. v86 emulates a ~100 MHz-class CPU:
node -etakes seconds,npm installtakes minutes. The node profile raises the default command timeout to 120 s (max 900 s) and the boot timeout to 180 s. - Pure-JS packages only. There is no compiler in the guest and no prebuilt binaries exist for 32-bit musl, so packages with native addons won't build.
- Networking is off by default — sandboxed code only reaches the internet when you opt in. In the browser, pass
network: { relayUrl: "wisps://your-relay/" }pointing at a Wisp or WebSocket ethernet relay you host.
In the browser, also serve rootfs-node.cpio.gz alongside the other assets.
File Operations
The shell is BusyBox ash (POSIX sh — no bashisms like {1..5} brace expansion):
const session = linux.session('files');
// Write to the session workspace (its default cwd)
await session.execute('echo "config data" > config.txt');
// Read and process
const result = await session.execute('cat config.txt');
console.log(result.stdout);
// Create scripts on the fly
await session.execute(`cat > /tmp/process.sh << 'EOF'
#!/bin/sh
for i in $(seq 1 5); do
echo "Iteration $i"
done
EOF
chmod +x /tmp/process.sh
/tmp/process.sh
`);Real-World Examples
1. LLM Agent Backend
Use MiniNux to safely execute code suggestions from Claude or other LLMs:
import { Linux } from 'mininux';
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
const linux = new Linux();
const userRequest = "Write a script that calculates fibonacci up to 10 numbers";
const message = await client.messages.create({
model: "claude-sonnet-5",
max_tokens: 1024,
messages: [{ role: "user", content: userRequest }],
system: "You are a helpful assistant. Write Python code when asked. Wrap code in ```python...``` blocks."
});
// Extract code from response
const code = message.content[0].text.match(/```python\n([\s\S]*?)\n```/)[1];
// Execute safely in MiniNux sandbox
const result = await linux.execute(`python3 << 'EOF'\n${code}\nEOF`);
console.log("Output:", result.stdout);
console.log("Errors:", result.stderr);2. Educational CTF/Hacking Challenges
Create isolated environments for security challenges:
const sandbox = new Linux();
// Set up challenge environment
const session = sandbox.session('ctf-challenge-1');
await session.execute('echo "flag{hidden_data_here}" > flag.txt');
await session.execute('chmod 600 flag.txt');
// User attempts to read flag
const userAttempt = await session.execute('cat flag.txt');
if (userAttempt.exitCode !== 0) {
console.log("Access denied! Try another approach...");
}3. Backend API for Command Execution
Build a safe REST API that executes user-submitted commands. Timeouts are built in — pass timeoutMs per command:
import express from 'express';
import { Linux } from 'mininux';
const app = express();
const linuxInstances = new Map();
function getOrCreateLinux(sessionId) {
if (!linuxInstances.has(sessionId)) {
linuxInstances.set(sessionId, new Linux());
}
return linuxInstances.get(sessionId);
}
app.post('/execute', express.json(), async (req, res) => {
const { sessionId, command, timeout = 30000 } = req.body;
if (!command || command.trim().length === 0) {
return res.status(400).json({ error: 'Command required' });
}
try {
const linux = getOrCreateLinux(sessionId);
const result = await linux.execute(command, {
session: sessionId,
timeoutMs: timeout,
});
res.json({
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode,
timedOut: result.timedOut,
durationMs: result.durationMs
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(3000, () => console.log('API running on :3000'));Usage:
curl -X POST http://localhost:3000/execute \
-H 'Content-Type: application/json' \
-d '{
"sessionId": "user-123",
"command": "ls -la /work",
"timeout": 30000
}'Session names (like all identifiers passed to
session()/execute({ session })) must match[A-Za-z0-9_-]+— validate user input first.
4. Multi-Tenant Sandbox Service
Parallel execution with isolated VMs:
import { Linux } from 'mininux';
// Each user gets their own VM for true isolation
const userSandboxes = new Map();
async function executeForUser(userId, command) {
if (!userSandboxes.has(userId)) {
userSandboxes.set(userId, new Linux());
}
return userSandboxes.get(userId).session(userId).execute(command);
}
// Parallel execution, no crosstalk
await Promise.all([
executeForUser('alice', 'echo "VM A"'),
executeForUser('bob', 'echo "VM B"'),
executeForUser('charlie', 'echo "VM C"')
]);API Reference
new Linux(options?)
Main class for managing a v86 WASM Linux instance.
interface LinuxOptions {
assets?: Partial<LinuxAssets>; // wasm, bios, vgaBios, kernel, initrd?
// Default: files under /mini-linux/ (browser)
profile?: "minimal" | "node"; // "node" = Node.js 22 + npm image. Default: "minimal"
network?: boolean | { relayUrl?: string }; // Emulated NIC with outbound TCP. Default: off
memoryMiB?: number; // Guest RAM, rounded up to a power of two. Default: 32 (node: 256)
bootTimeoutMs?: number; // Time allowed for boot. Default: 15_000 (node: 180_000)
commandTimeoutMs?: number; // Default per-command timeout. Default: 10_000 (node: 120_000, max 900_000)
onConsoleLine?: (line: string) => void; // Raw serial output, useful for boot debugging
kernelCommandLine?: string; // Extra kernel arguments
}start(): Promise<void>
Boot the VM and wait until the guest agent is ready. Optional — execute() boots on first use.
execute(command: string, options?): Promise<ExecuteResult>
Run a shell command (BusyBox ash).
interface ExecuteOptions {
session?: string; // Named session to run in. Default: "default"
timeoutMs?: number; // Per-command timeout override
}const result = await linux.execute('whoami');
// { stdout: 'root\n', stderr: '', exitCode: 0, timedOut: false, durationMs: 145 }session(name: string): LinuxSession
Get a handle to a named session with persistent state (synchronous).
resetSession(name: string): Promise<void>
Delete a session's files, working directory, and environment snapshot.
saveState(): Promise<ArrayBuffer> / restoreState(state): Promise<void>
Snapshot and restore the entire VM (RAM, all sessions). State files are v86-version-specific.
destroy(): Promise<void>
Shut down the VM and reject any pending commands. Always call when done.
LinuxSession
A persistent workspace with environment variables and working directory, rooted at /work/sessions/<name>.
execute(command: string, options?): Promise<ExecuteResult>
Run a command in this session's context.
reset(): Promise<void>
Wipe this session's files, cwd, and environment.
ExecuteResult
interface ExecuteResult {
stdout: string; // Command output
stderr: string; // Error output
exitCode: number; // Exit code (0 = success)
timedOut: boolean; // Whether command exceeded timeout
durationMs: number; // Execution time in milliseconds
}Architecture
MiniNux runs a complete Linux stack in WASM:
┌─────────────────────────────────────┐
│ Your JavaScript/Node.js App │
└────────────┬────────────────────────┘
│
├─ Linux.execute()
└─ LinuxSession.execute()
│
┌────────────▼────────────┐
│ TypeScript SDK │
│ (serial frame protocol) │
└────────────┬────────────┘
│
┌────────────▼──────────────────┐
│ v86 x86 WASM Emulator │
│ (32-bit Linux kernel) │
└────────────┬──────────────────┘
│
┌────────────▼──────────────────┐
│ BusyBox initramfs │
│ + MicroPython runtime │
│ + session RAM filesystems │
└───────────────────────────────┘Key Points:
- Each
Linuxinstance = one v86 VM (isolated, 32 MiB) - Commands serialized through base64-encoded serial frames
- Sessions live in
/work/sessions/<name>with persistent state - All execution is sandboxed—no access to host filesystem
- Commands are queued per instance and executed one at a time
Building From Source
# Prerequisites: Node.js 20+, Buildroot, make
npm install
# Build guest (Linux kernel + initramfs)
npm run build:guest
# Prepare v86 assets
npm run assets:v86
# Test everything
npm run test:v86
# Publish to NPM
npm publishSee CHUNKING.md for details on how large binary assets are split for GitHub.
Contributing
This is an experimental project, and contributions are welcome!
Areas for improvement:
- Performance — Optimize WASM initialization and command latency
- Features — Add syscall passthrough for real hardware access
- Docs — More examples and use-cases
- Testing — Security hardening and fuzzing
- UX — Better error messages and debugging
To contribute:
- Fork the repo
- Create a feature branch
- Write tests
- Submit a PR
Limitations & Caveats
- ⚠️ Not production-grade — Use for fun, learning, and experiments
- ⚠️ WASM performance — ~100-500ms overhead per command (v86 emulation)
- ⚠️ Memory-heavy — Each VM instance uses ~32 MiB
- ⚠️ MicroPython, not CPython — no pip, partial standard library
- ⚠️ BusyBox ash, not bash — POSIX sh syntax only
- ⚠️ Limited syscalls — Some Linux features not available in emulation
- ⚠️ Networking is opt-in — no internet access unless you pass
network: true(Node 22+) or a relay URL - ⚠️ Node profile is heavy — bigger download, 256 MiB RAM per VM, slow startup, pure-JS npm packages only
- ⚠️ Command size limit — Commands are capped at 64 KiB of UTF-8
- ⚠️ Browser compatibility — Requires WebAssembly support (all modern browsers)
Inspiration
Built for Cogniva (an AI blog research platform) to safely execute LLM-generated code. Perfect for:
- 🤖 AI agent backends
- 🎮 Interactive coding tutorials
- 🛡️ Security CTF challenges
- 📚 Educational computing environments
- 🔬 Isolated code experimentation
License
MIT — Feel free to use, modify, and distribute. Have fun!
Questions?
Open an issue on GitHub or reach out to the community.
Happy hacking! 🚀
