Skip to main content

sandlock Python SDK

Python bindings for sandlock, a lightweight process sandbox using Landlock and seccomp. No root, no Docker, no namespaces required.

Requires Linux 6.7+ with Landlock ABI v6.

pip install sandlock

Quick start

from sandlock import Sandbox

sandbox = Sandbox(
    fs_readable=["/usr", "/lib", "/lib64", "/bin", "/etc", "/proc", "/dev"],
    fs_writable=["/tmp"],
)
result = sandbox.run(["echo", "hello"], timeout=10)
assert result.success
print(result.stdout)  # b"hello\n"

API reference

Platform

sandlock.landlock_abi_version() -> int

Return the Landlock ABI version supported by the running kernel. Returns -1 if Landlock is unavailable.

sandlock.min_landlock_abi() -> int

Return the minimum Landlock ABI version required by sandlock (currently 6).

Confine

Apply a Sandbox's Landlock rules to the current process, in place. No fork, no exec.

from sandlock import Sandbox, confine

confine(Sandbox(
    fs_readable=["/usr", "/lib"],
    fs_writable=["/tmp"],
))

sandlock.confine(sandbox) -> None

Set PR_SET_NO_NEW_PRIVS and install the sandbox's Landlock ruleset on the live process. The confinement is irreversible.

Only Landlock fields are honored (fs_readable, fs_writable, fs_denied); IPC and signal scoping are always applied. Sandbox config that requires a supervisor or a fresh child (seccomp, network, resource limits, COW, env, policy_fn, etc.) is rejected rather than silently ignored. Raises ConfinementError on failure.

Sandbox

sandlock.Sandbox(**kwargs)

Sandbox configuration and runtime handle. Holds both the policy (filesystem, network, resource limits, etc.) and runtime state. Construct once, then call run() (blocking) or spawn() + lifecycle methods, or use as a context manager.

All config fields are optional. Unset fields mean "no restriction" unless noted otherwise. Runtime kwargs (name, policy_fn, init_fn, work_fn) are set at construction time alongside config fields.

A single Sandbox instance holds at most one running process at a time. For concurrent execution, create multiple instances.

Runtime kwargs:

  • name -- sandbox name (also its virtual hostname inside the sandbox). Auto-generated as sandbox-{pid} when omitted.
  • policy_fn -- optional callback for dynamic per-event policy decisions (see Dynamic policy).
  • init_fn / work_fn -- callbacks for COW fork mode (see Fork).

Sandbox is a context manager:

with Sandbox(fs_readable=["/usr", "/lib"]) as sb:
    result = sb.run(["echo", "hello"])

Filesystem (Landlock)

Parameter Type Default Description
fs_readable list[str] [] Paths the sandbox can read
fs_writable list[str] [] Paths the sandbox can read and write
fs_denied list[str] [] Paths explicitly denied
workdir str | None None Working directory; enables COW protection
chroot str | None None Path to chroot into before confinement
fs_mount dict[str, str] {} Map virtual paths to host directories inside chroot
fs_mount_ro dict[str, str] {} Like fs_mount, but writes under the virtual path are denied
image Image | None None Run inside a container image from pull_image()
cwd str | None None Child working directory

Network

Parameter Type Default Description
net_allow list[str] [] Outbound endpoint rules. Bare host:port is TCP; protocol prefixes opt others in: tcp://host:port, udp://host:port (or udp://*:* for any UDP), icmp://host (or icmp://* for any ICMP echo via the kernel ping socket — gated by net.ipv4.ping_group_range on the host). Empty = deny all. Raw ICMP is not exposed.
net_deny list[str] [] Default-allow outbound denylist for literal IP/CIDR targets. When combined with net_allow, denied destinations win.
net_allow_bind list[int | str] [] TCP ports the sandbox may bind (empty = deny all; ["*"] = allow any port; a listed 0 authorizes only bind(0))
net_deny_bind list[int | str] [] Default-allow TCP bind denylist. When combined with net_allow_bind, denied ports win.
port_remap bool False Transparent TCP port virtualization

HTTP ACL

Enforce method + host + path rules on HTTP traffic via a transparent MITM proxy. When http_allow is set, all non-matching HTTP requests are denied by default. Block rules are checked first and take precedence.

Parameter Type Default Description
http_allow list[str] [] Allow rules in "METHOD host/path" format
http_deny list[str] [] Block rules in "METHOD host/path" format
http_ports list[int] [80] TCP ports to intercept (443 added when http_ca is set)
http_ca str | None None CA certificate for HTTPS MITM
http_key str | None None CA private key for HTTPS MITM
http_inject_ca list[str] [] Trust bundle paths to splice the active MITM CA's public cert into at open time. Without http_ca, generates an ephemeral CA (key in memory only) and intercepts port 443. Requires an http_allow / http_deny rule
http_ca_out str | None None Writes the active CA's public certificate (PEM) to this path; never the private key. Requires an http_allow / http_deny rule

