Run untrusted Python safely
eryx is an open-source WebAssembly sandbox running full CPython 3.14. Embed it from Python, JavaScript or Rust, and let sandboxed code await exactly the host functions you choose to expose.
import eryx
def get_user(args):
return {"id": args["id"], "name": "Alice"}
sandbox = eryx.Sandbox(callbacks=[
{"name": "get_user", "fn": get_user},
])
# Untrusted code: no filesystem, no network,
# only the callbacks you hand it
result = sandbox.execute('''
user = await get_user(id=123)
print(f"Hello {user['name']}")
''')
print(result.stdout) # Hello Alice
e·ryx noun
- A genus of sand boas: small, non-venomous snakes that spend most of their lives buried in sand, watching the world through eyes set high on their heads.
- A sandbox where Python does the same. Buried in WebAssembly, it only sees what you choose to put above the surface.
What's in the sandbox
Real CPython 3.14
Not a subset. The full standard library and SQLite work, and you can add packages from wheels. Native extensions like numpy are experimental.
Async host callbacks
Expose host functions that sandboxed code can await, including several at once with asyncio.gather. Tools from MCP servers can be exposed the same way.
Locked down by default
No host filesystem and no network unless you enable them. Every run has limits on time, memory, instructions executed and callback count.
Opt-in networking
Allow only the hosts you list, with wildcards. Localhost and private ranges stay blocked unless you opt in. requests, httpx and urllib just work.
Secrets that don't leak
Sandboxed code only ever sees a placeholder. The real value is swapped in for requests to allowed hosts, and placeholders are redacted from output and files.
Sessions & filesystem
Variables persist across runs for REPL-style use, and state can be snapshotted and restored. Files written to the virtual filesystem stay for the session, and you can mount host directories read-only or read-write.
Fast startup
A precompiled, pre-initialized runtime creates new Python sandboxes in about 1–5ms. A sandbox pool keeps warm instances ready for heavy throughput.
Visibility & control
Stream stdout and stderr live, trace execution line by line, cancel long runs, and get a structured result back as JSON.
How callbacks work
You expose functions
Pick which host functions the sandbox may call
Code hits await
The sandbox suspends and calls into your host
Your function runs
Query a database, call an API, ask a human
Execution resumes
The result comes back as an ordinary Python value
Install
Pick your host language. The Python, Rust and JavaScript packages all ship with the runtime built in.
Python
PyPIpip install pyeryx
# or try it with nothing installed
uvx --with pyeryx eryx -c 'print("hi")'
Rust
crates.iocargo add eryx --features embedded
let sandbox = Sandbox::embedded().build()?;
let result = sandbox.execute("print(1 + 1)").await?;
JavaScript
npmnpm install @bsull/eryx
# Browser and Node; needs JSPI
# (Chrome/Edge 133+, Node --experimental-wasm-jspi)
gRPC server
Dockerdocker run -p 50051:50051 sd2k/eryx-server
# Shared warm pool, callback replay and
# suspension, TLS/mTLS, Prometheus metrics
Built for agents
Give an agent a Python sandbox over MCP, or let sandboxed code call your existing MCP tools.
eryx as an MCP server
// Claude Desktop, Cursor, VS Code, ...
{
"mcpServers": {
"eryx": {
"command": "uvx",
"args": ["--with", "pyeryx[serve]",
"eryx", "serve"]
}
}
}
Exposes a run_python tool backed by a persistent session.
MCP tools inside the sandbox
# Servers are discovered from your Claude Code,
# Cursor, VS Code, Zed, Gemini or Codex config
repos = await mcp["github"].search_repos(
query="eryx sandbox"
)
for r in repos["items"][:5]:
print(r["full_name"])
An agent can chain many tool calls in one run instead of a model round trip for each.
Hosted eryx
We're exploring a hosted eryx API so you don't have to run sandboxes yourself. It isn't available yet. Leave your email if you'd like to hear when it is.
One email when it's ready. Nothing else.