Run code you didn't write.
Grant it nothing you didn't mean to.
A Compartment is isolation as a language primitive: a fresh realm, capabilities only by explicit grant, and a timeout it cannot escape. Run a plugin, a dependency's callback, or a model's output in-process, without handing it the machine.
// a plugin we did not write, run with exactly two capabilities const plugin = ` const total = cart.reduce((n, i) => n + i.price, 0); discount(total > 100 ? 0.1 : 0); `; const c = new Compartment({ timeout_ms: 50, // a budget it can't escape globals: { // the only things it can reach cart: [{ price: 60 }, { price: 55 }], discount: (rate) => applyDiscount(rate), }, }); c.evaluate(plugin); c.evaluate("typeof fetch"); // the plugin never sees the network apply discount: 0.1 typeof fetch → undefined
No ambient authority
The realm starts empty
Ordinary JavaScript runs with everything the process can do lying around on the global object. A Compartment starts with none of it. require, fetch, process: not there unless you hand them over. Authority is the reference, so if the code does not hold it, it cannot use it.
const c = new Compartment(); c.evaluate("typeof require"); // undefined c.evaluate("typeof fetch"); // undefined c.evaluate("typeof process"); // undefined // grant exactly one capability, nothing more const box = new Compartment({ globals: { readKey: () => "sk-live-..." }, }); box.evaluate("readKey()"); // "sk-live-..."
The one trusted root
Everything grows from the host realm
Your program runs in the host realm: the single privileged realm that actually holds authority, the filesystem, the network, the process, and the only thing that can mint a Compartment or grant a capability into one. A Compartment is a child realm. It starts empty, and it can only ever hold what the host hands down.
This inverts the usual model. Ordinarily every module in a process shares one ambient global carrying all authority, and isolation is a convention you hope holds. Here authority flows one way only: down, by explicit reference, from host to child. A grant is usually a host function closing over real authority, so the tenant calls through the host rather than holding the thing itself. The host supervises; the compartment only executes.
// your program runs in the host realm: // the one realm that holds real authority import { readFile } from "node:fs/promises"; // mint a child; grant a narrow, wrapped // slice of the host's own authority const tenant = new Compartment({ globals: { readConfig: () => readFile("/cfg.json"), }, }); tenant.evaluate("readConfig()"); // ok tenant.evaluate("typeof readFile"); // undefined
const c = new Compartment({ timeout_ms: 50 }); c.evaluate(` try { while (true) {} // a hostile loop } catch (e) { "escaped?" // tries to swallow it } `); cruft: evaluation error: Interrupted
A deadline below the language
A timeout it cannot escape
The time budget is enforced beneath JavaScript. The interrupt is not an exception, so there is no catch to wrap it in and no event to intercept. A tenant cannot defeat the budget by trapping it, because from inside the language there is nothing to trap. Runaway and hostile loops alike are stopped.
The boundary holds both ways
Nothing leaks across it
A compartment has its own copies of the intrinsics. Code inside cannot reach out to poison the host, and one compartment cannot see into another. Isolation is real, not cooperative.
No prototype pollution
const c = new Compartment(); c.evaluate("Array.prototype.hacked = 1"); [].hacked; // host: undefined
No cross-talk
const a = new Compartment(); const b = new Compartment(); a.evaluate("globalThis.secret = 42"); b.evaluate("typeof secret"); // undefined
Whole-process capability control
Seal the process from the outside
Where a Compartment isolates a region of code from inside, the sandbox flags constrain an entire cruft process, without changing a line of source. Cruft treats I/O as a capability: --audit records every capability a program uses, and --sealed denies them unless the program declares what it legitimately needs.
# watch every I/O capability a program touches $ cruft --audit app.mjs file:///app.mjs stdio write(stdout) # deny all I/O; ungranted operations throw $ cruft --sealed app.mjs TypeError: no stdio capability granted
// package.json: grant exactly what's needed { "caps": { "stdio": { "stdout": true } } }
# your code runs normally; # node_modules is denied I/O $ cruft --sealed-deps app.mjs # a transitive dependency cannot reach # the filesystem or network behind your back
The supply-chain middle ground
Box just the dependencies
--sealed-deps is the common case: your first-party code keeps normal access, but everything under node_modules is denied I/O. A compromised or curious transitive dependency cannot quietly touch the disk or open a socket. --allow-net-loopback re-grants just local listen authority when you need it.
The installer
Dependencies arrive without running code
cruft install runs no lifecycle scripts. Not preinstall, install, postinstall, or prepare, from the root or from any dependency. There is no process spawn anywhere in the installer, which closes the largest npm supply-chain vector by leaving it out. A skipped script is reported, so nothing fails invisibly.
Every tarball is verified before it is extracted: its sha512 subresource-integrity hash is checked byte for byte against the lockfile, a mismatch aborts the install and nothing lands on disk, and a package that ships no integrity at all is refused rather than trusted. The tarball URL must be HTTPS and the registry's own origin, so a swapped mirror is rejected before a byte is fetched.
The lockfile is origin-pinned, not signature-authenticated: the installer still trusts the registry it fetches from, so it is not yet safe against a fully compromised registry.
// cruft-lock.json pins every dependency by content "[email protected]": { "integrity": "sha512-...", // checked before extraction "tarball": "https://registry.npmjs.org/left-pad/..." } $ cruft install verifying 214 packages against the lockfile no lifecycle scripts run integrity mismatch: [email protected] install aborted
# advisory: report supply-chain risks, then run npm $ cruft trust install -- npm install lifecycle-script esbuild postinstall native-addon node-sass binding.gyp known-malicious [email protected] MAL-2024-1234 known-vulnerability [email protected] GHSA-... # enforce: stop known-bad before npm launches $ cruft trust install --enforce -- npm install blocked: [email protected] (known-malicious) exit 77
Before an npm install runs
Known-bad packages, stopped at the door
cruft trust install is a preflight over what an npm install is about to touch. It checks each package against the OSV advisory database at its one pinned endpoint, api.osv.dev, and classifies the risks it finds: lifecycle scripts, native addons, known vulnerabilities, and known-malicious advisories (the OSV MAL- class). By default it reports them and lets the install proceed.
With --enforce it stops a known-bad package before npm ever launches, exiting 77. Enforcement blocks lifecycle scripts, native addons, and known-malicious advisories; an ordinary vulnerability is reported but not blocked, and if the OSV lookup cannot reach the network it reports no advisories rather than failing the install. The same lookup is available inside an agent as the osv.query tool.
The Node front door
Run real Node under an OS sandbox
cruft wrap runs your existing node, npm, and npx commands as a supervised child. The child is the real Node binary, so nothing about how it behaves changes; what Cruft adds is a boundary around it. --sandbox=macos-strict makes that boundary an OS-level one: on macOS the child runs under sandbox-exec with the network denied, filesystem writes denied, and a scrubbed environment. A write or a socket connect fails with EPERM.
This is the front door's one enforced control, and it is exact about its edges. It is macOS-only today, it denies general process exec but re-allows the child to launch its own binary, so it is not complete child-process denial, and the profile names (ci, paranoid, locked) are reporting tone, not stronger enforcement. cruft policy and cruft doctor print each control's real level.
# run your existing node/npm/npx, supervised $ cruft wrap -- node app.js # put a real OS sandbox around the child (macOS) $ cruft wrap --sandbox=macos-strict -- node build.js sandbox-profile=macos-strict controls=filesystem-write,network,environment,external-process-exec // inside the child: no writes, no network, scrubbed env fs.writeFileSync("/tmp/x", "1") // EPERM
Isolation you can afford
Cheap enough to use by default
Because a worker compartment is a realm over one shared, garbage-collected heap rather than a whole VM per worker, isolating at high cardinality stops being a memory emergency. The same production worker loads, at 2048 workers delivering 2000 events each:
| Workload (2048 workers) | Cruft | Node worker_threads |
|---|---|---|
| webhook transform | 274 MiB / 1.0 s | 6,247 MiB / 27.5 s |
| API policy | 272 MiB / 0.9 s | 8,265 MiB / 32.2 s |
Peak resident memory and wall time, same events delivered, both exiting cleanly. A per-plugin, per-request, or per-agent compartment is an ordinary thing to spin up, not a cost you have to ration.
The whole model
Compartments, capabilities, and the sealed process are one idea at three sizes: name the boundary, check what crosses it, refuse the unsound case loudly. Read how it fits the rest of the stack.