One policy, lowered per OS
You write a typed, deny-by-default policy once. procage lowers it to a Seatbelt profile or a Linux plan, and every construct is exact, an approximation you accept by name, or a typed capability gap.
PoliciesRust crate · macOS and Linux · pre-1.0
procage runs a child process under an OS sandbox: Seatbelt on macOS, without sandbox-exec, and Landlock, seccomp and namespaces on Linux. It is built for commands that run untrusted, adaptive code as your user, such as an LLM agent’s tool calls or a project’s build scripts.
Status: pre-1.0: the API may change between minor versions.
use std::process::Command;
use procage::native::{Cage, CageOptions, Grants};
use procage::policy::{presets, AbsPath, LowerOptions};
// Read the workspace; no writes, no network. Secrets,
// git hooks and launch agents are denied in any policy.
let workspace = AbsPath::new("/path/to/project")?;
let policy = presets::agent_readonly::v3(&workspace);
let options = CageOptions::default()
.with_lower(LowerOptions::default().accept_spawn_snapshot());
let cage = Cage::with_options(&policy, &options)?;
let mut child = cage.spawn(
Command::new("/usr/bin/grep")
.args(["-rn", "TODO", "."])
.current_dir("/path/to/project"),
&Grants::new(),
)?;
let status = child.wait()?;
// Dropping `child` kills every process the run started.A preset, prepared once and spawned. Linux hosts that cannot enforce part of it refuse with a list of gaps.
An agent that follows injected instructions, or a malicious build script, runs with your user id. It can read ~/.ssh, plant a git hook your shell later runs, register a launch agent, or keep a daemon alive after you thought it ended. procage is meant to keep that code away from your secrets, from files outside the paths you grant, from your terminal and session, and from persistence locations.
You write a typed, deny-by-default policy once. procage lowers it to a Seatbelt profile or a Linux plan, and every construct is exact, an approximation you accept by name, or a typed capability gap.
PoliciesA host that cannot enforce part of a policy refuses the cage and lists every gap. Cage::fit relaxes only what you name and tells you what would lift each relaxation.
Instead of a bare “Operation not permitted”, a reporting cage lists each denied operation with the process, the path and the rule behind it (not granted, hidden, protected, a hard-deny class), live or after the exit: every denial on macOS, every refused system call on Linux.
ViolationsAsk why a path can or cannot be read, written or executed: the deciding rule, and the narrowest change that would allow it. Diff two policies to see every rule one allows that the other does not. The kernel checks every answer on every CI lane.
Explain and diffSecrets, trust-handoff files such as git hooks and shell startup files, persistence locations, privilege changes and more are denied last, in every policy, and waivable only by class name.
Hard-deny classesInherited descriptors are closed, secret-looking variables dropped, the terminal kept out of reach, and every process the run started is killed when the run ends, double-forked daemons included.
Lifetime and hygieneEvery finished run says how it was caged: the policy digest, backend and tier, the OS build, and everything accepted short of the policy.
ReceiptsEvery known escape class has a stable id and a status in the threat model. A class is marked handled only when caged canaries on a real kernel show the refusal.
Threat modelThe library applies the sandbox in the forked child with system calls only. Or a small helper becomes the caged command with Cage::exec, for code that starts and kills processes itself.
procage measures what the host offers and never falls back to running unsandboxed. What a Linux host gets depends on its tier and Landlock ABI.
| Platform | Mechanism | Needs | Notable limits today |
|---|---|---|---|
| macOS | Seatbelt profile compiled and applied through libsandbox’s private interface, resolved at run time | A process that is not itself sandboxed | No pseudo-terminals; base profiles curated on macOS 26.6.2 |
| Linux tier T1 | User, mount, IPC, UTS and network namespaces, mounts, Landlock, seccomp | Unprivileged user namespaces, Landlock ABI 1 (Linux 5.13) or later | No PID namespace; no loopback networking; glob rules are gaps |
| Linux tier T2 | Landlock and seccomp only | Landlock ABI 1 or later (for example Docker’s default settings) | As T1, no proxy-only networking, and cannot protect or hide a path inside a writable tree |
Linux tests run on eight mainline kernels from 5.15 to 7.3.1 (Landlock ABI 1 to 11) and in Docker on both tiers. Details: macOS backend, Linux backend and tiers.
An OS sandbox reduces the risk from adaptive same-user code; it does not remove it. The kernel and OS services stay reachable, and a bug in them is an escape. When that is unacceptable, use a microVM, or a microVM with procage inside it for finer-grained policy.
The threat model lists every escape class and its status, and what the calling harness must handle itself.
↑↓ to move · Enter to open · Esc to close