Users frequently hit issues because a path unique to their environment isn't covered by a built-in profile. Today, fixing this requires a code change to policy.json, a new release, and users to upgrade. This feedback loop is too slow for something that's inherently environment-specific.
While nono does support some customization today — users can place a profile JSON file in ~/.config/nono/profiles/ to fully replace a built-in profile, pass --profile /path/to/custom.json for an arbitrary profile, use --allow/--read/--write CLI flags to add paths, and use profile inheritance via "extends" — the policy primitives themselves are not editable. Specifically:
- Group definitions (
policy.json): The actual paths inside groups likedeny_credentials,python_runtime,system_read_macosare embedded in the binary. If a group has the wrong path for your system (e.g., Homebrew installed at/opt/homebrewinstead of/usr/local/Homebrew), you cannot fix it without rebuilding nono. never_grantlist: Hardcoded paths that the supervisor will never grant access to. No override mechanism exists.required: truegroups: Cannot be removed from any profile, regardless of user need.- Network host groups (
network-policy.json): The actual hostnames inllm_apis,package_registries, etc. are embedded and immutable. ProtectedRoots: Hardcoded paths that can never be granted access to.
The profile override system lets you swap out which groups to use, but not what's inside those groups. That's the real pain point.
Replace the current profile system with a single Nonofile — a Dockerfile-inspired declarative format that lives in the project directory. The Nonofile is the only place sandbox configuration lives. It composes from built-in groups (which remain embedded in the binary as defaults) but can also patch, extend, or override those groups inline.
No ~/.config/nono/profiles/. No ~/.config/nono/policy.json. One file, one location, fully self-contained.
The binary continues to embed policy.json and network-policy.json, but strictly as primitive libraries — reusable building blocks, not application configs:
- Security groups:
deny_credentials,python_runtime,rust_runtime,system_read_macos,homebrew, etc. - Network host groups:
llm_apis,package_registries,github,sigstore, etc. - Sensitive path list: Used for warnings (not hard blocks — see below).
Removed from the binary:
- All application profiles (
claude-code,opencode,openclaw, etc.) - Application network profiles (
developer,claude-code, etc.) never_granthardcoded listProtectedRootshardcoded list
A Dockerfile-inspired declarative format that lives in a project's root directory.
# Nonofile — sandbox configuration for this project
# ── Security Groups ──────────────────────────────────────────────
GROUP system_read_macos
GROUP system_read_linux
GROUP system_write_macos
GROUP system_write_linux
GROUP python_runtime
GROUP rust_runtime
GROUP homebrew
# Patch a built-in group — add paths missing for your system
GROUP_ADD python_runtime READ /opt/weird-python/lib
GROUP_ADD python_runtime READ /opt/weird-python/bin
GROUP_ADD homebrew READ /opt/homebrew
# Remove a path from a built-in group
GROUP_REMOVE system_read_macos READ /usr/local/Homebrew
# Remove an entire default deny group (triggers a warning)
UNGROUP deny_shell_configs
# ── Filesystem ───────────────────────────────────────────────────
ALLOW $HOME/.local/share/myapp
READ /usr/local/share/mylib
WRITE /tmp/build-output
ALLOW_FILE ~/.config/myapp/settings.json
READ_FILE /etc/myapp.conf
# ── Working Directory ────────────────────────────────────────────
WORKDIR readwrite
# ── Execution ────────────────────────────────────────────────────
# interactive: preserve TTY for apps with interactive UIs (claude, vim, htop)
# monitored: nono monitors output, prints diagnostics on failure (default)
EXEC interactive
# Enable supervised mode for capability expansion approval
SUPERVISED on
# ── Network ──────────────────────────────────────────────────────
NETWORK_GROUP llm_apis
NETWORK_GROUP package_registries
NETWORK_ALLOW api.example.com
NETWORK_ALLOW *.internal.corp
# Allow the sandboxed process to listen on specific ports
ALLOW_BIND 8080
ALLOW_BIND 3000
# ── Proxy & Credentials ─────────────────────────────────────────
# Inject credentials via reverse proxy (credential never reaches the sandbox)
PROXY_CREDENTIAL anthropic
PROXY_CREDENTIAL openai
# Chain through a corporate proxy
EXTERNAL_PROXY squid.corp.internal:3128
# Load credentials into env vars from system keystore
ENV_CREDENTIAL ANTHROPIC_API_KEY anthropic-api-key
# ── Commands ─────────────────────────────────────────────────────
ALLOW_COMMAND cargo
BLOCK_COMMAND rm
# ── Rollback & Snapshots ────────────────────────────────────────
ROLLBACK on
ROLLBACK_STORE $HOME/.local/share/nono/snapshots
EXCLUDE target/
EXCLUDE node_modules/
INCLUDE vendor/
# ── Audit ────────────────────────────────────────────────────────
AUDIT on
AUDIT_STORE $HOME/.local/share/nono/audit
# ── Trust & Attestation ─────────────────────────────────────────
# Require trust verification for instruction files (SKILLS.md, CLAUDE.md, etc.)
TRUST on
# Trust policy file location (default: auto-discover trust-policy.json)
TRUST_POLICY ./trust-policy.json- One directive per line
#starts a comment (full line or trailing)- Blank lines ignored
- Variable expansion:
$HOME,$WORKDIR,$TMPDIR,$XDG_CONFIG_HOME,$XDG_DATA_HOME,$UID,~/ - Unknown directives are a hard error (fail-closed)
- No quoting needed — arguments are the rest of the line after the directive keyword
| Directive | Arguments | Effect |
|---|---|---|
| Groups | ||
GROUP |
name | Include a built-in security group |
UNGROUP |
name | Remove a default deny group (warns if sensitive) |
GROUP_ADD |
group access path | Add a path to a built-in group (READ, WRITE, or READWRITE) |
GROUP_REMOVE |
group access path | Remove a path from a built-in group |
| Filesystem | ||
ALLOW |
path | Read+write directory access |
READ |
path | Read-only directory access |
WRITE |
path | Write-only directory access |
ALLOW_FILE |
path | Read+write single file |
READ_FILE |
path | Read-only single file |
WRITE_FILE |
path | Write-only single file |
WORKDIR |
none|read|write|readwrite | Working directory access level |
| Execution | ||
EXEC |
interactive|monitored | Execution mode: interactive preserves TTY, monitored captures output (default) |
SUPERVISED |
on|off | Enable supervised mode for capability expansion approval |
| Network | ||
NETWORK_GROUP |
name | Include a network host group from network-policy.json |
NETWORK_ALLOW |
host | Allow an additional network host (exact or *.suffix) |
ALLOW_BIND |
port | Allow the sandboxed process to listen on a TCP port |
| Proxy & Credentials | ||
PROXY_CREDENTIAL |
service | Inject credentials via reverse proxy (e.g., anthropic, openai) |
EXTERNAL_PROXY |
host:port | Chain through a corporate/enterprise proxy |
ENV_CREDENTIAL |
envvar account | Load credential from system keystore into env var |
| Commands | ||
ALLOW_COMMAND |
command | Allow a normally-blocked command |
BLOCK_COMMAND |
command | Block a command |
| Rollback & Snapshots | ||
ROLLBACK |
on|off | Enable or disable rollback snapshots for the session |
ROLLBACK_STORE |
path | Directory for snapshot storage (default: platform state dir) |
EXCLUDE |
pattern | Exclude pattern from rollback snapshots |
INCLUDE |
dir-name | Force-include a directory that would otherwise be auto-excluded |
| Audit | ||
AUDIT |
on|off | Enable or disable audit trail for the session |
AUDIT_STORE |
path | Directory for audit log storage (default: platform state dir) |
| Trust & Attestation | ||
TRUST |
on|off | Enable or disable trust verification for instruction files |
TRUST_POLICY |
path | Path to trust-policy.json (default: auto-discover) |
GROUP_ADD and GROUP_REMOVE allow modifying the contents of a built-in group without replacing it entirely. This is the key mechanism for fixing path issues:
# The built-in python_runtime group doesn't know about your Python install
GROUP python_runtime
GROUP_ADD python_runtime READ /opt/custom-python/lib
GROUP_ADD python_runtime READWRITE /opt/custom-python/site-packages
# The built-in homebrew group points to the wrong location
GROUP homebrew
GROUP_REMOVE homebrew READ /usr/local/Homebrew
GROUP_ADD homebrew READ /opt/homebrewThe group is loaded from the embedded policy.json, then the patches are applied before sandbox resolution. GROUP_ADD/GROUP_REMOVE without a corresponding GROUP directive is an error — you must include the group to patch it.
When nono run is invoked:
- If
--nonofile pathis given, use that - Otherwise, look for
Nonofilein CWD, then walk up parent directories to repo root (.git) - If no Nonofile found, apply only the base deny groups + CLI flags
CLI flags (--allow, --read, etc.) are always additive on top of the Nonofile.
Validates a Nonofile and shows what sandbox it would produce:
nono build # Validate Nonofile in CWD, show resolved capabilities
nono build --nonofile path # Validate a specific Nonofile
nono build --dry-run # Show the Seatbelt/Landlock profile that would be generated
nono build --json # Output resolved config as JSON
The current application profiles become example Nonofiles shipped in the repo as reference:
examples/nonofiles/
claude-code.nonofile
opencode.nonofile
python-dev.nonofile
node-dev.nonofile
rust-dev.nonofile
go-dev.nonofile
Users copy the relevant example to their project directory as Nonofile and customize it for their environment.
We welcome community contributions of Nonofiles for applications, frameworks, and workflows. Contributed Nonofiles live in the same examples/nonofiles/ directory alongside the built-in examples and are reviewed via standard pull requests.
In the future, we may extend this to a hosted registry where users can publish and pull Nonofiles (similar to Docker Hub), but for now the repository is the single source.
Currently, never_grant is a hardcoded list of paths (e.g., ~/.ssh, ~/.gnupg, /etc/sudoers) that the supervisor will never grant access to, regardless of user approval. This is being removed.
Rationale: Users have legitimate reasons to access these paths — security engineers testing SSH key handling, backup tools reading ~/.gnupg, credential managers, etc. A hardcoded block with no override contradicts the principle that users own their machines.
Replacement: Sensitive path access triggers a warning at sandbox apply time:
warning: sensitive path accessible in this sandbox
~/.ssh (credentials) — not covered by deny_credentials
~/.gnupg (credentials) — not covered by deny_credentials
This is allowed but increases risk. To suppress: --quiet
- No error, no confirmation prompt, no
--forceflag needed - The default groups in the binary still deny these paths
- Users who
UNGROUPa deny group orGROUP_REMOVEa sensitive path see the warning - The warning can be suppressed with
--quiet - The sensitive path list itself is defined in policy.json (embedded) and is used only for warning generation
| Removed | Replacement |
|---|---|
--profile CLI flag |
Nonofile or raw CLI flags |
~/.config/nono/profiles/ directory |
Nonofile per project |
| Application profiles in policy.json | Example Nonofiles in repo |
| Application network profiles in network-policy.json | NETWORK_GROUP in Nonofile |
never_grant hardcoded list |
Warn-on-sensitive-access |
NeverGrantChecker in supervisor IPC |
SensitivePathWarner (warns, doesn't block) |
ProtectedRoots hardcoded list |
Covered by default deny groups + warnings |
required: true on groups |
All groups can be removed (with warning for sensitive ones) |
| Before | After |
|---|---|
| Policy primitives embedded, not editable | Primitives embedded as defaults, patchable via GROUP_ADD/GROUP_REMOVE |
| Application profiles shipped in binary | Example Nonofiles in repo, users copy + customize |
Config split across binary + ~/.config/nono/ |
Single Nonofile per project, nothing in ~/.config/nono/ |
never_grant blocks access unconditionally |
Warnings on sensitive path access, user can override |
| Wrong path in a group requires new release | User patches the group inline in their Nonofile |
This is a breaking change. Users currently relying on --profile would need to:
- Copy the relevant example Nonofile to their project directory
- Apply any environment-specific path fixes with
GROUP_ADD/GROUP_REMOVE - Replace
nono run --profile claude-code -- claudewithnono run -- claude(Nonofile is auto-discovered)