EN / FR
nono / architecture field guide

Deny everything.
Then open doors.

nono runs untrusted AI agents inside a kernel-enforced sandbox. Nothing is reachable unless a capability explicitly grants it; and once the walls are up, no process inside can take them down.

OS-enforced · Landlock + Seatbelt Irreversible once applied Policy-free core library
✕ ~/.ssh ✕ keychain ✕ browser data ✕ shell history ✕ open network AI agent pid 4242 · sandboxed ./workspace rw node runtime ro 127.0.0.1:8888 → proxy KERNEL-ENFORCED WALL
the mental model: walls by default, capabilities are the only doors
Color language, used in every diagram denied / blocked granted capability untrusted agent nono machinery OS kernel
01 · the problem

An AI agent is a very fast stranger with your shell

Coding agents read files, write files, run commands, and call the network; thousands of times per session, faster than you can review. Prompt injection, a poisoned dependency, or a plain bug can turn any of those actions hostile.

Application-level guardrails ("please don't touch ~/.ssh") live inside the same process they are guarding; anything that controls the agent controls the guardrail. nono moves the boundary down to the operating system: the kernel itself refuses the syscall. The agent doesn't behave because it was asked nicely; it behaves because denied is the default physical state of its world, and every granted path or port is a deliberate, auditable exception.

02 · sandboxing 101

What the kernel already offers

Both major desktop kernels ship a native way for a process to lock itself down before doing anything risky. nono is an orchestrator of these two primitives; it invents no enforcement of its own.

Landlock

Linux 5.13+ · LSM

A process builds a ruleset of filesystem (and, on newer kernels, network) permissions, then asks the kernel to restrict itself with it. From that syscall on, the rules apply to the process and every child it ever spawns.

  • Strictly an allow-list. Whatever the ruleset doesn't mention is unreachable. There is no way to say "allow this directory except that file".
  • Versioned ABI (v1-v6). Newer kernels add TCP connect/bind filtering per port (v4) and signal/IPC scoping (v6). nono detects the ABI at runtime.
  • Gaps on older kernels. Where Landlock can't filter the network, nono falls back to seccomp syscall interception; and if even that isn't possible, it refuses to run rather than run open.

Seatbelt

macOS · sandbox_init()

A process hands the kernel a profile, a small Scheme-like program that starts from (deny default) and enumerates exceptions. nono generates this profile from your capabilities and escapes every value that goes into it.

  • Deny-within-allow works. A profile can allow a directory yet still deny one file inside it; this is why some deny rules exist only on macOS.
  • Extensions. A supervisor can hand a running sandboxed process a token that widens access to one specific path; the mediated channel nono uses for interactive elevation.
  • Symlink surprises. /etc is really /private/etc; profiles must cover both faces of every alias, and nono's path canonicalization handles this.

The property everything else is built on: irreversibility

On both platforms, applying the sandbox is a one-way door. No API exists to widen a live sandbox from the inside; not for nono, not for the agent, not for code the agent tricks into running. Every rule is inherited by all descendants. This single kernel guarantee is what lets nono make promises that survive a fully compromised agent.

03 · the big picture

Four layers, one direction of trust

Intent flows down and gets more concrete at every step: flags become capabilities, capabilities become kernel rules. Nothing flows back up; a lower layer never asks an upper one for permission.

You Declare intent nono run --profile node-dev -- claude : a profile name, a few flags, a command to confine.
flags + profile → resolved policy
nono-cli Decide policy Owns every opinion: profiles, policy groups (deny lists, runtimes, system paths), packs, hooks, secrets, the network proxy, rollback UX, and the choice of execution strategy. All of it is data and code you can audit.
everything reduces to one CapabilitySet
nono (library) Build mechanism A pure sandbox primitive with zero built-in policy. It canonicalizes and validates every granted path, translates the CapabilitySet into a Landlock ruleset or a Seatbelt profile, and applies it. It grants exactly what it was told; nothing more, nothing implied.
ruleset / profile → one irreversible syscall
Kernel Enforce, forever Landlock or Seatbelt checks every file open, every connect, every signal, for the process and all of its descendants, until they exit. No userspace code is on this path.

Read the page bottom-up if you're evaluating guarantees (what can the kernel actually promise?), top-down if you're learning the codebase (where would my change go?).

04 · the load-bearing boundary

Mechanism never carries opinion

