Skip to content

Instantly share code, notes, and snippets.

@lukehinds
Created March 4, 2026 15:41
Show Gist options
  • Select an option

  • Save lukehinds/f7cab6393b00bb6d846dc55661b41ff9 to your computer and use it in GitHub Desktop.

Select an option

Save lukehinds/f7cab6393b00bb6d846dc55661b41ff9 to your computer and use it in GitHub Desktop.

RFC: Nonofile

Problem

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 like deny_credentials, python_runtime, system_read_macos are embedded in the binary. If a group has the wrong path for your system (e.g., Homebrew installed at /opt/homebrew instead of /usr/local/Homebrew), you cannot fix it without rebuilding nono.
  • never_grant list: Hardcoded paths that the supervisor will never grant access to. No override mechanism exists.
  • required: true groups: Cannot be removed from any profile, regardless of user need.
  • Network host groups (network-policy.json): The actual hostnames in llm_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.

Proposal

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.

Design

What Stays in the Binary

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_grant hardcoded list
  • ProtectedRoots hardcoded list

Nonofile

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

Syntax

  • 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 Reference

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 Patching

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/homebrew

The 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.

Discovery

When nono run is invoked:

  1. If --nonofile path is given, use that
  2. Otherwise, look for Nonofile in CWD, then walk up parent directories to repo root (.git)
  3. 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.

nono build

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

Example Nonofiles

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.

Community Nonofiles

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.

Replace never_grant with Warnings

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 --force flag needed
  • The default groups in the binary still deny these paths
  • Users who UNGROUP a deny group or GROUP_REMOVE a 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

What Gets Removed

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)

Summary

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

Migration

This is a breaking change. Users currently relying on --profile would need to:

  1. Copy the relevant example Nonofile to their project directory
  2. Apply any environment-specific path fixes with GROUP_ADD/GROUP_REMOVE
  3. Replace nono run --profile claude-code -- claude with nono run -- claude (Nonofile is auto-discovered)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment