# quickjs puts untrusted JavaScript in a WebAssembly sandbox from TypeScript

> The @sebastianwessel/quickjs package runs JavaScript and TypeScript inside a QuickJS engine compiled to WebAssembly, with a virtual file system, an injectable environment, a fetch client and a bundled test runner. It is aimed at TypeScript applications that need to execute code they did not write, such as user scripts or plugin code.

**sebastianwessel/quickjs** — A typescript package to execute JavaScript and TypeScript code in a webassembly quickjs sandbox

- Repository: https://github.com/sebastianwessel/quickjs
- Website: https://sebastianwessel.github.io/quickjs/
- Stars: 952 · Forks: 37
- Language: TypeScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/sebastianwessel-quickjs

## The problem is running code you did not write

Every application that lets users supply behaviour eventually faces the same question: where does that code run. eval in the host process gives it everything. A separate container gives it isolation but costs a process boundary. A WebAssembly sandbox gives it a small, bounded world with a real language runtime inside, and that is the trade the sebastianwessel/quickjs package makes.

The package description is a typescript package to execute JavaScript and TypeScript code in a webassembly quickjs sandbox. The README frames the use case as isolating and running untrusted code securely by leveraging the lightweight and fast QuickJS engine compiled to WebAssembly.

Two things follow from the engine choice. The interpreter runs inside your process, in linear memory owned by the WebAssembly module, so a runaway loop in the sandboxed code is contained by the runtime rather than by a supervisor. And because the engine is a separate implementation rather than the JavaScript engine your application already runs, the sandboxed code does not automatically inherit the host's globals, which is exactly the property you want when the code is untrusted.

The project is MIT licensed, published on npm as @sebastianwessel/quickjs, and the last push was on 2026-06-08 with version 3.1.0 tagged the same day. Earlier tags include v3.0.1 on 2026-02-13 and v3.0.0 on 2025-07-26.

## loadQuickJs is the expensive half, runSandboxed is the cheap one

The API is split into two steps, and the split is the main thing to understand before writing any code against it.

The first step loads the QuickJS WebAssembly variant and initialises it. The README is explicit that this is a resource intensive job and should be done only once if possible. The second step runs code inside the sandbox and returns a result.

```typescript
import variant from "@jitl/quickjs-ng-wasmfile-release-sync";
import { type SandboxOptions, loadQuickJs } from "@sebastianwessel/quickjs";

const { runSandboxed } = await loadQuickJs(variant);

const options: SandboxOptions = {
  allowFetch: true, // inject fetch and allow the code to fetch data
  allowFs: true, // mount a virtual file system and provide node:fs module
  env: {
    MY_ENV_VAR: "env var value",
  },
};
```

The call itself passes a callback that receives an object with an evalCode function, plus the options.

```typescript
const result = await runSandboxed(
  async ({ evalCode }) => evalCode(code),
  options,
);
```

What comes back is a result object rather than a thrown error, and the README shows its shape as { ok: true, data: '<!doctype html>...' }. That shape is what makes the package usable in a server, since a sandbox failure becomes a value to inspect instead of an exception that might have crossed a boundary. The variant import is the WebAssembly build of the engine, and the package also depends on quickjs-emscripten and quickjs-emscripten-sync, the latter providing a synchronous build for callers that cannot await.

The project documentation lives at sebastianwessel.github.io/quickjs, there is an online playground, and the example/ directory in the repository holds runnable versions of each usage pattern.

## The sandbox surface is opt-in: file system, env and fetch

The defaults matter for a sandbox, so it is worth being explicit about what you get unless you ask for it. The options object shown above contains three capabilities, and each one is a hole you deliberately open.

allowFetch injects a fetch client and allows the code to fetch data. Without it, the sandboxed code has no outbound HTTP of its own. That is a network egress decision, and it belongs to whoever owns the threat model rather than to a default value in a library.

allowFs mounts a virtual file system and provides the node:fs module. The filesystem is virtual rather than the real one, and the credit list names memfs as the basis for it. The example in the README shows the difference from the host system: inside the sandbox, joining src and dist logs src/dist, and the environment variable is read from the injected env object rather than from the real process environment.

env is how you pass values in. The sandboxed code reads env.MY_ENV_VAR, so the map you supply becomes the visible environment, which is a clean way to avoid handing over the host's process environment by accident.

Beyond these, custom Node modules are mountable, so you can expose exactly the module surface a plugin is allowed to use rather than a general purpose API. The example directory shows the shape of that, with separate folders for a basic run, async code, a browser case, a custom module, a function call, a server, a timer, TypeScript input, user code, an AI example and running tests.

## TypeScript input is handled by the package, not by the engine

QuickJS is a JavaScript engine and knows nothing about TypeScript syntax. The package advertises the ability to execute JavaScript and TypeScript code, and the example directory has a dedicated typescript entry alongside the general user-code example, so the type stripping is part of the package's job rather than something the caller does before calling in.

That is a meaningful convenience for a plugin system, where the person writing the plugin may well write TypeScript and expect it to run. The alternative for a caller is to transpile on the host, which means a separate toolchain, a watch step for development, and a decision about which target to compile to.

