Skip to main content
kern

Kern Sandbox

Your model writes the code. This runs it where it can't touch your machine.

Works with Claude Code · Cursor · Claude Desktop · LM Studio · LangChain · pi

CI License: Apache-2.0 Release PyPI npm Runs on

rootless · no daemon · no socket · no VM · no cloud · no account

The runtime · MCP server · Security model · Benchmarks

An agent's tool-call, a generated snippet, a notebook cell, a CI step: it arrives, you run it, and nobody has read it first.

In a terminal:

# if the venv line fails, your distribution ships it separately:
#     sudo apt install python3-venv
python3 -m venv .venv && . .venv/bin/activate
pip install -U kern-sandbox   # on Linux x86_64 and aarch64, this brings kern with it

On a Mac, run these inside a Linux VM, with your code: macOS.

Then in a Python file:

import kern_sandbox as kern

r = kern.run_code("print(sum(range(100)))")
print(r.stdout, r.fault)   # 4950  None

That call ran the code in a fresh container from an OCI image, with no network, memory and PID caps and a deadline applied from outside, and threw the container away before returning.

  • Cheap enough for every call: a hundred calls are a hundred containers, and nothing is left behind.
  • State when you want it: a Sandbox() you keep open shares /workspace, and kernel() keeps one warm interpreter where variables carry too.
  • Imports are precompiled once: the image's standard library is compiled in the background the first time you use that image and mounted READ-ONLY into every box after it, so import json, re in a fresh container is about 3x cheaper. pyc_cache=False turns it off.
  • Two parts, one install: the kern binary is the isolation, this package is the API in front of it. The Linux wheels carry the binary, so the venv has kern doctor and kern ps too.

When you would use this

Your model just wrote a script and you are about to run it. Paste it into run_code instead of your terminal. It runs in a container built from an image, so there is no home directory of yours in there to delete and no key to read. A hallucinated rm -rf ~ resolves to the container's own /root, which is mounted read-only, so it fails there too.

An agent writing and running code in a loop. Give it the LangChain tool or the MCP server. Nothing step 3 left behind is waiting for step 12, and a step that hangs or runs out of memory comes back as a value your loop can branch on.

Analysis you did not write. A chart comes back as an image the model can see, and a failure comes back labelled, so you can tell a bug in the code from the sandbox stopping it.

The basics, one call each

Each block starts from import kern_sandbox as kern, as above, and prints what the comment says.

Give it a file, get a file back. A Sandbox() you keep open has a /workspace that you and the code both see.

job = """
import csv
rows = list(csv.DictReader(open("in.csv")))
hottest = max(rows, key=lambda row: int(row["temp"]))
open("out.txt", "w").write(hottest["city"])
"""
with kern.Sandbox() as sb:
    sb.write_file("in.csv", "city,temp\nRome,24\nOslo,9\n")
    sb.run_code(job)
    print(sb.read_file("out.txt"))   # b'Rome'

Keep variables between steps. kernel() is one warm interpreter, the way a notebook is.

with kern.Sandbox() as sb, sb.kernel() as k:
    k.run_code("x = 40")
    r = k.run_code("print(x + 2)")
    print(r.stdout)   # 42

Install a package first. setup= runs once, with network, before any of your code. The code itself still runs without network.

with kern.Sandbox(setup="pip install numpy") as sb:
    r = sb.run_code("import numpy as np; print(np.pi)")
    print(r.stdout)   # 3.141592653589793

Let it reach one host. There is no network by default. egress_allow opens the hosts you name and nothing else; network=True opens everything, your own loopback included.

code = ("import urllib.request as u; "
        "print(u.urlopen('https://pypi.org').status)")
r = kern.run_code(code)
print(r.exit_code)   # 1: no network
r = kern.run_code(code, egress_allow=["pypi.org"])
print(r.stdout)      # 200

Run a shell command.

r = kern.run_code("uname -s && python3 --version", language="bash")
print(r.stdout)   # Linux, then the image's Python 3.12

Get a chart back. A matplotlib figure open when the code ends comes back as a PNG on r.results, and fig or display(fig) draws a figure the code holds. In a kernel() session a figure is closed once its cell has returned it, as in Jupyter, so later cells do not return it again.

plot = """
import matplotlib.pyplot as plt
plt.plot([1, 4, 9, 16])
plt.show()
"""
with kern.Sandbox(setup="pip install matplotlib") as sb:
    r = sb.run_code(plot)
    open("chart.png", "wb").write(r.results[0].png)

The result says who stopped the run

A scrolling Python session, seven calls. run_code returns ('4950', None); an infinite loop under timeout_s=3 returns fault.type 'timeout' and exit 137; a 400 MiB allocation under memory_mb=128 returns 'oom' and 137; a urlopen with the network off returns fault None and exit 1, so the code raised and the sandbox stopped nothing; os.remove('/root/.bashrc') comes back OSError [Errno 30] Read-only file system; print(1) on alpine:3.19 returns 'exec_failed' and 127 because alpine ships no python3; and print(1) on an image that does not exist returns 'startup_failed' and 1. Exit 137 does not say which of those it was; the fault field does.

