Runtime Isolation

Kernel-Level Isolation for AI Agents

nono uses Landlock (Linux and Windows/WSL2) and Seatbelt (macOS) to create irrevocable, kernel-enforced allow-lists. No root, no containers, no overhead.

Why kernel-level sandboxing

AI coding agents run as your user, with access to everything you have: SSH keys, cloud credentials, source code across every project. Traditional sandboxing approaches like Docker or VMs introduce significant overhead — a daemon, image management, networking configuration, and volume mounts just to let an agent edit a file.

nono takes a different approach. On Linux, it uses Landlock LSM to restrict filesystem access at the kernel level. On macOS, it uses Seatbelt (the same sandbox framework behind every App Store application). Both mechanisms are irrevocable once applied — the sandbox cannot be loosened, only tightened.

This means zero runtime overhead after sandbox initialization, no root privileges required, and automatic inheritance by all child processes. The AI agent and every subprocess it spawns are structurally unable to access anything outside the allow-list.

What agents can actually reach

When a coding agent runs as your user, it inherits your full filesystem access. That includes your ~/.ssh directory, cloud credential files like ~/.aws/credentials and ~/.config/gcloud, any .env files sitting in project roots, and every source repository you have checked out locally.

A compromised dependency, a prompt injection in a document the agent reads, or simply a poorly scoped task can turn an otherwise well-intentioned agent into an exfiltration path. The agent doesn't need to be malicious. It just needs to be running with access it shouldn't have.

This is the threat nono addresses at the structural level. The allow-list isn't a filter that inspects what the agent tries to do. It's a kernel rule that makes operations outside the allow-list physically impossible to execute. The difference matters: filters can be bypassed through obfuscation, encoding, or unexpected syscall sequences. Kernel enforcement cannot.

getting-started.sh
# Install
brew install nono
# Sandbox Claude Code — kernel-enforced filesystem isolation
nono run --profile claude-code --allow-cwd -- claude
# Sandbox any process with default-deny filesystem
nono run --allow-cwd -- python my_agent.py
# Add network filtering — only LLM API endpoints allowed
nono run --allow-cwd --network-profile minimal -- python my_agent.py
# Inject credentials from keychain (real keys never enter the sandbox)
nono run --allow-cwd --proxy-credential openai -- python my_agent.py

Linux: Landlock LSM

Landlock is a Linux Security Module introduced in kernel 5.13. Unlike AppArmor or SELinux, which require root privileges and system-wide policy configuration, Landlock can be applied by any unprivileged process to restrict its own capabilities. This makes it a natural fit for sandboxing agent processes without touching system configuration.

The core model is a ruleset of allowed operations on specific paths. Rules are applied per-thread and inherited by all children. Once applied, the ruleset cannot be loosened, only tightened further. This irrevocability is by design: the kernel provides no mechanism to expand access after the sandbox is active.

nono targets Landlock ABI v5, which covers filesystem read, write, execute, and directory operations. On kernels that don't support the target ABI, nono degrades gracefully, applying the highest supported ABI version and logging a warning. This means agents still run in environments with older kernels, with progressively weaker but still meaningful restrictions.

The practical implication: when nono calls ruleset.restrict_self(), the agent process and every subprocess it spawns are bound by the filesystem allow-list at the kernel level. There is no way for a subprocess to escape into the parent's broader access scope.

macOS: Seatbelt

On macOS, nono uses Seatbelt, the same sandboxing framework Apple uses to isolate every App Store application. Seatbelt has been part of macOS since 10.5 and is the platform's authoritative process-level isolation mechanism.

Seatbelt profiles are written in a Scheme-like policy language. When nono initialises on macOS, it generates a Seatbelt profile from the JSON allow-list configuration and applies it to the current process using sandbox_init. Like Landlock, the profile is irrevocable once applied.

The generated profile allows read and write access to the declared paths, blocks access to everything else, and respects any network proxy configuration in the profile. The result is behaviourally equivalent to the Linux implementation: the agent can only reach what the profile explicitly permits.

Windows: WSL2

On Windows, nono runs inside WSL2 and applies Landlock enforcement to the Linux kernel that powers it. WSL2 runs a real Linux kernel (currently 6.6), so Landlock's filesystem isolation works natively. Your Windows filesystem is accessible via /mnt/c and subject to the same kernel-enforced allow-lists as any Linux path.

nono detects WSL2 at runtime using kernel-controlled indicators (not environment variables, which can be spoofed) and automatically adjusts its feature set. Most features work identically to native Linux. A small number of advanced features are unavailable due to WSL2 kernel limitations:

  • Capability elevation via seccomp notify is disabled (WSL2 kernel bug). nono warns and continues without it.
  • Credential proxy enforcement defaults to fail-secure. Profiles can opt in via the wsl2_proxy_policy field when credential injection is needed without full network lockdown.
  • Per-port network rules require Landlock ABI v4+, which will be available when the WSL2 kernel upgrades.

Overall feature parity is 84%. Filesystem isolation, network allowlists, profiles, undo, and audit trail all work. The limitations are clearly surfaced via nono setup --check-only which reports the WSL2 feature matrix.

Composable JSON Profiles

