Skip to main content
jsbox

A small JavaScript sandbox for Python

jsbox embeds QuickJS (via rquickjs and PyO3) in Python. Run untrusted, LLM-generated JavaScript with hard limits and nothing but the functions you hand it.

CI PyPI License

What this is

Agent frameworks increasingly need to run JavaScript the model itself wrote: a data transform, a "compute the answer" snippet, a batch of tool calls. That code is untrusted by construction. jsbox runs it in an in-process QuickJS runtime that has no filesystem, no network, no require, no timers and no Node.js APIs. The guest can reach exactly what the host bound, and it is stopped by a wall-clock timeout, a heap limit and a stack limit.

It is small (a ~0.7 MB wheel, ~150 µs to create a runtime, no Rust toolchain needed to install) and deliberately boring: an interpreter, not a JIT. If you need raw JavaScript speed, use a V8 based engine; if you need a tiny, fast-starting, hard-to-misuse sandbox, this is that.

from jsbox import Runtime

rt = Runtime()
rt.bind_function("add", lambda a, b: a + b)
print(rt.eval("add(2, 3)"))  # 5

Get started

pip install jsbox  # or: uv pip install jsbox

Python 3.10+ on Linux, macOS and Windows (one abi3 wheel per platform).

Tool calling with limits

from jsbox import JsTimeoutError, Runtime

rt = Runtime(memory_limit=16 * 1024 * 1024)  # 16 MB heap


def get_weather(city: str) -> str:
    return f"72F and sunny in {city}"


rt.bind_function("getWeather", get_weather)

# Model-generated JS calling back into your tool:
print(rt.eval("getWeather('Zurich')"))  # 72F and sunny in Zurich

# Code that might not terminate is stopped, and the runtime stays usable:
try:
    rt.eval("while (true) {}", timeout=2.0)
except JsTimeoutError:
    print("runaway script killed after 2s")
print(rt.eval("1 + 1"))  # 2

Data in, data out

Values are copied across the boundary; the guest never holds a reference to a Python object.

JavaScript Python
null, undefined None
boolean, string bool, str
integral number, BigInt int (BigInt never wraps)
other number float
Array list (a tuple goes in as an array)
plain Object dict (own enumerable keys, functions skipped)
Uint8Array, ArrayBuffer bytes (bytes/bytearray go in as Uint8Array)
rt.bind_object("config", {"debug": True, "tags": ["a", "b"], "blob": b"\x00\x01"})
print(rt.eval("config.tags.length"))  # 2
print(rt.eval("({ok: true, rows: [1, 2, 3]})"))  # {'ok': True, 'rows': [1, 2, 3]}

Anything else (functions, symbols, Date, non-str dict keys, cycles) raises instead of being silently mangled.

Syntax check

rt.check_syntax(code) (or module=True) parses without running and raises JavaScriptError (SyntaxError) if invalid.

Promises

A script that returns a promise is settled by draining microtasks, so async/await works. A promise that can never settle (there are no timers or I/O) is an error, not a hang.

print(rt.eval("(async () => { await null; return 7 })()"))  # 7

ES modules

rt.add_static_module("math", "export const answer = 42;")
rt.add_static_module("app", "import {answer} from 'math'; export const x = answer + 1;")
print(rt.eval_module("app"))  # {'x': 43}

Only modules you registered can be imported, by import or import(). There is no file or URL loading.

Async host functions

Bound functions may be async def. Under eval_async they reach JS as Promises, run on your event loop, and can be in flight concurrently (Promise.all). timeout= covers the whole call, including time spent waiting on the host.

import asyncio
from jsbox import Runtime


async def fetch(x):
    await asyncio.sleep(0.01)
    return x * 2


async def main():
    rt = Runtime()
    rt.bind_function("fetch", fetch)
    print(await rt.eval_async("Promise.all([1, 2, 3].map(x => fetch(x)))"))  # [2, 4, 6]


asyncio.run(main())

A plain eval() that calls an async function raises TypeError: use eval_async.

console and unconvertible results

rt = Runtime(on_console=lambda level, args: print(level, args), lossy=True)
rt.eval("console.log('hi', [1, 2]); console.error('oops')")  # log ['hi', [1, 2]] / error ['oops']
rt.eval("({ok: true, run() {}})")  # {'ok': True, 'run': '[function]'}

