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.
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.
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.
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.
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.
/etc is really /private/etc; profiles must cover both faces of every alias, and nono's path canonicalization handles this.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.
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.
nono run --profile node-dev -- claude : a profile name, a few flags, a command to confine.
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?).
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.
Deterministic, embeddable, policy-free. Everything here answers "how do I enforce exactly this?"
nono why.Opinionated, user-facing, replaceable. Everything here answers "what should be allowed, and how do I tell the human?"
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.
nono runFrom 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.
Flags are parsed; the named profile (built-in, user file, or installed pack) is loaded and merged with the command line.
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.
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.
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.
two processes now exist, with very different fates
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.One question decides everything: does anything need to keep living outside the sandbox after the agent starts?
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.
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).
The Cargo workspace mirrors the architecture: everything depends on the nono library; the library depends on none of them.
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.
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.
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.
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.
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.
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.
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.
--insecure flag and warns loudly.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.
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.
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.
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.
At launch, the supervisor resolves credential references (op://, keyring://…) into memory. The real key is born outside the walls and never crosses them.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 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.
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.
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.
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.