docker run gives you exit 137 and leaves you to guess whether that was your timeout, the OOM killer or something else. This tells you. Every row was run:

the code fault.type exit_code
print(sum(range(100))) None 0
while True: pass, timeout_s=3 timeout 137
bytearray(400<<20), memory_mb=128 oom 137
urlopen(...), network off None 1
print(1) on alpine:3.19, no python3 exec_failed 127
print(1) on an image that is not there startup_failed 1

The fourth row is the one a loop gets wrong: the network was off, so the code raised and the sandbox did nothing. fault is read from a pipe kern writes rather than from stdout, so code that prints [exit 0] can't fake it. Also killed and escape_blocked. These values and the exit codes are stable: one changes only to correct an outcome that was reported wrong, and the release that does it says so first in its CHANGELOG entry.

Works with

  • Any MCP client: Claude Code, Cursor, Claude Desktop, LM Studio, Zed. The package ships kern-mcp, a stdio server, and charts come back as images the model can see. One session backs the connection, so files carry between tool calls and variables do not, unless KERN_MCP_KERNEL=1: docs/MCP.md.
  • LangChain: kern_code_tool() is a StructuredTool, and a fault comes back labelled for the model. There is a shell policy too: LANGCHAIN-SHELL.md.
  • pi: kern-pi routes its file and shell tools into a box, your working directory at /workspace.
  • Python and Node: the same API on both, pip install kern-sandbox and npm i kern-sandbox.
{
  "mcpServers": {
    "kern": { "command": "uvx", "args": ["--from", "kern-sandbox", "kern-mcp"] }
  }
}

A client spawns the server from its own PATH, so a venv is invisible to it: uvx fetches the package, kern included on Linux, and pipx install kern-sandbox is the other way. From macOS or Windows swap the command for wsl or ssh.

Safe by default

Two threats, and the second is not covered by the first. A compromised dependency is stopped by the filesystem and the network: no network unless you ask, a read-only root, and only the paths you name. A prompt-injected agent is not, because it runs the code you asked for. The defence there is that the credentials were never in the box at all, which is why mounts over them are refused rather than discouraged.

A bare Sandbox() runs python:3.12-slim with a 30 second deadline, no network, no host mounts, seccomp on and capabilities dropped. The timeout is mandatory: there is no value that disables it. Every relaxation is a named argument, and two have surprised people, both measured:

  • Mounts over sensitive sources are refused even if you ask: the host's own directories, kern's own state, and 17 credential directories by name (.ssh, .aws, .kube, .gnupg, .netrc, .npmrc, .git-credentials and the rest), plus ~/.config/gh and ~/.config/gcloud. No opt-out. Mount a copy.
  • network=True includes the host's loopback, where unauthenticated services live. A test read the host's SSH banner off 127.0.0.1:22. egress_allow is the middle setting and is route-level.

How fast

Horizontal bar chart on a log scale, times the cost of one kern-sandbox call: kern-sandbox with a prewarm pool 21x faster, kern-sandbox 1x, llm-sandbox with its session kept alive 5x, podman run --rm 19x, docker run --rm 20x, and Docker Sandboxes (sbx) into an already running sandbox 29x. One tool-call, print(1), rootless, 2026-09-21; a heavier call is 7x rather than 20x.

A tool-call costs a twentieth of docker run. Most of what is left is CPython starting inside the box, not the box, so a heavier call narrows it: import json,re is 7x rather than 20x, and 3x of that is the stock image compiling its standard library, which a precompiled one removes. The prewarm bar is what a call gets while the pool keeps up. (BENCHMARKS.md has the method. Measure your own.)

Compared to what you are probably doing

  • a venv isolates imports, not the process: the code still has your files, your keys and your network.
  • docker run per call is the same idea with a daemon in front of it, and costs 20x as much per call.
  • nono fences the environment you already have with Landlock, so your own tools are there and state carries between commands. This builds a new one from an image instead. Measured both ways in BENCHMARKS.md.
  • bubblewrap and nsjail are the building blocks kern uses: no images, no cgroup caps, and no verdict, so you get an exit code and work out the rest.
  • a microVM (Firecracker, Kata) or gVisor is a stronger boundary than this one, and the right answer when the code is actively hostile. It costs what a machine costs.
  • E2B, Modal, Daytona do the same job in someone else's cloud, with an account and your code leaving the machine.

Current limitations

  • Not a boundary against deliberately hostile code. Namespaces, cgroups and seccomp, for your own or semi-trusted code. If it is hostile or someone else's, use a microVM or gVisor: SECURITY.md.
  • Caps bind only where your host delegates a cgroup. kern doctor says whether yours does; require_limits=True refuses to start rather than run uncapped.
  • Nothing bounds the workspace. It is a host directory, so a job can fill your disk.
  • A non-root user= shares the workspace through a POSIX ACL. The account is a uid of kern's range on disk, so the workspace needs a filesystem with ACLs and the host a range in /etc/subuid. A file or directory that account closes on purpose (mkstemp's 0600, mkdtemp's 0700) is reached through a short-lived box of the same image running as that account, one box start per access.
  • Not inside a container without --privileged, and not on Google Colab. A box mounts a fresh /proc, and the kernel allows that only where an unmasked /proc already exists. A container that masks parts of its own /proc, which is the default and is why it is safe, leaves none, so the box stops at mount(proc) failed: Permission denied. Measured on Colab, kernel 6.6.122.
  • The binary comes with the package only on Linux x86_64 and aarch64. Anywhere else pip picks the wheel without it, and the package drives a kern on PATH or in $KERN_BIN, installed with the install script. $KERN_BIN always wins over the bundled one.

