Deno compatibility
Cruft exposes a `Deno` global that reshapes its Node layer into the Deno runtime API. Most of the namespace works, including the file system, subprocesses, environment, and process info. The permission sandbox is not enforced, and Deno's `npm:`, `jsr:`, and `https:` imports are not supported.
Security: the permission sandbox is not enforced.Deno.permissionsalways grants and gates nothing, so Deno's--allow-*model provides no isolation on Cruft. Do not use it as a security boundary. For real isolation, run untrusted code in a Compartment, which enforces capabilities at the realm boundary. See Permissions are not enforced below.
Cruft ships a Deno global so code written against the Deno runtime API can run on Cruft's engine. It is on by default and can be turned off with the environment variable CRUFT_DENO_COMPAT=0, after which typeof Deno is "undefined".
The namespace is built on top of Cruft's Node compatibility layer: the same host operations that back node:fs, node:os, and node:http are reshaped into the Deno API. That is the key to reading this page. Anything Deno does that Node also does, the file system, subprocesses, environment, and process information, works well. Anything with no Node backing is either absent or a stub, and the two Deno concepts with no equivalent at all, the permission sandbox and npm:/jsr:/https: imports, do not exist here.
The global
Deno.version and Deno.build report identity:
Deno.version; // { deno: "2.8.0", v8: "14.9.0-cruft", typescript: "6.0.3" }
Deno.build.os; // "darwin"
Deno.build.arch; // "arm64"
Deno.pid, Deno.ppid, Deno.execPath(), Deno.hostname(), Deno.args, and Deno.noColor all report real values.
The file system
The full synchronous and asynchronous file API is implemented:
await Deno.writeTextFile("/tmp/note.txt", "hello");
await Deno.readTextFile("/tmp/note.txt"); // "hello"
Deno.statSync("/tmp/note.txt").size; // 5
readFile and readFileSync return a plain Uint8Array (not a Node Buffer), matching Deno:
Deno.readFileSync("/tmp/note.txt").constructor.name; // "Uint8Array"
Working members: readTextFile, readFile, writeTextFile, writeFile, readDir, stat, lstat, mkdir, remove, rename, realPath, readLink, copyFile, truncate, chmod, chown, link, symlink, utime, makeTempDir, open, and create, each with its *Sync variant. Deno.open returns a Deno.FsFile handle with .write() and .close(), and Deno.SeekMode is { Start: 0, Current: 1, End: 2 }.
Environment
Deno.env is fully working:
Deno.env.set("FOO", "bar");
Deno.env.get("FOO"); // "bar"
Deno.env.has("FOO"); // true
Deno.env.delete("FOO");
Deno.env.toObject().PATH; // the current PATH
Subprocesses
Deno.Command spawns real processes:
const { code, success, stdout } = await new Deno.Command("echo", {
args: ["hi", "there"],
}).output();
new TextDecoder().decode(stdout); // "hi there\n"
output, outputSync, and spawn are all present. Deno.exit(code) exits the process, and Deno.kill(pid, signal) signals one (Deno.kill of a missing pid raises ESRCH).
Process and system info
Deno.cwd(), Deno.chdir(), Deno.loadavg(), Deno.networkInterfaces(), Deno.memoryUsage(), Deno.consoleSize(), Deno.uid(), Deno.gid(), Deno.umask(), and Deno.osRelease() all report real values. Deno.inspect(value) formats a value in Deno's style, and Deno.errors carries the standard 23 error classes (NotFound, AlreadyExists, PermissionDenied, and the rest), with Node .code values mapped onto them.
Standard streams
Deno.stdout, Deno.stderr, and Deno.stdin accept byte writes:
Deno.stdout.write(new TextEncoder().encode("direct to stdout\n"));
The Deno resource-id field (Deno.stdout.rid) is undefined; Cruft does not model Deno's resource-id table.
Deno.serve
Deno.serve binds a port and serves requests through a handler:
Deno.serve({ port: 8000, onListen: (a) => console.log("on", a.port) }, (req) =>
new Response("hello from cruft " + new URL(req.url).pathname),
);
onListen fires with the bound address, and an external client receives the handler's Response. Two caveats: the return value is a plain object, not a Deno HttpServer instance, and a fetch() to the server from its own process does not complete (the event loop does not service both sides of a same-process request); serve to an external client instead.
Permissions are not enforced
Deno.permissions exists, but it is a compatibility shim, not a sandbox. Every query returns granted, revoke reports a denied status but changes nothing, and no file, network, environment, or subprocess operation is ever gated on a permission.
(await Deno.permissions.query({ name: "read", path: "/etc" })).state; // "granted"
await Deno.permissions.revoke({ name: "read" }); // reports "denied"
Deno.readTextFileSync("/etc/hosts"); // still succeeds
Do not rely on Deno.permissions or the --allow-* model for isolation. Cruft's actual isolation primitive is the Compartment, which enforces capabilities at the realm boundary.
Limitations
- No permission sandbox.
Deno.permissionsalways grants and enforces nothing (above). Use Compartments for real isolation. - No
npm:,jsr:, orhttps:imports. These specifiers are not resolved; they fall through to Node-stylenode_moduleslookup and fail. Use Node-style bare, relative, andnode:specifiers instead. - FFI throws.
Deno.dlopen,Deno.UnsafeCallback,Deno.UnsafeFnPointer, andDeno.UnsafePointerViewthrow "not supported on cruft". - No
Deno.test,Deno.bench, orDeno.metrics. The test and bench runners and the metrics table are absent. Usecruft --test. - Only
Deno.serveon the network side. The lower-levelDeno.listen,Deno.connect,Deno.serveHttp,Deno.upgradeWebSocket, andDeno.listenTlsare not present. - Some fields are stubbed.
Deno.osUptime()returns a constant0,Deno.mainModuleis not populated, and the resource-id fields on the standard streams areundefined.