What is Cruft?

Cruft is a JavaScript runtime with its own engine, built so you can run npm dependencies without giving each one your files, network, and environment. It runs untrusted code in Compartments that start with only the abilities you grant, and can withhold host access from your whole dependency tree.

Every JavaScript project runs code it didn't write. A fresh create-vite app pulls in on the order of a hundred packages before the first line of application code. The graph is too deep to read and it changes on every npm install.

Nothing in Node stops any one of those packages from reading your .env, opening a socket to an arbitrary address, or shelling out in a postinstall script. A dependency runs with exactly your authority: your files, your network, your environment variables, your process. left-pad, event-stream, the xz backdoor: each was this same fact, arriving on a different day. The trade has been accepted because the alternative was to leave the ecosystem, and the ecosystem is the point.

Cruft refuses that trade. Everything below is a consequence of taking that one problem seriously inside a JavaScript runtime.

The tools Node already gives you

The usual way to contain something in Node is to reach for a boundary. Node offers two.

A process. child_process, or a container. This is real isolation: a separate address space, its own file descriptors. It is also coarse. You serialize across it, you pay OS scheduling for it, and it is all-or-nothing. Saying "this code may read this directory and make these calls and nothing else" means assembling seccomp, namespaces, and a container spec around it. Isolation you build, not isolation you have.

A worker. worker_threads. Lighter than a process, but a Worker is a separate V8 isolate: a separate heap. Anything mutable you share, you postMessage and it gets structured-cloned or handed off. The boundary is real, but it is a wall you copy things over, and it exists to protect the heap, not to constrain what the worker's code is allowed to do. A worker still has your file system.

So Node's tools isolate memory or the whole process. Neither isolates authority, the thing at issue with that dependency.

Compartments

Describe the boundary directly:

Run this code in its own global scope, with only the abilities explicitly handed to it, and if it runs too long, stop it, without spawning a process and without copying the heap.

That is a value, not a subprocess. In Cruft it is written:

const c = new Compartment({
  globals: { fetch },          // it gets fetch, and nothing else
  timeout_ms: 50,              // and 50ms, which it cannot escape
});
c.evaluate(untrustedSource);   // no fs, no env, no process (none were granted)

Compartments use no new isolation technology. They draw the boundary Node can't: authority, scoped to a value, in-process. A Compartment is the same shape as a Node Worker, code running somewhere else, with the axis rotated: a worker isolates the heap and shares your authority; a Compartment shares the heap and isolates authority.

The same question applies to the whole program. A dependency three levels down the graph runs at import time without ever being called explicitly. So: what if the runtime withheld host authority from the entire dependency tree unless a package declared it needed it? That is what cruft --sealed-deps does, and what --audit records first. The industry calls the goal "supply-chain security." Cruft's version is the runtime declining to hand the package the authority in the first place. A scanner reports after the fact which package was malicious; Cruft withholds the authority before that matters. Attacks that can't be detected become attacks that can't reach anything.

Why this needs a different engine

Node is a shell around V8. Deno wraps V8 too; Bun wraps JavaScriptCore. In all of them the engine (the object model, the heap, the garbage collector) is a black box the runtime drives from the outside. Isolation, in that arrangement, can only be drawn where the runtime can reach: around the engine. That is why the only real boundaries are the isolate and the process. Authority per value, sharing the heap, lives inside the box, and the box belongs to the engine vendor.

Cruft's engine is not V8. The parser and bytecode compiler, the interpreter, the garbage collector, and the JIT (LeJIT) are all Cruft's own implementation in Rust, following the ECMAScript and WHATWG specifications rather than reusing any existing engine. This is the enabling condition for everything above. A Compartment can be a first-class object because the object model is Cruft's to define. Capabilities can be withheld per-module because the module loader and the heap are two ends of one design. The worker model, Imogen, lets workers share an immutable heap through handles instead of copying, because the collector was built knowing workers exist. Each of these is a property of owning the box.

Owning the engine carries an obligation to match the spec. Cruft runs the full official test262 conformance corpus, tracking a small set of triaged, accepted compatibility exceptions rather than claiming a clean 100% pass; the conformance gate holds the line against pass-to-fail regressions. Beyond the spec suite, it continuously diffs its output against Node and V8 on real npm workloads, with a secondary, informational comparison against JavaScriptCore, and triages every divergence as a bug rather than accepting it silently.

The npm ecosystem still runs

None of this would matter if the price were rewriting your stack. Cruft's compatibility target is the Node ecosystem, on purpose: it resolves node:* and bare specifiers (fs, path, http, …), runs both CommonJS and ES modules, honors package.json resolution, and executes .ts/.mts/.cts by stripping types the way Node's own --strip-types does. No separate build step. There is a package manager in the same binary: cruft install, a lockfile, a content-addressed store.

The bar is concrete: real packages, and real workloads like a vite build, should run unmodified. Where they don't yet, that gap is named in these docs.

Limitations

  • The engine is Cruft's own, not a V8 fork. A different engine has different bugs and a different performance curve, and the JIT is younger than V8's by decades.
  • Some surfaces are partial. A few exist so that feature-detection succeeds but do not implement the whole API. Each one is marked in these docs rather than implied complete.
  • Node compatibility is not a drop-in guarantee. It is broad, but where Node's behavior diverges from the ECMAScript spec, Cruft treats the spec as canonical and Node's behavior as an explicitly modeled compatibility exception. Usually invisible, occasionally the reason something behaves differently than Node.