Getting started with Cruft
A ten-minute walkthrough from installing Cruft to a running web server with an embedded SQLite database, a sandboxed plugin compartment, and a supply-chain-sealed dependency tree. Every command and its output comes from a real session against cruft 0.0.10.
This walkthrough takes you from nothing to a running web server with a database, a sandboxed plugin, and a supply-chain-sealed dependency tree, about ten minutes. Every command and its output was captured from a real session against cruft 0.0.10.
1. Get Cruft
Cruft ships as a single binary. Two paths today:
- npm package:
crufton the npm registry wraps prebuilt platform binaries (linux/darwin, x64/arm64, win32-x64):npm install -g cruft. - From source: clone the repository and
cargo build --release --bin cruft -p cruft; the binary lands intarget/release/cruft.
Check it:
$ cruft --version
cruft 0.0.10
2. Hello, world
$ cat hello.js
const who = process.argv[2] ?? "world";
console.log(`hello, ${who}`);
$ cruft hello.js cruft
hello, cruft
cruft <file> runs a file (cruft run <file> is the explicit form); flags after the file go to your program's process.argv, like node. No project setup, no config file.
3. TypeScript, no build step
Rename nothing, configure nothing: a .ts extension is enough:
$ cat tasks.ts
interface Task { title: string; done: boolean }
const tasks: Task[] = [{ title: "read the docs", done: true }];
console.log(tasks.filter(t => t.done).length, "done");
$ cruft tasks.ts
1 done
Types are erased Node---strip-types-style (no type checking, that stays in your editor), and because erased spans become spaces, runtime errors point at your actual .ts lines with no source maps. Details: TypeScript support.
Try the REPL too: just run cruft with no arguments: persistent state, _ for the last result, tab completion, multi-line continuation.
4. Install and use a package
$ cat package.json
{"name":"tut","version":"1.0.0","dependencies":{"is-odd":"^3.0.0"}}
$ cruft install
+ is-number@6.0.0
+ [email protected]
cruft install: 2 installed, 0 skipped
Cruft's built-in package manager resolves against the npm registry exactly as npm does, hard-links from a global content-addressed store (pnpm-style), and writes cruft-lock.json; commit that file; re-installs from it are exact and network-free.
$ cat odd.cjs
const isOdd = require("is-odd");
console.log("3 is odd:", isOdd(3));
$ cruft odd.cjs
3 is odd: true
CommonJS and ESM both work, mixed, like modern node. (Current boundaries: devDependencies and lifecycle scripts aren't processed yet; see the package manager.)
5. A web server
The native idiom is a fetch-shaped handler:
$ cat server.mjs
import { serve } from "cruft:serve";
serve({
port: 8080,
handler: (req) => {
const url = new URL(req.url);
if (url.pathname === "/") return "hello from cruft";
return new Response(JSON.stringify({ path: url.pathname }),
{ headers: { "content-type": "application/json" } });
},
});
console.log("serving on http://127.0.0.1:8080");
$ cruft server.mjs &
serving on http://127.0.0.1:8080
$ curl -s http://127.0.0.1:8080/
hello from cruft
$ curl -s http://127.0.0.1:8080/api/x
{"path":"/api/x"}
Return a string, a { status, headers, body } object, or a Response; handlers may be async. node:http's (req, res) style works too if you're porting. Details: cruft:serve.
6. A database, embedded
No install, no daemon: SQLite is part of the runtime (an independent engine that writes the real sqlite3 file format):
$ cruft -e 'const { Database } = require("bun:sqlite");
const db = new Database("app.db");
db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
db.prepare("INSERT INTO users (name) VALUES (?)").run("ada");
console.log(db.query("SELECT * FROM users").all());'
[ { id: 1, name: 'ada' } ]
The app.db it just wrote opens in any SQLite tool. There's also an embedded PostgreSQL engine and a boundary-validating ORM over both, see the SQL stack and the ORM.
7. Run untrusted code in a compartment
A Compartment is a fresh, empty realm: code inside can reach only what you hand it.
$ cruft -e 'const c = new Compartment({
globals: { shout: (s) => s.toUpperCase() },
timeout_ms: 100,
});
console.log(c.evaluate("shout(\"sandboxed\")"));
console.log(c.evaluate("typeof process"));'
SANDBOXED
undefined
No process, no fetch, no filesystem: they are simply absent. Nothing is blocking them; they were never placed in the realm. The timeout_ms budget stops runaway loops and cannot be caught from inside. Use it for plugins, tenant code, or model-generated code. Details: Compartments and capabilities.
8. See what your dependencies actually do
$ cruft --audit hello.js
hello, world
# cruft audit log — 1 records
# format: <caller>\t<capability>\t<operation>\t<unix_micros>
file:///…/hello.js stdio write(stdout) 1784994699155160
Every I/O operation, attributed to the module that made it, even console.log is a recorded stdio write. Then flip on enforcement for your dependency tree:
$ cruft --sealed-deps app.mjs
Your code runs normally; everything under node_modules is denied I/O; a compromised transitive dependency can't read your files or phone home, because the authority was never in its hands. (Full --sealed mode exists too, gating network, filesystem, and directory enumeration and supporting a stdio grant; --sealed-deps is the simplest starting point.)
Where to next
- Concepts: what Cruft is and the mental model, seven short pages.
- Coming from Node? The CLI reference covers the exit-code and flag differences; the module reference states exactly how deep each
node:*module goes. - The distinctive stuff: Imogen workers (thousands of isolated workers per process), CruftScript (the sound language), the agent compartment (audited LLM-agent execution), and the Node front door (supervise existing node/npm workflows before migrating them).