Runtime(buffer=True) installs a small Node-style Buffer (a non-enumerable Uint8Array subclass, no I/O). JSZip and pptxgenjs detect it and UTF-8-encode through it, which makes a pptxgenjs write() about 20% faster; it is off by default so guests still see a non-Node environment.

console.* is a no-op without on_console. With lossy=True, functions, symbols and cycles in a result become "[function]", "[symbol]", "[Circular]" instead of raising.

Vendored npm libraries

Browser bundles such as pptxgenjs, three, vega, d3, turf, dagre and bpmn-moddle run unmodified: the host prepends the bundle (and a few byte-plumbing shims) as trusted source, the guest calls the library, and bytes come back as bytes. The test suite runs these exact files (tests/test_onehub_compat.py).

Hard kill

timeout= cannot interrupt one long native operation. For untrusted code that must not wedge the host, run it in a child process that is killed at the deadline:

from jsbox import run_killable

run_killable("let x = 3n; for (;;) x = x * x", timeout=1.0)  # JsTimeoutError after ~2 s, process gone
run_killable("[1, 2, 3].map(x => x * 2)", timeout=5, memory_limit=32 * 1024 * 1024)  # [2, 4, 6]

It costs a process spawn (about 50-100 ms) and runs pure computation only: bound functions and modules cannot cross the process boundary. Other keyword arguments go to Runtime. For host functions, run your own worker process and kill it the same way.

Errors

Situation Python exception
JS throws, or a syntax error JavaScriptError (.name, .message, .stack)
timeout= exceeded JsTimeoutError
A bound Python function raises the original exception, re-raised unchanged
Value cannot be converted TypeError / ValueError

JS that catches a host error sees only host function failed: <ExceptionClass>. The message of your Python exception (paths, secrets, internals) never reaches the guest.

Examples

Runnable scripts live in examples/ (see its README).

Monty data crunching + three.js in jsbox

A model often writes two programs: Python for the data, JavaScript for what only the JS ecosystem does well. Monty runs the Python half, jsbox the JavaScript half, and neither can touch your files or network. Here Python computes a height field and three.js turns it into a 3D model (pip install jsbox pydantic-monty; the three.js bundle and the small _sandbox.py helper live in vendor/libs and examples/):

import asyncio
from pathlib import Path

from _sandbox import load_libs, new_runtime
from pydantic_monty import Monty

# 1. Model-written Python (runs in Monty) crunches the data: a 24x24 height field.
with Monty() as pool, pool.checkout() as session:
    heights = session.feed_run("""
import math
[[round(math.sin(x / 3) * math.cos(y / 3) * 2, 3) for x in range(24)] for y in range(24)]
""")

# 2. Model-written JavaScript (runs in jsbox) turns it into a three.js scene and a .glb file.
rt = new_runtime()
load_libs(rt, "three-0.180.0-gltf.bundle.js")
rt.bind_object("heights", heights)
glb = asyncio.run(rt.eval_async("""
(async () => {
  const n = heights.length, geo = new THREE.PlaneGeometry(10, 10, n - 1, n - 1);
  const pos = geo.attributes.position;
  heights.flat().forEach((h, i) => pos.setZ(i, h));
  geo.computeVertexNormals();
  const scene = new THREE.Scene();
  scene.add(new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color: 0x3b8f5f })));
  return await new Promise((ok, fail) => new GLTFExporter().parse(scene, ok, fail, { binary: true }));
})()
""", timeout=60))
Path("terrain.glb").write_bytes(glb)
print(f"terrain.glb: {len(glb)} bytes")

More of the same, all runnable: Vega-Lite charts, d3 force graphs, ECharts dashboards, turf maps, three.js scenes and a pptxgenjs deck. See examples/.

Security model