Rule format: "METHOD host/path" where method and host can be * for wildcard, and path supports trailing * for prefix matching. Paths are normalized (percent-decoding, .. resolution, // collapsing) before matching to prevent bypasses.

sandbox = Sandbox(
    fs_readable=["/usr", "/lib", "/etc"],
    http_allow=[
        "GET docs.python.org/*",
        "POST api.openai.com/v1/chat/completions",
    ],
    http_deny=["* */admin/*"],
)
result = sandbox.run(["python3", "agent.py"])

Chroot with mount mapping

Map host directories into a chroot — like Docker's -v /host:/container but without kernel bind mounts or root privileges. Each sandbox gets its own persistent workspace while sharing a read-only rootfs.

sandbox = Sandbox(
    chroot="/opt/rootfs",
    fs_mount={"/work": "/tmp/sandbox-1/work"},
    fs_readable=["/usr", "/bin", "/lib", "/etc"],
    cwd="/work",
)
result = sandbox.run(["python3", "task.py"])

Combine with workdir + max_disk for quota-enforced writes:

sandbox = Sandbox(
    chroot="/opt/rootfs",
    fs_mount={"/work": "/tmp/sandbox-1/work"},
    workdir="/tmp/sandbox-1/work",
    fs_storage="/tmp/sandbox-1/cow",
    max_disk="100M",
    on_exit="commit",
    fs_readable=["/usr", "/bin", "/lib", "/etc"],
)

Container images

sandlock.pull_image(reference, cache_dir=None) -> Image fetches and unpacks a container image without a Docker daemon or root. Sandbox(image=...) runs inside it: the image's rootfs becomes the chroot, read access to / inside it is granted, and its env and working directory fill only what env and cwd leave unset.

Like a container's writable layer, every write lands in a copy-on-write branch that is discarded when the run ends, so the cached image never changes. To keep output, mount a host directory with fs_mount and grant it in fs_writable; setting workdir, or an on_error other than "abort", is rejected, and on_exit is always abort. The cache belongs to the invoking user, so it is only as protected as that user's other files.

Reference Source
docker://python:3.12, docker://ghcr.io/org/img@sha256:... registry (Docker Hub by default)
oci:<dir>[:tag] OCI image layout directory
oci-archive:<file>[:tag] tar of an OCI image layout
docker-daemon:<ref>, docker-daemon:sha256:<id> local Docker daemon (Docker 25+)
from sandlock import Sandbox, pull_image

image = pull_image("docker://python:3.12-slim")
result = Sandbox(image=image, max_memory="512M").run(["python3", "-c", "print('hello')"])

# Keep output on the host; writes elsewhere are discarded.
Sandbox(image=image, fs_mount={"/out": "/srv/job-1"}, fs_writable=["/out"]).run(
    ["python3", "-c", "open('/out/result.txt', 'w').write('done')"]
)

# Or the image's own command:
Sandbox(image=image).run(image.config.default_cmd())

Image has rootfs and config; ImageConfig has entrypoint, cmd, env (KEY=VALUE strings), working_dir, and default_cmd(). Both are frozen dataclasses. References use skopeo's transport syntax (containers-transports(5)): the transport prefix is required, and a tag together with a digest is rejected. Registry credentials come from docker login's ~/.docker/config.json (or $DOCKER_CONFIG); credential helpers are not run. Every blob is verified against its digest, and each image is unpacked once into cache_dir, or $XDG_CACHE_HOME/sandlock/images by default. A cached image named by digest starts without network access. Failures raise SandlockError.

Resource limits

Parameter Type Default Description
max_memory str | int | None None Memory limit, e.g. "512M" or int bytes
max_processes int | None None Peak concurrent process limit; None is unlimited
max_open_files int | None None Max file descriptors (RLIMIT_NOFILE)
max_cpu int | None None CPU throttle as percentage of one core (1-100)
cpu_cores list[int] | None None CPU cores to pin sandbox to
num_cpus int | None None Visible CPU count in /proc/cpuinfo

Syscall filtering (seccomp)

Parameter Type Default Description
extra_deny_syscalls list[str] [] Extra syscall or syscall-group names to block in addition to Sandlock defaults (groups expand to their member syscalls)
extra_allow_syscalls list[str] [] Syscall groups to allow that are blocked by default (e.g. "sysv_ipc" to enable SysV shared memory, semaphores, and message queues); unknown groups and individual syscall names are rejected

Sandlock always applies its default syscall blocklist.

Deterministic execution

Parameter Type Default Description
random_seed int | None None Seed for deterministic getrandom()
time_start datetime | str | None None Start time for time virtualization: RFC 3339 with an offset, or an aware datetime
no_randomize_memory bool False Disable ASLR
no_huge_pages bool False Disable Transparent Huge Pages
deterministic_dirs bool False Sort directory entries lexicographically

Environment

Parameter Type Default Description
clean_env bool False Start with minimal environment
env dict[str, str] {} Variables to set/override in the child

GPU access

Parameter Type Default Description
gpu_devices list[int] | None None GPU device indices to expose ([] = all)

Misc

Parameter Type Default Description
uid int | None None Map to given UID inside a user namespace (e.g. 0 for fake root). Set together with gid
gid int | None None Map to given GID inside the user namespace. Must be set together with uid (both or neither)
no_coredump bool False Disable core dumps

COW filesystem isolation

Parameter Type Default Description
fs_storage str | None None Storage directory for the seccomp COW upper layer / deltas
max_disk str | int | None None Disk quota for COW storage, e.g. "1G" or int bytes
on_exit BranchAction COMMIT COMMIT, ABORT, KEEP, or DEFER
on_error BranchAction ABORT COMMIT, ABORT, KEEP, or DEFER

Protection opt-out

By default sandlock enforces every Landlock protection the host kernel supports and refuses to start when a required protection is unavailable. Two keyword arguments on Sandbox opt out of the strict default on a per-protection basis:

Parameter Type Default Description
allow_degraded list[Protection] [] Enforce each listed protection where the host kernel supports it, silently skip it where it does not.
disable list[Protection] [] Never enforce each listed protection, even on a kernel that supports it.
from sandlock import Sandbox, Protection

sb = Sandbox(
    fs_readable=["/data"],
    fs_writable=["/tmp"],
    allow_degraded=[Protection.SIGNAL_SCOPE, Protection.ABSTRACT_UNIX_SOCKET_SCOPE],
)

The two allow_degraded entries let the sandbox build on Linux kernels below 6.12, where the v6 IPC scopes are unavailable; on a capable kernel the scopes remain enforced. The protection policy is persisted with a checkpoint, so a restored sandbox keeps the exact posture it was built with. See the "Protection opt-out" section of ../docs/sandbox-reference.md for the per-protection ABI floors and the full semantics.

sandbox.run(cmd, timeout=None) -> Result

Run a command, capturing stdout and stderr.

  • cmd -- list of strings (command and arguments).
  • timeout -- max execution time in seconds (float). None = no timeout.
result = sandbox.run(["python3", "-c", "print(42)"], timeout=10.0)

sandbox.spawn(cmd) -> None

Spawn cmd without waiting. Use pid, pause(), resume(), kill(), and wait() to manage the process lifecycle.

Raises RuntimeError if a process is already running.

Sugar for create(cmd) + start(); use those directly when you need the fork-park-exec split (e.g. starting several sandboxes in lockstep, or attaching external tracing to the parked PID before the child execs).

sandbox.create(cmd) -> None

Fork the sandboxed child and install policy. The child is parked between policy install and execve; call start() to release it. pid is available after this call but the child is not yet running user code.

Raises RuntimeError if a process is already running.

sandbox.start() -> None

Release a previously create()d child to execve.

Raises RuntimeError if no child has been created.

sandbox.wait() -> Result

Wait for the running process to finish and return its Result.

Inspecting and deferring COW changes

Every Result from a sandbox with a workdir carries changes, the list of files and directories the run added, modified, or deleted in its COW branch. A dry run is a run whose branch action is ABORT:

sandbox = Sandbox(workdir=wd, on_exit=BranchAction.ABORT, ...)
result = sandbox.run(["sh", "-c", "echo hi > out.txt"])
for change in result.changes:
    print(change.kind, change.path)  # "A out.txt"

BranchAction.DEFER leaves the branch in the sandbox after the run exits so the caller can inspect it and decide later. While pending, upper_dir holds the new bytes of every added or modified file, laid out like workdir:

sandbox = Sandbox(workdir=wd, on_exit=BranchAction.DEFER, ...)  # on_error stays ABORT
result = sandbox.run(["python3", "tool.py"])
if sandbox.pending:
    if approve(result.changes, sandbox.upper_dir):
        sandbox.commit()
    else:
        sandbox.abort()
  • sandbox.pending -> bool: True between a DEFER run's exit and commit() / abort().
  • sandbox.upper_dir -> str | None: the pending branch's upper directory.
  • sandbox.commit(): merge into workdir. Blocks up to 5s on a workdir another sandbox is merging into; raises BranchError if the merge fails, leaving the branch preserved on disk. Last-writer-wins against anything that changed the workdir since the run.
  • sandbox.abort(): discard the branch.

A pending sandbox refuses another run() until it is decided. Leaving a with block, or letting the sandbox go away, preserves an undecided branch on disk the same way KEEP does; nothing is ever merged without a decision.

sandbox.run_interactive(cmd) -> int

Run with inherited stdio (no capture). Returns the exit code.

sandbox.name -> str | None

The sandbox name.

sandbox.pid -> int | None

The child PID while running, None otherwise.

sandbox.is_running -> bool

True if a process is currently running in this sandbox.

sandbox.ports() -> dict[int, int]

Current port mappings {virtual_port: real_port} while running. Only contains entries where port remapping occurred. Requires port_remap=True.

sandbox.pause() / sandbox.resume() / sandbox.kill()

Send SIGSTOP / SIGCONT / SIGKILL to the sandbox process group. Raises RuntimeError if the sandbox is not running.

sandbox.checkpoint(save_fn=None) -> Checkpoint

Capture a checkpoint of the running sandbox. See Checkpoint.

sandbox.restore_interactive(cp) -> None

Restore a checkpoint into a fresh, fully-sandboxed process. The checkpoint image is injected over a parked child, which resumes at the saved program counter under the full confinement; the sandbox is running when this returns (no start() needed). Manage it with pid, pause(), resume(), kill(), and wait() as usual.

x86_64 only. The image is rebuilt by an execve'd restore stub in a fresh, already-confined process, and the kernel vDSO is relocated onto the checkpoint-recorded base, so ordinary libc programs resume. Assumes the restore runs on the same kernel that took the checkpoint.

Raises RuntimeError if a process is already running in this sandbox or the restore fails. See Checkpoint.

sandbox.restore_skipped -> list[SkippedFd]

Fds the last restore_interactive() could not transparently recreate (sockets, pipes, memfds, pseudo-filesystem paths); the restored process runs without them. Each entry is a SkippedFd named tuple with fd (the fd number in the checkpointed process) and path (the resource it pointed at, e.g. pipe:[12345]). Empty if this sandbox never restored a checkpoint or every fd was restored.

Result

Returned by sandbox.run().

Attribute Type Description
success bool True if exit code is 0
exit_code int Process exit code
stdout bytes Captured standard output
stderr bytes Captured standard error
error str | None Error message on failure
changes list[Change] Filesystem changes the run made to its COW branch (empty without workdir)

Change

Attribute Type Description
path str Path relative to workdir
before Entry | None The workdir entry when the run first touched the path; None if absent
after Entry | None The branch entry when the change set was read; None if removed
kind str Derived: "A" (no before), "M" (both sides), "D" (no after)
content_unchanged bool Both sides present with the same kind and digest or target
type_changed bool Both sides present with different kinds

renames(changes) pairs each deleted file with the added file carrying the same digest, as (old_path, new_path) tuples.

Entry

Attribute Type Description
kind str "file", "dir", "symlink", or "other"
mode int Permission bits
size int Byte length for a file; 0 otherwise
digest bytes | None SHA-256 of the bytes; files only
target str | None Link target; symlinks only

Stage and Pipeline

Chain sandboxed commands with pipes using the | operator:

result = (
    sandbox_a.cmd(["echo", "hello"])
    | sandbox_b.cmd(["tr", "a-z", "A-Z"])
).run()
assert result.stdout == b"HELLO\n"

sandbox.cmd(args) -> Stage

Create a lazy Stage bound to this sandbox.

pipeline.run(stdout=None, timeout=None) -> Result

Run the pipeline. Each stage's stdout feeds the next stage's stdin.

Prompt guard

PromptGuard, Rule, ScanReport, Finding, ScanError, StatisticalClassifier, and TransformersClassifier are available from sandlock_guard and sandlock.guard; only the latter adds stage().

PromptGuard(threshold="medium", max_bytes=1_048_576, scan_timeout=2.0, rules=(), model=None, classifier=None)

threshold: low, medium, high, or critical. max_bytes: positive UTF-8 input byte limit. scan_timeout: positive finite stage scanning deadline in seconds, starting after EOF. rules: iterable of custom Rule objects, copied to an immutable tuple and added to the built-in rules. Duplicate IDs, including built-in IDs, raise ValueError; non-Rule entries raise TypeError. classifier: optional StatisticalClassifier or TransformersClassifier configuration. model: compatibility alias for classifier=StatisticalClassifier(model). Supplying both raises ValueError; unsupported classifiers raise TypeError. A classifier score meeting its own threshold adds a high severity finding. The IDs statistical-injection and transformers-injection are reserved.

StatisticalClassifier(path, threshold=None)

Load an existing classifier JSON file. threshold=None uses the value in the file; an explicit threshold must be strictly between 0 and 1. Invalid models raise ValueError; file access errors raise OSError.

TransformersClassifier(path, positive_labels, threshold=0.5, max_length=512, stride=256)

Configure local CPU inference; requires the optional transformers extra. positive_labels: nonempty sequence of exact class names whose softmax scores are summed. Supports mutually exclusive classification with at least two classes. max_length: 4 to 8192 tokens, within the model's limit. stride: overlapping tokens, less than the window's content capacity. threshold: strictly between 0 and 1. path names a directory with regular model and fast-tokenizer files, fingerprinted at construction. Runtime loading is lazy; scan failures raise ScanError.

Rule(id: str, pattern: str, severity: str, message: str)

Immutable custom rule. id starts with an ASCII letter and contains up to 64 letters, digits, underscores, dots, or hyphens. pattern is a nonempty Python regex compiled case-insensitively; severity uses the same levels as threshold. message is a nonblank finding description. Invalid fields raise ValueError.

guard.scan(text: str) -> ScanReport

Scan synchronously without a wall-clock timeout. Raises TypeError for non-string input or ScanError if inspection cannot complete.

guard.stage(*, max_memory=None) -> Stage

Return a sandboxed stage forwarding original UTF-8 bytes only after approval. Exit codes: 0 accepted, 1 rejected, 2 input or scanning error. max_memory overrides the default memory limit: 256 MiB for rules or statistics, 2 GiB for Transformers. Changed model files fail closed at worker startup.

Report and error types

  • ScanReport: flagged: bool (any finding meets the threshold), findings: tuple[Finding, ...], input_bytes: int, ruleset_version: str, model_score: float | None (maximum window score), model_digest: str | None (SHA-256 of the JSON file or neural model file manifest).
  • Finding: rule_id: str, severity: str, message: str.
  • ScanError.code: str: inspection failure identifier.

See Prompt injection defenses for usage and limits.

Gather

Fan multiple producers into one consumer via named pipes. Each producer's stdout is delivered to the consumer under a label the consumer reads from a sandlock.inputs dict. This is the structural primitive behind the XOA pattern: producers and consumer are independent sandboxes, and the consumer never executes producer code inside its own LLM call.

from sandlock import Sandbox

planner  = Sandbox(...)   # writes code
searcher = Sandbox(...)   # produces data
executor = Sandbox(...)   # consumes both

result = (
    searcher.cmd(["python3", "-c", "..."]).as_("data")
    + planner.cmd(["python3", "-c", "..."]).as_("code")
    | executor.cmd(["python3", "consume.py"])
).run()

Inside consume.py:

from sandlock import inputs

code = inputs["code"]
data = inputs["data"]
exec(compile(code, "<planner>", "exec"), {"data": data})

stage.as_(name) -> NamedStage

Label a Stage's stdout so the consumer can address it by name.

named_stage + named_stage_or_gather -> Gather

Combine two or more NamedStage values into a Gather. Repeated + extends the gather:

g = a.as_("x") + b.as_("y") + c.as_("z")

gather | consumer_stage -> GatherPipeline

Compose a Gather with a consumer Stage to form the runnable pipeline.

gather_pipeline.run(timeout=None) -> Result

Run all producers in parallel; each producer's stdout is wired to a pipe the consumer reads via inputs[name]. Returns the consumer's Result.

sandlock.inputs

Lazy dict-like accessor available inside the consumer process. Reads each producer's pipe on first access and caches the value.

from sandlock import inputs

inputs["code"]        # str: the producer's full stdout, decoded as utf-8
"data" in inputs      # bool
list(inputs.keys())   # ["data", "code"]

The pipe fds are passed via the _SANDLOCK_GATHER env var (name:fd,name:fd,...); the inputs object parses it on first access. Users do not interact with the env var directly.

Dynamic policy

Use policy_fn to make per-syscall decisions at runtime:

from sandlock import Sandbox, SyscallEvent, PolicyContext

def my_policy(event: SyscallEvent, ctx: PolicyContext):
    if event.category == "network" and event.host == "evil.com":
        return True   # deny
    if event.category == "file" and "/secrets" in (event.path or ""):
        ctx.deny_path("/secrets")
        return True   # deny
    return False      # allow

sb = Sandbox(..., policy_fn=my_policy)

SyscallEvent

Attribute Type Description
syscall str Syscall name (e.g. "openat", "connect")
category str "file", "network", "process", or "memory"
pid int Process ID
parent_pid int Parent process ID
path str | None File path (for file events)
host str | None Hostname (for network events)
port int Port number (for network events)
argv tuple[str, ...] | None Command arguments (for execve)
denied bool Whether this event was already denied by static policy

Helper methods:

  • event.path_contains(s) -- True if path contains substring s
  • event.argv_contains(s) -- True if any argv element contains s

PolicyContext

Methods available inside policy_fn:

Method Description
ctx.restrict_network(ips) Restrict to given IP addresses
ctx.grant_network(ips) Allow additional IP addresses up to the static network ceiling
ctx.restrict_max_memory(bytes) Lower memory limit
ctx.restrict_max_processes(n) Lower process limit
ctx.restrict_pid_network(pid, ips) Per-PID network restriction
ctx.deny_path(path) Deny access to a path
ctx.allow_path(path) Remove a previously denied path

Dynamic IP restrictions resolve with legacy priority (per-PID override > live policy > static allowlist); the static net_deny layer is checked after the allow verdict and always wins, so a dynamic override can never erase it.

Callback return values:

Return Meaning
None, False, 0 Allow
True, -1 Deny (EPERM)
positive int Deny with that errno
"audit", -2 Allow but flag for audit

Fork

COW fork for parallel execution with shared initialization:

sb = Sandbox(
    fs_readable=[...],
    init_fn=lambda: load_model(),
    work_fn=lambda clone_id: process(clone_id),
)
clones = sb.fork(4)  # returns ForkResult with .pids

sandbox.reduce(cmd, fork_result) -> Result

Pipe combined clone output into a reducer command:

result = sandbox.reduce(["python3", "sum.py"], clones)

Checkpoint

Save and restore sandbox state:

sb = Sandbox(...)
# ... start a long-running process ...
cp = sb.checkpoint(save_fn=lambda: my_state_bytes())
cp.save("my-snapshot")

# Later:
cp2 = Checkpoint.load("my-snapshot", restore_fn=lambda data: rebuild(data))

# Or restore the OS-level process image into a fresh sandbox
# (x86_64, vDSO-free programs):
sb2 = Sandbox(...)   # same policy
sb2.restore_interactive(cp2)
for f in sb2.restore_skipped:
    print(f"fd {f.fd} not restored: {f.path}")
Method Description
cp.save(name, store=None) Persist checkpoint to disk
Checkpoint.load(name, store=None, restore_fn=None) Load from disk; if the checkpoint carries app_state and restore_fn is given, call restore_fn with it. Neither save_fn nor restore_fn is mandatory, and they need not be paired
Checkpoint.list(store=None) List saved checkpoint names
Checkpoint.delete(name, store=None) Delete a saved checkpoint
sb.restore_interactive(cp) Restore the process image into a fresh sandbox; see Sandbox
sb.restore_skipped Fds the last restore could not recreate, as SkippedFd(fd, path)

Properties: cp.name (str), cp.app_state (bytes or None).

Default store: ~/.sandlock/checkpoints/.

Profiles

Load sandbox configuration from TOML files in ~/.config/sandlock/profiles/. They are parsed by the same core parser as the CLI, so a profile means the same thing to both. Profiles contain sandbox config only; pass the sandbox name at construction: Sandbox(..., name=...).

from sandlock import load_profile, list_profiles

sandbox = load_profile("web-scraper")
names = list_profiles()

Exceptions

SandlockError (base)
  +-- SandboxError         invalid sandbox configuration
  +-- SandboxRuntimeError  sandbox lifecycle errors
        +-- ForkError          fork failed
        +-- ChildError         child exited abnormally
        +-- BranchError        COW branch operation failed
        |     +-- BranchConflictError   sibling branch committed (ESTALE)
        +-- ConfinementError   Landlock/seccomp setup failed
              +-- LandlockUnavailableError   no Landlock support
              +-- SeccompError               seccomp filter failed
                    +-- NotifError           notif supervisor error
  +-- MemoryProtectError   mprotect failed

All exceptions are importable from sandlock.exceptions or directly from sandlock:

from sandlock import SandlockError, SandboxError, SandboxRuntimeError

Enums

BranchAction

  • BranchAction.COMMIT -- merge writes on exit
  • BranchAction.ABORT -- discard writes
  • BranchAction.KEEP -- leave the branch on disk for recovery tooling
  • BranchAction.DEFER -- hold the branch for commit() / abort()

MCP integration

Sandboxed tool execution for AI agents. Each tool runs in a per-call sandbox with deny-by-default permissions.

pip install 'sandlock[mcp]'

McpSandbox

from sandlock.mcp import McpSandbox

mcp = McpSandbox(workspace="/tmp/agent", timeout=30.0)
  • workspace -- directory the sandbox can read (default: "/tmp/sandlock").
  • timeout -- default timeout in seconds per tool call (default: 30.0).

mcp.add_tool(name, func, *, description="", capabilities=None, input_schema=None)

Register a local tool. func must be a top-level function in an import-safe module: the worker imports that module by name and calls the function in a fresh per-call sandbox. Module-level imports, helpers, constants, and state are all fine; lambdas, methods, and nested functions are rejected. Guard any module startup logic under if __name__ == "__main__":.

A tool that declares a parameter named workspace receives the sandbox's workspace path automatically (injected at call time, hidden from the LLM schema, and not overridable by the model). No env wiring needed.

# tools.py  (an importable module)
import os

def read_file(path: str, *, workspace: str) -> str:
    with open(os.path.join(workspace, path)) as f:
        return f.read()
import tools

mcp.add_tool("read_file", tools.read_file,
    description="Read a file from the workspace",
)

No capabilities = read-only, clean environment, no network. Grant permissions explicitly:

Capability Example Description
fs_writable ["/tmp/agent"] Paths the tool can read and write
net_allow ["api.example.com:443", "udp://1.1.1.1:53"] Outbound endpoints. Bare host:port is TCP; udp://... / icmp://... schemes opt UDP / ICMP echo in.
env {"KEY": "val"} Environment variables to pass
max_memory "256M" Memory limit

Any Sandbox field name is accepted as a capability key.

await mcp.add_mcp_session(session)

Discover tools from a remote MCP server. Capabilities are read from sandlock:* keys in the tool's annotations or meta dict.

await mcp.call_tool(name, arguments=None, *, timeout=None) -> str

Call a tool by name. Local tools run in a per-call sandbox. MCP tools are forwarded to their server session.

result = await mcp.call_tool("read_file", {"path": "data.txt"})

mcp.get_policy(tool_name) -> Sandbox

Return the computed Sandbox for a registered tool.

mcp.tool_definitions_openai() -> list[dict]

Tool definitions in OpenAI function-calling format, for use with chat completion APIs.

mcp.tools -> dict[str, Any]

All registered tools (local and MCP).

MCP server

A standalone MCP server with built-in sandboxed tools (shell, python, read_file, write_file, list_files):

# stdio (for Claude Desktop / Cursor)
sandlock-mcp --workspace /tmp/sandbox

# SSE (remote)
pip install 'sandlock[mcp-remote]'
sandlock-mcp --transport sse --host 0.0.0.0 --port 8080 --workspace /tmp/sandbox

Claude Desktop configuration:

{
  "mcpServers": {
    "sandlock": {
      "command": "sandlock-mcp",
      "args": ["--workspace", "/tmp/sandbox"]
    }
  }
}

policy_for_tool(*, workspace, capabilities=None) -> Sandbox

Build a deny-by-default Sandbox from explicit capabilities. Used internally by McpSandbox but available for direct use.

capabilities_from_mcp_tool(tool) -> dict

Extract sandlock:* capabilities from an MCP tool's annotations/meta.

License

Apache-2.0

Metadata

Release files for sandlock 0.9.0

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

Source distribution (sdist)

Source distribution for sandlock 0.9.0
File Size Uploaded
sandlock-0.9.0.tar.gz 160.2 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for sandlock 0.9.0
File
sandlock-0.9.0-cp314-cp314-manylinux_2_28_x86_64.whl CPython 3.14 CPython 3.14 Linux glibc 2.28+ x86-64 Details
sandlock-0.9.0-cp314-cp314-manylinux_2_28_aarch64.whl CPython 3.14 CPython 3.14 Linux glibc 2.28+ ARM64 Details
sandlock-0.9.0-cp313-cp313-manylinux_2_28_x86_64.whl CPython 3.13 CPython 3.13 Linux glibc 2.28+ x86-64 Details
sandlock-0.9.0-cp313-cp313-manylinux_2_28_aarch64.whl CPython 3.13 CPython 3.13 Linux glibc 2.28+ ARM64 Details
sandlock-0.9.0-cp312-cp312-manylinux_2_28_x86_64.whl CPython 3.12 CPython 3.12 Linux glibc 2.28+ x86-64 Details
sandlock-0.9.0-cp312-cp312-manylinux_2_28_aarch64.whl CPython 3.12 CPython 3.12 Linux glibc 2.28+ ARM64 Details
sandlock-0.9.0-cp311-cp311-manylinux_2_28_x86_64.whl CPython 3.11 CPython 3.11 Linux glibc 2.28+ x86-64 Details
sandlock-0.9.0-cp311-cp311-manylinux_2_28_aarch64.whl CPython 3.11 CPython 3.11 Linux glibc 2.28+ ARM64 Details
sandlock-0.9.0-cp310-cp310-manylinux_2_28_x86_64.whl CPython 3.10 CPython 3.10 Linux glibc 2.28+ x86-64 Details
sandlock-0.9.0-cp310-cp310-manylinux_2_28_aarch64.whl CPython 3.10 CPython 3.10 Linux glibc 2.28+ ARM64 Details

Total release size: 45.4 MB

Release files / sandlock-0.9.0.tar.gz

Download URL sandlock-0.9.0.tar.gz
Size 160.2 kB
Tags Source
SHA-256 checksum
How to use checksums
659c83517f5f0f7b81189ee99e3fad1530ed74f0467ab07c02374ad37f814145
BLAKE2b-256 checksum
How to use checksums
5a9cb709e93876697cc13c9e7755e482bf1f49d91bc3ead3602396c448c902fd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / sandlock-0.9.0-cp314-cp314-manylinux_2_28_x86_64.whl

Download URL sandlock-0.9.0-cp314-cp314-manylinux_2_28_x86_64.whl
Size 4.5 MB
Tags CPython 3.14 Linux glibc 2.28+ x86-64
SHA-256 checksum
How to use checksums
b49c0738359a1a97815daae8e0820a3686d8af73fdaecee5053d47238e8a181f
BLAKE2b-256 checksum
How to use checksums
0d20ccd517d329e6fcb37b4fd8dd0ec63595e51ef64c803c6cc1b7a239e178db
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / sandlock-0.9.0-cp314-cp314-manylinux_2_28_aarch64.whl

Download URL sandlock-0.9.0-cp314-cp314-manylinux_2_28_aarch64.whl
Size 4.5 MB
Tags CPython 3.14 Linux glibc 2.28+ ARM64
SHA-256 checksum
How to use checksums
9f5db6b522928a8435d6bee845c1fe2f58ef1507222da3ea62d457acb6fc4eb8
BLAKE2b-256 checksum
How to use checksums
d50a14689f2fcaa50b793053a5b418ee39d286b7561f19cb3da9bd80af3f695c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / sandlock-0.9.0-cp313-cp313-manylinux_2_28_x86_64.whl

Download URL sandlock-0.9.0-cp313-cp313-manylinux_2_28_x86_64.whl
Size 4.5 MB
Tags CPython 3.13 Linux glibc 2.28+ x86-64
SHA-256 checksum
How to use checksums
757c941e1f0a083063ea46dbf0e9234dde30c066cb289df58c4720d658b5652b
BLAKE2b-256 checksum
How to use checksums
75931fbc95b264b1b736cc2485c2ddf962ac6c0d598f6ce2d82a123db8d9316c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / sandlock-0.9.0-cp313-cp313-manylinux_2_28_aarch64.whl

Download URL sandlock-0.9.0-cp313-cp313-manylinux_2_28_aarch64.whl
Size 4.5 MB
Tags CPython 3.13 Linux glibc 2.28+ ARM64
SHA-256 checksum
How to use checksums
998d1d859cbcd934f7865bd907397d21ceacfce60df4eed613aee937d27f2ab4
BLAKE2b-256 checksum
How to use checksums
918c11b7569ca533a5603a82839028aedf3f431526b649e27ad9610a0e2b16bb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / sandlock-0.9.0-cp312-cp312-manylinux_2_28_x86_64.whl

Download URL sandlock-0.9.0-cp312-cp312-manylinux_2_28_x86_64.whl
Size 4.5 MB
Tags CPython 3.12 Linux glibc 2.28+ x86-64
SHA-256 checksum
How to use checksums
d3cdb9703b394cbd8a34af606c71ead295504b5f6ab3f25c09a4254edf212da1
BLAKE2b-256 checksum
How to use checksums
8803cd8e94d7f343ab2da9effc15afff74a01277f163a53b2fdc0dd335021a3a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / sandlock-0.9.0-cp312-cp312-manylinux_2_28_aarch64.whl

Download URL sandlock-0.9.0-cp312-cp312-manylinux_2_28_aarch64.whl
Size 4.5 MB
Tags CPython 3.12 Linux glibc 2.28+ ARM64
SHA-256 checksum
How to use checksums
1913b786736b11feed3e8367fffccfba3df444d2ac00a98f53475ca089622632
BLAKE2b-256 checksum
How to use checksums
550df1057b0f7a41c0af1046d8810b9b8a24a5216063b9bda41eb882779608d6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / sandlock-0.9.0-cp311-cp311-manylinux_2_28_x86_64.whl

Download URL sandlock-0.9.0-cp311-cp311-manylinux_2_28_x86_64.whl
Size 4.5 MB
Tags CPython 3.11 Linux glibc 2.28+ x86-64
SHA-256 checksum
How to use checksums
f28186365370c3e442139b3dcdcb5b887778aa78ddfb506f343b36e51d5e0dff
BLAKE2b-256 checksum
How to use checksums
9e46676f40fd9ff13b0c771394fb15e00215720b5175f0eaf713cb6b754699d5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / sandlock-0.9.0-cp311-cp311-manylinux_2_28_aarch64.whl

Download URL sandlock-0.9.0-cp311-cp311-manylinux_2_28_aarch64.whl
Size 4.5 MB
Tags CPython 3.11 Linux glibc 2.28+ ARM64
SHA-256 checksum
How to use checksums
055f1ec05b78e40ab16f71a0ac46781e8abb2f6e1ba0b0b2de1d0fd0a0c13053
BLAKE2b-256 checksum
How to use checksums
b9c4aba4c9d83a2cc95dbfbe2fe458e7ee4abf2051a3ea5ded9dacf74f7a19ca
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / sandlock-0.9.0-cp310-cp310-manylinux_2_28_x86_64.whl

Download URL sandlock-0.9.0-cp310-cp310-manylinux_2_28_x86_64.whl
Size 4.5 MB
Tags CPython 3.10 Linux glibc 2.28+ x86-64
SHA-256 checksum
How to use checksums
94417b8c08469b8fb5eb0d64952d4c5af35d18e37b6a918451b4d61f0dcdb0c4
BLAKE2b-256 checksum
How to use checksums
564a938e7024eb8adb184c8046310ca910b6bc23d3abfc0ebbb757e7c11a378e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / sandlock-0.9.0-cp310-cp310-manylinux_2_28_aarch64.whl

Download URL sandlock-0.9.0-cp310-cp310-manylinux_2_28_aarch64.whl
Size 4.5 MB
Tags CPython 3.10 Linux glibc 2.28+ ARM64
SHA-256 checksum
How to use checksums
fcca0ec03414770695ed6a80d3ab5d0990a7a83869d333e096e7c1bc776afc6e
BLAKE2b-256 checksum
How to use checksums
fab5bdd43be512978acbaac7da2f019268ef9efa0c1d6850cffeb3c002da48b3
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

This release

0.9.0 This release

11 release files

0.8.9

11 release files

0.8.8

11 release files

0.8.5

9 release files

0.8.4

9 release files

0.8.3

9 release files

0.8.2

9 release files

0.8.1

9 release files

0.8.0

9 release files

0.7.0

9 release files

0.6.0

5 release files

0.5.0

5 release files

0.4.8

5 release files

0.4.7

5 release files

0.4.6

5 release files

0.4.4

1 release file

0.4.3

1 release file

0.4.2

1 release file

0.4.1

1 release file

0.4.0

1 release file

0.3.0

2 release files

0.2.0

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