More

Charts and mime-typed results without a Jupyter kernel, the full API, snapshots, and the measured sharp edges: SANDBOX-NOTES.md.

Runs on Linux, with unprivileged user namespaces and cgroup v2, and Python 3.9+. Windows through WSL2; on a Mac, inside a Linux VM, where the same pip install is the whole install. install notes. Apache-2.0.

Metadata

Release files for kern-sandbox 0.2.47

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

Source distribution (sdist)

Source distribution for kern-sandbox 0.2.47
File Size Uploaded
kern_sandbox-0.2.47.tar.gz 411.6 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for kern-sandbox 0.2.47
File Interpreter ABI Platform
kern_sandbox-0.2.47-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64.whl Python 3 none Linux glibc 2.17+ x86-64, Linux musl 1.1+ x86-64 Details
kern_sandbox-0.2.47-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl Python 3 none Linux musl 1.1+ ARM64, Linux glibc 2.17+ ARM64 Details
kern_sandbox-0.2.47-py3-none-any.whl Python 3 none any Details

Total release size: 3.2 MB

Release files / kern_sandbox-0.2.47.tar.gz

Download URL kern_sandbox-0.2.47.tar.gz
Size 411.6 kB
Tags Source
SHA-256 checksum
How to use checksums
9a849d015fc653f66612b084734ac2a511e4197524961dd0783a99eb4e878f06
BLAKE2b-256 checksum
How to use checksums
cb5d496e5d55869f6d11759ebb089b041aa596886ee3bb217c8707c62498b2d2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / kern_sandbox-0.2.47-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64.whl

Download URL kern_sandbox-0.2.47-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64.whl
Size 1.3 MB
Tags Linux glibc 2.17+ x86-64 Linux musl 1.1+ x86-64 Python 3
SHA-256 checksum
How to use checksums
475874022dd547b22f5331242cee13d5fbdee082720509051f3b39e343876f6e
BLAKE2b-256 checksum
How to use checksums
dbea5eea8b55f55cc2d1f6aa3e6758384fff416d85f40d1c87616318f2c56bf5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / kern_sandbox-0.2.47-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl

Download URL kern_sandbox-0.2.47-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl
Size 1.2 MB
Tags Linux glibc 2.17+ ARM64 Linux musl 1.1+ ARM64 Python 3
SHA-256 checksum
How to use checksums
36f9e2aad3cb5170b12beea7c5657e51acaa9fcefafa42592829726707638271
BLAKE2b-256 checksum
How to use checksums
5ed4e1cf2260801b2a153893aaa86367ac24d005ec40d58c3a994078aaf94dc8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / kern_sandbox-0.2.47-py3-none-any.whl

Download URL kern_sandbox-0.2.47-py3-none-any.whl
Size 217.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3bd09a9575e4db8cadc7ba4e8d36620e066fbb98919bd8cefe28002139029957
BLAKE2b-256 checksum
How to use checksums
10d284742df61f40b3d71f49c554bdc1a980fc92b754ebbc0c978391db295bbe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

0.2.48

4 release files

This release

0.2.47 This release

4 release files

0.2.46

4 release files

0.2.42

4 release files

0.2.41

4 release files

0.2.40

2 release files

0.2.39

2 release files

0.2.38

2 release files

0.2.37

2 release files

0.2.36

2 release files

0.2.35

2 release files

0.2.34

2 release files

0.2.33

2 release files

0.2.32

2 release files

0.2.31

2 release files

0.2.30

2 release files

0.2.29

2 release files

0.2.28

2 release files

0.2.27

2 release files

0.2.26

2 release files

0.2.25

2 release files

0.2.24

2 release files

0.2.22

2 release files

0.2.20

2 release files

0.2.19

2 release files

0.2.18

2 release files

0.2.17

2 release files

0.2.16

2 release files

0.2.15

2 release files

0.2.14

2 release files

0.2.13

2 release files

0.2.12

2 release files

0.2.11

2 release files

0.2.10

2 release files

0.2.9

2 release files

0.2.8

2 release files

0.2.7

2 release files

0.2.6

2 release files

0.2.5

2 release files

0.2.4

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.43

2 release files

0.1.33

2 release files

0.1.32

2 release files

0.1.31

2 release files

0.1.30

2 release files

0.1.29

2 release files

0.1.28

2 release files

0.1.27

2 release files

0.1.26

2 release files

0.1.25

2 release files

0.1.24

2 release files

0.1.23

2 release files

0.1.22

2 release files

0.1.21

2 release files

0.1.20

2 release files

0.1.11

2 release files

0.1.10

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

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