There is no permission system to configure. A fresh Runtime can compute and nothing else; what it can do is exactly what you bound.

  • No ambient authority. No require, process, fetch, setTimeout, file or network access.
  • Heap, stack and copy limits with safe defaults. memory_limit (64 MB), max_stack_size (1 MB, at most 4 MB: a larger limit would let guest recursion overflow the native stack and crash the process, so it is rejected), max_value_bytes (16 MB, shared by all arguments of one host call or console call, and per conversion of a result). Pass 0 to memory_limit to disable the heap limit.
  • Timeouts you cannot swallow. timeout= interrupts the interpreter loop; try/catch in the guest cannot catch it, and it also covers property getters hit while converting the result. There is no default timeout: without timeout=, while(true){} runs forever. The deadline is checked every few thousand interpreter operations, so it cannot interrupt a single long native operation, and a loop whose every iteration does heavy native work overshoots badly. Known cases: BigInt powers, splice in a loop over million-element arrays, huge normalize/replaceAll. (Catastrophic regular expressions do stop on time. The worst offenders are refused up front: array methods such as sort/reverse/join/fill on arrays longer than 2^26 and BigInt() of strings over 32768 characters throw a RangeError. These guards are best-effort, for example an array-like object with a length getter can slip past them: use run_killable when the code is untrusted.) The same applies to slow host callbacks, console callbacks and conversion of huge arguments: the timeout fires once they return.
  • Host BaseExceptions (KeyboardInterrupt, SystemExit) raised by a bound function stop the script and propagate to the caller; the guest cannot catch them.
  • One call at a time per Runtime. Calling eval from inside a host function, or overlapping eval_async calls on one Runtime, raises RuntimeError. Use one Runtime per task.
  • Host errors are not guest-readable, and a swallowed host error is never blamed on a later, unrelated JS error.
  • Isolated runtimes. Two Runtimes share nothing; a Runtime is bound to its thread.

What this is not: QuickJS is a C engine in your process. A memory-safety bug in it is a bug in your process, so for hostile multi-tenant workloads also use OS-level isolation (a container or a separate process). Bound host functions are privileged code you wrote, so keep them narrow. A timeout cannot interrupt a Python callback that is itself stuck (it fires as soon as that callback returns), nor a single long native operation: for untrusted code, run the Runtime in a process you can kill (or reject patterns up front). run_killable is the built-in hard kill for pure computation.

Limitations

  • TextEncoder/TextDecoder (UTF-8) and btoa/atob are built in and native.
  • Runtime(buffer=True) adds a small Node-style Buffer (UTF-8, latin1, base64, hex). It is off by default because it is ambient API surface; turn it on for JSZip-based libraries such as pptxgenjs, which then take their native UTF-8 path (about 20% faster on a 20-slide deck).
  • No timers (setTimeout), fetch, or Date/Map/Set conversion yet. Libraries that only use timers to yield work with a microtask shim.
  • Interpreter speed: it is not a JIT. CPU-bound JavaScript runs roughly 4-8x slower than V8 (a 20-slide pptxgenjs deck ≈ 0.45 s vs 0.1 s; fib(30) ≈ 70 ms vs 10 ms). Startup and boundary crossings are where it wins: creating a runtime ≈ 0.2 ms, a complete host call (JS to Python and back) ≈ 0.5 µs, eval('1+1') ≈ 2 µs.
  • Python ints outside i64 cannot be sent in.
  • performance.now() and Date expose a real clock and the host's timezone to the guest; strict mode turns Map/Date/RegExp results into {} (use lossy=True to get JSON-shaped values).
  • A bind_object dict key __proto__ sets the bound object's prototype (it does not pollute Object.prototype).

Development

python -m venv .venv && .venv/bin/pip install maturin pytest pytest-asyncio hypothesis
.venv/bin/maturin develop --release
.venv/bin/pytest

Metadata

Release files for jsbox 0.1.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for jsbox 0.1.3
File Size Uploaded Provenance Actions
jsbox-0.1.3.tar.gz 2.5 MB With provenance Details for jsbox-0.1.3.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for jsbox 0.1.3
File Actions
jsbox-0.1.3-cp310-abi3-win_amd64.whl CPython 3.10 abi3 Windows x86-64 Details for jsbox-0.1.3-cp310-abi3-win_amd64.whl
jsbox-0.1.3-cp310-abi3-musllinux_1_2_x86_64.whl CPython 3.10 abi3 Linux musl 1.2+ x86-64 Details for jsbox-0.1.3-cp310-abi3-musllinux_1_2_x86_64.whl
jsbox-0.1.3-cp310-abi3-musllinux_1_2_aarch64.whl CPython 3.10 abi3 Linux musl 1.2+ ARM64 Details for jsbox-0.1.3-cp310-abi3-musllinux_1_2_aarch64.whl
jsbox-0.1.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.10 abi3 Linux glibc 2.17+ x86-64 Details for jsbox-0.1.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
jsbox-0.1.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.10 abi3 Linux glibc 2.17+ ARM64 Details for jsbox-0.1.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
jsbox-0.1.3-cp310-abi3-macosx_11_0_arm64.whl CPython 3.10 abi3 macOS 11.0+ ARM64 Details for jsbox-0.1.3-cp310-abi3-macosx_11_0_arm64.whl