The single most structuring decision in nono: the library knows how to sandbox, only the CLI knows what to sandbox. If the library shipped even one built-in "sensible default", every embedder would inherit an invisible policy they never reviewed.

the library ships mechanism

nono

Deterministic, embeddable, policy-free. Everything here answers "how do I enforce exactly this?"

  • CapabilitySet · a builder for grants: paths (read / write, file or subtree), network mode, ports, Unix sockets, signal & IPC scoping, resource limits.
  • Sandbox · one entry point that turns a CapabilitySet into kernel state; the point of no return.
  • SandboxState · a serialized snapshot of the grants, re-validated on every load so stored state can't smuggle paths past canonicalization.
  • QueryContext · answers "would this path be allowed?" without applying anything; powers nono why.
  • keystore · resolves credential references into zeroized in-memory values.
  • undo · content-addressed snapshots for rollback: object store, Merkle tree, exclusion filters.
the cli ships policy

nono-cli

Opinionated, user-facing, replaceable. Everything here answers "what should be allowed, and how do I tell the human?"

  • Policy groups · ~40 named bundles (deny credentials, node runtime, git config, dangerous commands…) resolved per platform.
  • Profiles · built-in, user-authored, or pulled as signed packs; they compose groups, grants, network rules, hooks.
  • Execution strategies · Direct vs Supervised; fork, supervise, reap.
  • Proxy wiring · host filtering and credential injection around the sandboxed child.
  • Rollback lifecycle · when to snapshot, what to exclude, how to restore.
  • Diagnostics & UX · the "why did this fail" footer, suggested flags, learn-from-denials.
CapabilitySet is the contract

Every profile, flag, pack, and policy group; no matter how it was expressed; must reduce to this one explicit, inspectable value before anything is enforced. The C FFI exposes exactly this contract to other languages, and none of the CLI's opinions come with it.

05 · execution flow

The life of nono run

From keystroke to sandboxed agent in two phases: an unsandboxed decide phase that turns intent into a CapabilitySet, then a split: one process stays outside as supervisor, one goes inside forever.

decide · 1

Parse and load the profile

Flags are parsed; the named profile (built-in, user file, or installed pack) is loaded and merged with the command line.

decide · 2

Resolve policy into a CapabilitySet

Policy groups expand into concrete grants. Every path is canonicalized and must exist. A grant overlapping a deny rule, or touching nono's own state directory, is a fatal error; not a warning.

decide · 3

Stage the runtime services

Secrets are resolved from the keystore into memory. If network policy filters traffic, the proxy starts now and the CapabilitySet is narrowed to "loopback, one port". If rollback is on, a baseline snapshot of the workspace is taken.

decide · 4

Pick the strategy

Anything that needs a living parent; proxy, rollback, interactive elevation, diagnostics; selects Supervised. Otherwise Direct applies the sandbox in place and execs: nono vanishes and only the walls remain.

fork()

two processes now exist, with very different fates

Parent · the supervisor

stays unsandboxed
  1. Hardens itself instantly. Debugger attachment is blocked the moment the fork returns; if hardening fails, the child is killed and the run aborts. An unsandboxed process must not be an attachable one.
  2. Supervises. Reaps the child, forwards signals, relays the terminal, serves the proxy, and answers kernel notifications when the child touches something outside its grants; which can become an interactive "allow this?" prompt.
  3. Closes the loop. When the child exits: after-hooks run, an incremental rollback snapshot records what changed, and every observed denial becomes a diagnostic footer with the exact flag that would have allowed it.
  4. Exits with the child's code. To your shell, it was just the command.

Child · becomes the agent

sandboxed for life
  1. Connects back to the supervisor's socket; the one Unix socket it is allowed to reach.
  2. point of no returnApplies the sandbox. The CapabilitySet becomes a Landlock ruleset or Seatbelt profile, and the kernel enforces it. From this line on, not even this process's own code can widen its world.
  3. Becomes the agent. execve() replaces the child with the actual command. The agent starts life already inside the walls, inherits them into every subprocess, and cannot tell them to move.
Why sandbox in the child, not before the fork? The parent needs powers the sandbox would destroy: writing snapshots, running the proxy, prompting you. So the split comes first, and only the process that will become the agent walks through the one-way door.
06 · two ways to run

Direct or Supervised

One question decides everything: does anything need to keep living outside the sandbox after the agent starts?

$ nono wrap -- make build

Direct apply, then exec

The sandbox is applied to the current process, then exec replaces it with your command. nono ceases to exist; there is no supervisor, no extra process, no attack surface beyond the kernel rules themselves.

