skills/gondolin/SKILL.md
Isolated micro-VMs for sandboxed code execution (untrusted code, testing, builds)
npx skillsauth add jcsaaddupuy/badrobots gondolinInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Run untrusted code, install packages, or execute commands in isolated Linux micro-VMs with full control over network, filesystem, and environment.
import { VM } from "@earendil-works/gondolin";
const vm = await VM.create();
const result = await vm.exec("echo 'Hello from isolated VM'");
console.log(result.stdout);
await vm.close();
const shell = vm.shell({ attach: false });
shell.write('cd /tmp\n');
shell.write('npm install lodash\n');
shell.write('node -e "console.log(require(\\"lodash\\").VERSION)"\n');
shell.write('exit\n');
for await (const chunk of shell) {
process.stdout.write(chunk);
}
Use Cases:
Key Features:
VM.create() → vm.exec() / vm.shell() → vm.close()
↓ ↓ ↓
Boot VM Run commands Destroy VM
Important: Always call vm.close() to free resources.
# Install Gondolin
npm install -g @earendil-works/gondolin
# Install QEMU (required)
# macOS
brew install qemu
# Linux (Debian/Ubuntu)
sudo apt install qemu-system-x86-64 qemu-system-aarch64
Create gondolin-import.mjs for cross-environment compatibility:
import { execSync } from 'child_process';
import { existsSync } from 'fs';
import { join } from 'path';
function getGlobalNodeModules() {
// 1. Check NODE_PATH
if (process.env.NODE_PATH) {
const paths = process.env.NODE_PATH.split(':');
for (const p of paths) {
if (p && existsSync(p)) return p;
}
}
// 2. Try npm root -g
try {
return execSync('npm root -g', { encoding: 'utf-8' }).trim();
} catch {}
// 3. Platform defaults
return process.platform === 'darwin'
? '/opt/homebrew/lib/node_modules'
: '/usr/local/lib/node_modules';
}
const gondolinPath = join(
getGlobalNodeModules(),
'@earendil-works/gondolin/dist/src/index.js'
);
if (!existsSync(gondolinPath)) {
throw new Error('Gondolin not found. Install: npm install -g @earendil-works/gondolin');
}
export const { VM, createHttpHooks, MemoryProvider, RealFSProvider } =
await import(`file://${gondolinPath}`);
Then use: import { VM } from "./gondolin-import.mjs";
import { VM } from "@earendil-works/gondolin";
const vm = await VM.create();
// Single command
const result = await vm.exec("uname -a");
console.log(result.stdout);
console.log(result.exitCode); // 0 = success
// Array syntax (no shell)
const result2 = await vm.exec(["python3", "-c", "print('Hello')"]);
await vm.close();
const vm = await VM.create();
const shell = vm.shell({ attach: false });
// Send commands
shell.write('cd /tmp\n');
shell.write('mkdir workspace\n');
shell.write('echo "test" > file.txt\n');
shell.write('cat file.txt\n');
shell.write('exit\n');
// Stream output
for await (const chunk of shell) {
process.stdout.write(chunk);
}
await vm.close();
const vm = await VM.create({
sandbox: {
rootOverlay: true // Enable writable root filesystem
}
});
const shell = vm.shell({ attach: false });
shell.write('apk update\n');
shell.write('apk add nodejs npm\n');
shell.write('node --version\n');
shell.write('exit\n');
for await (const chunk of shell) {
process.stdout.write(chunk);
}
await vm.close();
Note: Changes are NOT persistent (lost when VM closes).
import { VM, createHttpHooks } from "@earendil-works/gondolin";
const { httpHooks, env } = createHttpHooks({
allowedHosts: ["api.github.com"],
secrets: {
GITHUB_TOKEN: {
hosts: ["api.github.com"],
value: process.env.GITHUB_TOKEN
}
}
});
const vm = await VM.create({ httpHooks, env });
// Secret is injected only for allowed hosts
const result = await vm.exec(
'curl -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/user'
);
await vm.close();
Security: Secrets are never visible to guest, only injected by host.
Problem: By default, Gondolin blocks requests to private IP ranges (10.x.x.x, 192.168.x.x, 172.16.x.x) for security.
Solution: Use blockInternalRanges: false when you need to access internal corporate services:
import { VM, createHttpHooks } from "@earendil-works/gondolin";
const { httpHooks, env } = createHttpHooks({
allowedHosts: [".*"],
blockInternalRanges: false, // ⚠️ Allow internal IPs
secrets: {
API_KEY: {
hosts: ["internal-api.company.com"],
value: process.env.API_KEY
}
}
});
const vm = await VM.create({ httpHooks, env });
// Can now access services that resolve to private IPs
const result = await vm.exec(
'curl -H "Authorization: Bearer $API_KEY" https://internal-api.company.com/health'
);
await vm.close();
How it works:
blockInternalRanges: true (default)Troubleshooting:
nslookup internal-api.company.com
# If it returns 10.x.x.x or 192.168.x.x, you need blockInternalRanges: false
gondolin CLI does NOT expose this option - use SDK directlyimport { VM, MemoryProvider } from "@earendil-works/gondolin";
const vm = await VM.create({
vfs: {
mounts: {
"/workspace": new MemoryProvider() // In-memory filesystem
}
}
});
// Write to virtual filesystem
await vm.exec("echo 'data' > /workspace/file.txt");
await vm.exec("cat /workspace/file.txt");
await vm.close();
VFS Providers:
MemoryProvider() - In-memory (fast, ephemeral)RealFSProvider(hostPath) - Mount host directory (read/write)ReadonlyProvider(hostPath) - Read-only mountExample: Mount host directory (from container context):
import { VM, RealFSProvider } from "@earendil-works/gondolin";
const vm = await VM.create({
vfs: {
mounts: {
"/root/.pi": new RealFSProvider("/root/.pi") // Mount host directory
}
}
});
// Files from host are accessible in VM
await vm.exec("ls -la /root/.pi/agent/extensions");
await vm.close();
Common use case: When running in Docker, mount the container's directory that's volume-mounted from the host:
Host: ~/.pi/agent/extensions/
↓ (Docker -v)
Container: /root/.pi/agent/extensions/
↓ (VFS RealFSProvider)
VM: /root/.pi/agent/extensions/
const vm = await VM.create();
const proc = vm.exec(["bash", "-c", "for i in {1..5}; do echo $i; sleep 1; done"], {
stdout: "pipe" // Required for streaming
});
// Stream output as it arrives
for await (const chunk of proc) {
console.log("Output:", chunk);
}
const result = await proc;
console.log("Exit code:", result.exitCode);
await vm.close();
⚠️ IMPORTANT: Building custom images requires the Gondolin Git repository. The npm package does NOT support this.
For npm package users, use runtime installation instead (Pattern 3).
For persistent packages with the Git repository:
1. Clone repository:
git clone https://github.com/earendil-works/gondolin.git
cd gondolin
npm install
2. Install Zig (required for guest binaries):
# macOS
brew install zig
# Linux
wget https://ziglang.org/download/0.15.2/zig-linux-x86_64-0.15.2.tar.xz
tar xf zig-linux-x86_64-0.15.2.tar.xz
export PATH=$PATH:$(pwd)/zig-linux-x86_64-0.15.2
3. Create build-config.json:
{
"arch": "aarch64",
"distro": "alpine",
"alpine": {
"version": "3.23.0",
"kernelPackage": "linux-virt",
"kernelImage": "vmlinuz-virt",
"rootfsPackages": [
"linux-virt",
"rng-tools",
"bash",
"ca-certificates",
"curl",
"nodejs",
"npm",
"python3",
"git"
],
"initramfsPackages": []
},
"rootfs": {
"label": "gondolin-root"
}
}
Note: Use "arch": "x86_64" for Intel/AMD processors.
4. Build guest binaries:
make -C guest
5. Build the image:
npx tsx host/bin/gondolin.ts build \
--config build-config.json \
--output ./custom-assets
6. Use the custom image:
const vm = await VM.create({
sandbox: {
imagePath: "./custom-assets"
}
});
// Packages already installed!
const result = await vm.exec("node --version");
Or set environment variable:
export GONDOLIN_GUEST_DIR=./custom-assets
node your-script.mjs
const vm = await VM.create({
// Network policy
httpHooks, // From createHttpHooks()
env, // Environment variables
// Virtual filesystem
vfs: {
mounts: {
"/path": new MemoryProvider()
},
hooks: {
// Intercept file operations
}
},
// Sandbox configuration
sandbox: {
rootOverlay: true, // Writable root (tmpfs)
imagePath: "./assets", // Custom guest image
debug: ["*"], // Enable debug logs
}
});
vm.exec(command, {
stdin: true, // Enable stdin writing
stdout: "pipe", // Enable stdout streaming
stderr: "pipe", // Enable stderr streaming
pty: true, // Allocate pseudo-terminal
env: { VAR: "value" }, // Additional env vars
cwd: "/tmp" // Working directory
})
vm.shell({
command: ["/bin/bash", "-i"], // Shell command
attach: false, // Don't auto-attach (good for automation)
env: { PS1: "vm> " }, // Environment
cwd: "/root" // Working directory
})
The gondolin CLI is great for quick interactive testing:
# Simple interactive shell
gondolin bash
# With host mounts
gondolin bash --mount-hostfs /data:/workspace
# With secrets
gondolin bash --host-secret [email protected]
Use the SDK (Node.js) when you need:
blockInternalRanges: false (not available in CLI)onRequest, onResponse, onRequestHeadThe CLI does not expose these SDK options:
blockInternalRanges - Cannot disable internal IP blockingonRequest, onResponse - No custom HTTP hooksExample: Accessing internal services requires SDK:
// ✅ SDK - Can access internal IPs
import { VM, createHttpHooks } from "@earendil-works/gondolin";
const { httpHooks, env } = createHttpHooks({
allowedHosts: [".*"],
blockInternalRanges: false // CLI cannot do this!
});
const vm = await VM.create({ httpHooks, env });
# ❌ CLI - Cannot disable internal IP blocking
gondolin bash --allow-host internal-api.company.com
# Still blocked if resolves to 10.x.x.x!
Always close VMs
const vm = await VM.create();
try {
await vm.exec("...");
} finally {
await vm.close();
}
Use attach:false for automation
const shell = vm.shell({ attach: false });
Check exit codes
const result = await vm.exec("command");
if (result.exitCode !== 0) {
console.error("Command failed:", result.stderr);
}
Stream output for long-running commands
const proc = vm.exec(cmd, { stdout: "pipe" });
for await (const chunk of proc) {
console.log(chunk);
}
Use custom images for production
Don't forget to close VMs
// ❌ BAD: VM leaked
const vm = await VM.create();
await vm.exec("...");
// Missing vm.close()
Don't use attach:true without TTY
// ❌ BAD: Will hang in automation
vm.shell({ attach: true });
Don't rely on runtime installs for production
// ❌ BAD: Slow, not persistent
vm.exec("apk add nodejs");
// ✅ GOOD: Build custom image instead
Don't ignore network policy
// ❌ BAD: Unrestricted network access
VM.create();
// ✅ GOOD: Explicit allowlist
VM.create({
httpHooks: createHttpHooks({
allowedHosts: ["api.example.com"]
})
});
Don't use array form exec() when shell expansion needed
// ❌ BAD: $VAR sent literally, not expanded
vm.exec(["curl", "-H", `Authorization: Bearer $SECRET`, ...])
// ✅ GOOD: Shell expands variables
vm.exec('curl -H "Authorization: Bearer $SECRET" ...')
Don't add custom onRequest hooks without understanding
// ❌ BAD: Bypasses secret injection
createHttpHooks({
secrets: {...},
onRequest: (req) => req // Breaks secret replacement!
})
// ✅ GOOD: Let Gondolin handle secret injection
createHttpHooks({ secrets: {...} })
Don't forget PATH in interactive shells
// ❌ BAD: Commands might not be found
vm.shell({ command: ["/bin/bash", "-i"] })
// ✅ GOOD: Explicit PATH
vm.shell({
command: ["/bin/bash", "-i"],
env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" }
})
# macOS
brew install qemu
# Linux
sudo apt install qemu-system-x86-64 qemu-system-aarch64
# Verify
which qemu-system-aarch64
Guest images (~200MB) auto-download on first run.
Cache location:
~/.cache/gondolin/~/.cache/gondolin/Manual download:
GONDOLIN_GUEST_DIR=/path/to/cache gondolin bash
Check installation:
npm list -g @earendil-works/gondolin
Set NODE_PATH:
export NODE_PATH=$(npm root -g)
node your-script.mjs
Use portable import helper (see Installation section).
Add hosts to allowedHosts:
const { httpHooks } = createHttpHooks({
allowedHosts: [
"registry.npmjs.org",
"dl-cdn.alpinelinux.org"
]
});
Symptom: Requests to corporate/internal services get 403 errors, but public APIs work fine.
Diagnosis:
# Check if the hostname resolves to private IPs
nslookup internal-api.company.com
# If you see 10.x.x.x, 192.168.x.x, or 172.16.x.x → internal IP blocking
Solution: Disable internal IP range blocking (SDK only, not available in CLI):
const { httpHooks, env } = createHttpHooks({
allowedHosts: [".*"],
blockInternalRanges: false, // Allow internal IPs
secrets: {
API_KEY: {
hosts: ["internal-api.company.com"],
value: process.env.API_KEY
}
}
});
Why it happens:
CLI limitation: The gondolin bash command does NOT expose blockInternalRanges option. You must use the SDK directly with a custom script.
The npm package does NOT support building custom images.
Solution 1: Use runtime installation instead (recommended)
const vm = await VM.create({
sandbox: { rootOverlay: true }
});
await vm.exec("apk add nodejs npm");
Solution 2: Build from Git repository
git clone https://github.com/earendil-works/gondolin.git
cd gondolin
npm install
make -C guest # Build Zig binaries
npx tsx host/bin/gondolin.ts build --config config.json --output ./assets
Why: The npm package only includes compiled code for running VMs, not the guest source code (Zig) needed for building custom images.
Runtime installations with rootOverlay: true are temporary.
Solution: Build a custom guest image (see Pattern 7).
Check architecture:
const result = await vm.exec("uname -m");
console.log(result.stdout); // aarch64 or x86_64
Modify guest boot process:
const vm = await VM.create({
sandbox: {
imagePath: "./custom-assets",
// Custom init scripts in image
}
});
Intercept file operations:
const vm = await VM.create({
vfs: {
mounts: { "/data": new MemoryProvider() },
hooks: {
onRead: (path, data) => {
console.log("Reading:", path);
return data;
},
onWrite: (path, data) => {
console.log("Writing:", path);
return data;
}
}
}
});
Enable SSH to running VM:
const vm = await VM.create();
const sshInfo = await vm.ssh();
console.log("SSH:", sshInfo.port);
// Connect: ssh -p <port> root@localhost
Enable debug output:
const vm = await VM.create({
sandbox: {
debug: ["*"], // All components
// Or specific: ["sandbox", "network", "vfs"]
debugLog: (component, message) => {
console.log(`[${component}]`, message);
}
}
});
const pythonCode = `
import json
import sys
data = {"status": "ok", "version": sys.version}
print(json.dumps(data))
`;
const result = await vm.exec(["python3", "-c", pythonCode]);
const output = JSON.parse(result.stdout);
console.log(output);
const shell = vm.shell({ attach: false });
shell.write('cd /tmp\n');
shell.write('npm init -y\n');
shell.write('npm install lodash\n');
shell.write('node -e "console.log(require(\\"lodash\\").VERSION)"\n');
shell.write('exit\n');
for await (const chunk of shell) {
process.stdout.write(chunk);
}
// Build phase (custom image)
// $ gondolin build --config config.json --output ./image
// Run phase
const vm = await VM.create({
sandbox: { imagePath: "./image" }
});
await vm.exec("npm test");
await vm.close();
Reuse VMs when possible
const vm = await VM.create();
await vm.exec("command1");
await vm.exec("command2"); // Reuse same VM
await vm.close();
Cache guest images
Use custom images for speed
Stream large outputs
const proc = vm.exec(cmd, { stdout: "pipe" });
for await (const chunk of proc) {
// Process incrementally
}
Enable hardware acceleration
// Create VM
const vm = await VM.create();
const vm = await VM.create({ httpHooks, env, vfs, sandbox });
// Execute
const result = await vm.exec("command");
const result = await vm.exec(["cmd", "arg1"], { options });
// Interactive shell
const shell = vm.shell({ attach: false });
shell.write('command\n');
for await (const chunk of shell) { }
// Streaming
const proc = vm.exec(cmd, { stdout: "pipe" });
for await (const chunk of proc) { }
// Close
await vm.close();
// Network policy
const { httpHooks, env } = createHttpHooks({
allowedHosts: ["host.com"],
secrets: { KEY: { hosts: ["host.com"], value: "secret" } }
});
// Network policy with internal IP access
const { httpHooks, env } = createHttpHooks({
allowedHosts: [".*"],
blockInternalRanges: false, // Allow 10.x.x.x, 192.168.x.x, etc.
secrets: { KEY: { hosts: ["internal-api.company.com"], value: "secret" } }
});
// VFS mount
import { RealFSProvider } from "@earendil-works/gondolin";
const vm = await VM.create({
vfs: {
mounts: {
"/root/.pi": new RealFSProvider("/root/.pi")
}
}
});
// Build image
$ gondolin build --config config.json --output ./assets
| Issue | Problem | Solution |
|-------|---------|----------|
| 403 on internal APIs | Default blocks private IPs | Use blockInternalRanges: false (SDK only) |
| $VAR not expanded | Array form exec doesn't expand | Use string form: vm.exec('cmd $VAR') |
| Secret not injected | Custom onRequest hook | Remove custom hook or use SDK properly |
| Command not found | PATH not set in shell | Set env.PATH in shell options |
| CLI can't access internal | CLI limitation | Use SDK with custom script |
0 - Success1-255 - Command failedresult.exitCode~/.cache/gondolin/$(npm root -g)/@earendil-works/gondolinGondolin provides secure, isolated Linux micro-VMs for:
Key patterns:
vm.exec()vm.shell({ attach: false })rootOverlay: truecreateHttpHooks()Always remember: await vm.close() to free resources!
development
DuckDB patterns for JSON/JSONL analysis, array unnesting, and common gotchas. Use when querying JSON files, nested data, or encountering "UNNEST not supported here" errors.
development
Mealie recipe manager API: recipes, shopping lists, meal plans. Requires MEALIE_BASE_URL and MEALIE_API_KEY.
business
TimeWarrior time tracking: start/stop intervals, query durations by tag or issue, compute totals for issue tracker time reporting
development
Bookmark manager for saving, searching, and annotating web content. Use when: (1) saving a webpage for later reference, (2) searching previously saved bookmarks, (3) adding highlights/annotations to saved content, (4) user asks to 'bookmark this' or 'save this article'. Requires READECK_BASE_URL and READECK_API_KEY environment variables.