The test runner feature follows the same logic. The package includes a test runner and a chai based expect, so a plugin author can write assertions and run them inside the sandbox, which is what the run-tests example demonstrates. The credit list names Chai as the source of the assertion style, and the reference implementation is the memfs backed virtual file system plus the Chai expectations.

Both of these features point at the same use case. The package is less a general JavaScript interpreter than a place to run small user supplied programs with their own tests and their own files, close to how a build tool runs a plugin.

## Bun, tshy and two module dialects shape the package

The build tooling in package.json explains a lot about how the package is meant to be consumed. The scripts are run with Bun, the build is bun run build:vendor followed by tshy and then a copy step for the module builds directory, and the test script runs the vendor build, a tsc --noEmit type check and then bun test.

The tshy configuration excludes the test files, the vendor directory, the example directory and the docs from the published build, and it declares two dialects, esm and commonjs. So the package ships in both module systems, which matters for a library that is often pulled into a mixed codebase, and it publishes only the dist directory.

The engines field requires node >=18.0.0, and the package is published with public access. There is also a jsr.json at the root and a postpublish script that runs npx jsr publish, so the same source is distributed to a second registry.

The performance story in the README leans on the engine being lightweight, and the repository carries the tools to back that up. loadtest.ts sits at the root with autocannon named among the tools, poolifier-web-worker is listed, and the example server case uses Hono. Those are the parts of the repository aimed at measuring throughput rather than demonstrating the API, and they are the files to look at if the claim about speed matters to your decision.

## Where quickjs is the wrong tool

The first boundary is the size of the standard library. A QuickJS sandbox gives a JavaScript core plus the handful of Node modules the package chooses to expose. There is no DOM, so a script that manipulates a page cannot run there, and there is no access to arbitrary native modules. Code that needs the file system of the real machine, a database driver, or an image library is out of scope.

The second boundary is the WebAssembly start-up cost. The README calls the load and init step resource intensive, which means a workload that creates a fresh runtime per request will pay that cost per request. The design pushes you towards one loaded runtime and many runSandboxed calls, but that is an architectural constraint on your application, not just an optimisation.

The third is the type system boundary. TypeScript is executed, but nothing in the sandbox checks types at run time, so a plugin written in TypeScript gets no protection from its own annotations. If type safety across the host and plugin boundary is a requirement, the check has to happen on the host side before the code is handed over.

There is also a comparison worth making explicitly. A worker thread or a child process gives you isolation with a full runtime, at the cost of sharing the host environment and its memory model. The WebAssembly sandbox gives you a smaller world with a fast startup and a clean capability model, and accepts that some code simply cannot run in it. For user supplied snippets and plugin code the smaller world is the point, and for anything that needs the platform it is the wrong tool.

## Conclusion

Adopt quickjs when your TypeScript application has to run code it did not author, such as customer supplied automation or a plugin system, because the QuickJS engine in WebAssembly gives isolation and the package supplies the plumbing most such systems end up writing, namely a virtual file system, a fetch client, environment injection and a test runner. Do not adopt it if the code needs a DOM, real Node APIs or native modules, because the sandbox is deliberately a small standard library, and check that a fetch client is acceptable in your threat model before enabling allowFetch. Verify first that you call loadQuickJs once per process, since the README calls that step resource intensive, and confirm the Node version in your build meets the engines requirement of node >=18.0.0.

## FAQ

### How do I run JavaScript in the quickjs sandbox?

Load the WebAssembly variant with loadQuickJs(variant) once, then call runSandboxed with a callback that receives evalCode and your SandboxOptions. The call resolves to a result object with an ok flag and a data field rather than throwing.

### Can sandboxed code read files or make network requests in quickjs?

Only if you enable them. allowFs mounts a virtual file system and provides the node:fs module, and allowFetch injects a fetch client and permits http(s) calls. Values passed through the env option are visible to the sandboxed code as env.VARNAME.

### Does the quickjs package support TypeScript code?

Yes, the package is described as executing JavaScript and TypeScript, and the repository ships a typescript example. The QuickJS engine itself only understands JavaScript, so the type handling happens in the package before the code reaches the sandbox.

### What Node version does the quickjs package require?

The package.json engines field requires node >=18.0.0. The package is published publicly on npm as @sebastianwessel/quickjs and is also published to JSR, with a postpublish script running npx jsr publish.

### Can I write tests for code that runs in the quickjs sandbox?

The package includes a test runner and a chai based expect, and the repository has a run-tests example showing it. The assertion style comes from Chai, which is credited as a dependency of the library.

## Sources

- [License: MIT](https://github.com/sebastianwessel/quickjs/blob/main/LICENSE)
- [Project website](https://sebastianwessel.github.io/quickjs/)
- [README](https://github.com/sebastianwessel/quickjs/blob/main/README.md)
- [Releases](https://github.com/sebastianwessel/quickjs/releases)
- [sebastianwessel/quickjs on GitHub](https://github.com/sebastianwessel/quickjs)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/sebastianwessel-quickjs