The trade: with nobody outside the walls, nothing that requires an outside can work.

minimal footprint zero runtime overhead proxy rollback elevation prompts diagnostic footer
$ nono run --profile node-dev -- claude

Supervised fork, then sandbox the child

The default for agent work. The parent stays outside as a hardened supervisor; only the child is confined. Everything interactive and forensic lives here: the network proxy, rollback snapshots, "allow this file?" prompts, and the post-run explanation of every denial.

The trade: one privileged process remains, so it defends itself (debugger-attachment blocked, hardening failure is fatal).

proxy + credential injection rollback elevation prompts diagnostics
07 · the workspace

Four crates, one kernel of truth

The Cargo workspace mirrors the architecture: everything depends on the nono library; the library depends on none of them.

nono-cli the product: policy, profiles, packs, strategies, UX nono-proxy network filter, credential injection, TLS intercept nono-ffi C ABI + generated header: embed the primitive anywhere nono the library: capabilities, sandbox, state, query, keystore, undo POLICY-FREE · DEPENDS ON NOTHING HERE runs in supervisor builds & applies re-exports the contract
arrows read "depends on"; trust and reuse both flow toward the library

This shape is a guarantee, not a convenience: because the library cannot see the CLI, no policy can leak downward into the primitive, and anyone embedding nono (via Rust or the C FFI) starts from a genuinely neutral mechanism.

08 · the supporting cast

Subsystems around the sandbox

The sandbox says no. These subsystems make that livable: recover from damage, reach the network safely, share policy, keep secrets secret, and understand every refusal.

undo / rollback

A time machine for the workspace

Before the agent runs, every tracked file's content is hashed into a content-addressed object store, and a Merkle tree pins the entire workspace under a single root hash. After the run, a second pass shows exactly what changed; and can put it all back, atomically.

go deeper

Content addressing means identical content is stored once, snapshots are cheap, and corruption is self-evident: re-hash an object and compare. The Merkle root turns "is anything different?" into one comparison.

Exclusions keep VCS directories (.git and friends) and user-configured globs out of tracking, so a restore never fights your version control. Editor atomic-save temp files are tracked separately so restores don't leave orphans behind.

The primitives (object store, Merkle tree, snapshot manager, exclusion filter) live in the library; the CLI owns the lifecycle: when to snapshot, how sessions are browsed, and the interactive restore.

network proxy

One door to the internet, with a guard

Under network policy, the sandbox allows exactly one destination: loopback, one port; where the proxy listens inside the supervisor. Every request the agent makes must walk past it.

go deeper

Host filtering: HTTPS tunnels are checked against your allowlist plus a built-in denylist (cloud metadata endpoints and similar). Corporate proxies can be chained behind it; nono's denylist still applies as a floor.

Credential injection: the agent calls a local route with no real secret; the proxy attaches the actual API key (or signs the request, e.g. AWS SigV4) on the way out. A leaked agent environment leaks nothing usable.

TLS interception, when domain-level rules require reading inside HTTPS, uses a session-scoped CA generated per run; trusted only by that sandbox, discarded afterwards.

packs & trust

Policy you can install, but not be fooled by

Profiles travel as packs pulled from a registry. Because a pack literally decides what an agent may touch, packs are signed and verified before use.

keylessthe default: Sigstore certificate transparency, verified against the public trusted root.
keyedyour own ECDSA P-256 key, for internal or air-gapped registries; no public infrastructure phoned.
unsignedchecksum only; requires an explicit --insecure flag and warns loudly.
go deeper

No silent downgrade: once a trusted key is configured, nothing (not a flag file, not an environment variable) can quietly drop verification; only the explicit insecure flag can, and it announces itself.

Trust is graded by origin: escape hatches like raw Seatbelt rules are honored only in profiles you wrote; any pack or registry profile has them stripped before use, because such a rule can grant anything.

secrets

Credentials as references, never values

A profile names secrets by reference; op://, keyring://, apple-password://, env:, file:, cmd:; resolved outside the sandbox at launch and injected as environment variables. No secret ever appears on a command line or in a log.

go deeper

In memory, values live in zeroizing wrappers: wiped when dropped, not left for a core dump. In output, every scheme has a redacted form, so diagnostics can mention which credential without ever showing it.

Combined with the proxy's injection mode, the agent can be given a placeholder while the real credential exists only inside the supervisor; the strongest posture for untrusted code, detailed below in the phantom token pattern.