Define exactly what an agent can access with declarative JSON profiles. Built-in security groups cover language runtimes, cache directories, and editor integrations. Profiles are version-controlled alongside your code.

See profile reference →
terminal
$ nono run --allow-cwd --proxy-allow llmapi -- agent
▄█▄ nono v0.7.0
▀▄^▄▀ - Halo Nono!
Capabilities:
Filesystem:
/Users/user/.claude [read+write] (dir)
/Users/user/.local/share/claude [read] (dir)
/Users/user/.claude.json [read+write] (file)
/Users/user/.gitconfig [read] (file)
/Users/user/dev/myproject [read+write] (dir)
+ 45 system/group paths (use -v to show)
Network:
outbound: proxy (localhost:0)
Supervised mode: child sandboxed, parent manages network proxy.
Applying Kernel sandbox protections.
profiles/claude-code.json
{
"meta": {
"name": "claude-code",
"version": "1.0.0",
"description": "Anthropic Claude Code CLI agent"
},
"security": {
"groups": ["node_runtime", "python_runtime", "rust_runtime"]
},
"filesystem": {
"allow": ["$HOME/.claude"],
"read_file": ["$HOME/.gitconfig"]
},
"network": { "block": false },
"workdir": { "access": "readwrite" },
"undo": {
"exclude_patterns": ["node_modules", ".next", "target"]
},
"interactive": true
}
crates/nono/src/sandbox/linux.rs
use landlock::{
ABI, Access, AccessFs, AccessNet, BitFlags,
PathBeneath, PathFd, Ruleset, RulesetAttr,
RulesetCreatedAttr,
};
const TARGET_ABI: ABI = ABI::V5;
pub fn apply_sandbox(caps: &CapabilitySet) -> Result<()> {
let mut ruleset = Ruleset::default()
.handle_access(AccessFs::from_all(TARGET_ABI))?
.create()?;
for (path, access_mode) in caps.paths() {
let flags = access_to_landlock(access_mode);
let fd = PathFd::new(path)?;
ruleset = ruleset.add_rule(
PathBeneath::new(fd, flags)
)?;
}
// Irrevocable: cannot be loosened after this call
ruleset.restrict_self()?;
Ok(())
}

Security groups and profile composition

Profiles are built from two layers: security groups, which are maintained by nono and cover common runtime requirements, and explicit path declarations for project-specific access.

Security groups like node_runtime, python_runtime, and rust_runtime grant read access to the paths those toolchains actually need: binary directories, cache paths, package manager directories, and editor integration files. Using groups means profiles stay concise and are maintained as toolchain layouts change, rather than requiring manual path auditing for every new project.

Explicit path declarations cover the project directory, any additional config files the agent needs, and any paths that fall outside the standard groups. The $HOME and $CWD variables are expanded at runtime, making profiles portable across machines and team members.

Profiles are version-controlled alongside the code they protect. That means sandbox policy gets the same review process as everything else: diffs in pull requests, history in git, and the ability to tighten or audit access over time.

Kernel sandboxing vs the alternatives

The choice of isolation mechanism involves real tradeoffs. Here is how kernel-level sandboxing compares to the approaches most teams reach for first.

Containers (Docker, Podman)

Containers are designed for service isolation, not single-process sandboxing. Running an agent in a container requires a daemon, an image, volume mount configuration, and typically a rebuild cycle when the agent needs access to a new path. Startup adds latency that matters in interactive coding sessions. Volume mounts are coarse: giving the container access to a project directory usually means giving it access to more than intended.

Containers also provide weaker isolation than their reputation suggests. A misconfigured volume mount, a privileged flag, or a container escape vulnerability can expose the host. They are useful for many things, but the overhead-to-isolation ratio is poor for sandboxing a single process.

Virtual machines

VMs provide strong isolation but at significant cost: boot time, memory overhead, and the operational burden of managing images. For interactive agent workflows, the round-trip to a VM for every file operation is impractical. VMs solve a different problem to what nono addresses.

Application-level sandboxes and policy filters

Some agent frameworks implement their own access controls in userspace, intercepting filesystem calls before they reach the OS. These approaches have a fundamental limitation: they are enforced by the same process they are meant to constrain. A bug in the agent, a crafted input, or an unexpected code path can bypass an application-level policy in ways that kernel enforcement cannot be bypassed.

Kernel-level sandboxing

nono's approach has a different tradeoff profile. Initialisation takes a few milliseconds. After that, enforcement is zero-overhead because it happens in the kernel, not in a monitoring process. There is no daemon, no image, no volume configuration. The agent runs as a normal process. The sandbox is simply a set of kernel rules that say which paths the process is allowed to touch.

The limitation worth being honest about: kernel sandboxing controls filesystem and network access but does not provide the memory isolation of a VM. For the threat model of a coding agent with excessive filesystem access, it is the right tool. For complete process isolation between mutually untrusted workloads, a VM remains appropriate.

Key Properties

Irrevocable

Once applied, the sandbox cannot be loosened. Only tightened.

Unprivileged

No root, no capabilities, no suid binaries required.

Inherited

Child processes automatically inherit the sandbox restrictions.

Zero Overhead

Kernel-level enforcement. No runtime performance cost after init.

Get started with nono

Runtime safety infrastructure that works on macOS, Linux, Windows, and in CI.