Total release size: 7.8 MB

Release files / jsbox-0.1.3.tar.gz

Download URL jsbox-0.1.3.tar.gz
Size 2.5 MB
Tags Source
SHA-256 checksum
How to use checksums
d4d7cd9cbc03d2808540279b9fd437c17751b0f3e2ca0c7f564819dd1980268a
BLAKE2b-256 checksum
How to use checksums
ce6de62047ec50fad865364aed5b22934e4740e0631bfce4d9395e19e3604c49
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / jsbox-0.1.3-cp310-abi3-win_amd64.whl

Download URL jsbox-0.1.3-cp310-abi3-win_amd64.whl
Size 775.8 kB
Tags CPython 3.10 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
733abd9d1460b7674802e7dcd2334ecb7779638efa348c5b2855178283c04e44
BLAKE2b-256 checksum
How to use checksums
211c924bf2ab50feccd5db1364725f982a10a203d6e51526f6bc0bca9bd0b512
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / jsbox-0.1.3-cp310-abi3-musllinux_1_2_x86_64.whl

Download URL jsbox-0.1.3-cp310-abi3-musllinux_1_2_x86_64.whl
Size 1.1 MB
Tags CPython 3.10 Linux musl 1.2+ x86-64 abi3
SHA-256 checksum
How to use checksums
4c40bb3441a914e26b40795b7430b8f7d367de83418f4474bd35fb94ed635e36
BLAKE2b-256 checksum
How to use checksums
3c22124df70ac2732cf7c73aa0f18bf20ca29f4a257633af90777abc80120c9b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / jsbox-0.1.3-cp310-abi3-musllinux_1_2_aarch64.whl

Download URL jsbox-0.1.3-cp310-abi3-musllinux_1_2_aarch64.whl
Size 1.0 MB
Tags CPython 3.10 Linux musl 1.2+ ARM64 abi3
SHA-256 checksum
How to use checksums
ab00f3bd002156da68f53ba8b991a673252b151ce13fa4a4da7fe59f3a2d5b66
BLAKE2b-256 checksum
How to use checksums
c04a3af991f11c1bb8fdeadfc294244c2aae78116db715f9f672ac9d7458fbe7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / jsbox-0.1.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL jsbox-0.1.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 862.8 kB
Tags CPython 3.10 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
1977c92bc77eb63e3966a095bddb6915b917adb93e6063672f1a75f97e49c49e
BLAKE2b-256 checksum
How to use checksums
399e247be9aa26e6681bd05552cdf1d4ed0f0314eb75fbd7895e493f7538a190
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / jsbox-0.1.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL jsbox-0.1.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 834.8 kB
Tags CPython 3.10 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
8a59397b6bb51798c2916bd5dc090d2f18427a552822e88278f4c6b170def6b9
BLAKE2b-256 checksum
How to use checksums
d3843b0b9a7be0db6b785d27c69f521d65ed877de274831a0044e9a16b1f1409
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / jsbox-0.1.3-cp310-abi3-macosx_11_0_arm64.whl

Download URL jsbox-0.1.3-cp310-abi3-macosx_11_0_arm64.whl
Size 785.9 kB
Tags CPython 3.10 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
3a44d578939324c46ae44890ee9b6fcdc8d1062ce66ece28b9d88a216a60990d
BLAKE2b-256 checksum
How to use checksums
77952c8ccd8ea25c30b75f7b89137f7ce160f04a43a20ac74961432174f4e754
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Security for jsbox 0.1.3

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 7 files in this release have been uploaded with provenance metadata.

Select from 7 files

Attestation for jsbox-0.1.3.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page