secrets × proxy · the strongest posture

The phantom token pattern

Injecting a secret as an environment variable is good hygiene, but the secret still lives inside the sandbox; an agent that can read its own environment can leak it. The phantom token pattern removes the secret from the sandbox entirely: the agent holds a session-scoped decoy, and the swap for the real credential happens in the supervisor, on the way out.

SANDBOX AI agent BASE_URL = http://127.0.0.1:8888 API_KEY  = nono-tok-4f2a91… ↑ phantom · worthless outside this run :8888 SUPERVISOR · OUTSIDE THE WALLS op://vault/openai → sk-live-9xk2… 🔑 proxy validate phantom · swap · filter Authorization: nono-tok… Authorization: sk-live-9xk2… (real) api.openai.com evil.exfil.example direct connect → blocked by kernel host not in allowlist → refused by proxy
the real key exists only to the right of the wall; both exfiltration paths die, each at a different layer
1 · resolve outside

At launch, the supervisor resolves credential references (op://, keyring://…) into memory. The real key is born outside the walls and never crosses them.

2 · hand out a decoy

The agent's environment gets a phantom token and a loopback base URL. To the agent's SDK it looks exactly like a normal API key; it authenticates only to this session's proxy.

3 · swap at the door

On every request, the proxy validates the phantom, replaces it with the real credential, and forwards. It can even sign outbound requests (OAuth2 refresh, AWS SigV4) so no long-lived key exists on the wire at all.

4 · compromise is a shrug

Assume the worst: prompt injection wins, the agent dumps its whole environment to an attacker. They get a token that is invalid outside this session's loopback and dies when the run ends. There was nothing to steal.

diagnostics

Every "no" comes with an explanation

Denials aren't silent failures. The supervisor records each one, and when the run ends it prints a footer: what was blocked, why, and the exact flag or grant that would allow it next time. Iterating on a profile is a feedback loop, not archaeology.

go deeper

nono why answers permission questions without running anything, by replaying a saved capability state through the same resolution logic the sandbox used; grant-time and query-time can never disagree.

Learning from denials closes the loop: the grants a run was missing can be saved straight back into the profile you ran with.

c ffi

The primitive, embeddable anywhere

A C ABI with a generated header exposes the library's contract; build a capability set, apply the sandbox, query permissions, serialize state, read diagnostics; to any language that can call C.

go deeper

The surface is deliberately the mechanism half only: none of the CLI's profiles, packs, proxy, or policy groups cross the boundary. An embedder gets the same neutral primitive the CLI itself builds on, and brings their own policy.

09 · security invariants

Rules the codebase refuses to break

These are not guidelines; they are properties every change is checked against. If a feature can't be built without bending one, the feature loses.

Irreversible by design

No API widens a live sandbox. Expansion exists only through mediated channels: an interactive elevation prompt answered by the supervisor, or a macOS extension token for one specific path. Never silently.

Fail secure, always

If a rule can't be enforced exactly as asked; old kernel, missing feature, unloadable config; nono errors out instead of running weaker. There is no "best effort" mode for security.

Canonicalize at the boundary

Paths are resolved and existence-checked when granted, and re-validated whenever stored state is reloaded. Symlink swaps, alias faces like /etc vs /private/etc, and string-prefix tricks all die here.

Deny is the default state

A capability that isn't granted doesn't exist. Groups grant minimally, read and write are separate decisions, and nono's own state directory can never be granted; the agent cannot rewrite its own history or its jailer's memory.

Mechanism carries no policy

The library applies exactly the CapabilitySet it is given. Every opinion; deny lists, dangerous commands, profiles; lives in the CLI, where it is data you can read and audit.

The supervisor guards itself

The one process left outside the walls blocks debugger attachment the instant it forks, and treats failure to harden as fatal: kill the child, abort the run.

Secrets are references

Credentials resolve in memory, zeroize on drop, and appear in logs only in redacted form. With proxy injection, the real value never enters the sandbox at all.

Escape hatches are personal

Raw platform rules can grant anything, so they are honored only from profiles you authored yourself. Anything installed from a pack or registry gets them stripped before use.

10 · the whole model in one line
The CLI decides.
The library builds.
The kernel enforces, irreversibly.

Hold onto that, plus one image: an agent in a sealed room where every door was placed on purpose. Everything else in nono; profiles, packs, the proxy, rollback, diagnostics; exists either to choose those doors well, or to live gracefully with the walls.