nono profile command provides scaffolding and tooling for creating custom profiles. Instead of reverse-engineering the JSON structure from existing profiles, you can generate skeleton files, get editor autocomplete via JSON Schema, and access an LLM-oriented authoring guide.
File Format
Profile files use JSONC (JSON with Comments): standard JSON plus// line comments, /* */ block comments, and trailing commas. Both .json and .jsonc file extensions are accepted.
How extends Works
extends lets your profile build on top of an existing one. Instead of writing everything from scratch, you pick a base and only specify what you want to add or change.
default and adds one extra directory. The base profile is unchanged.
What happens when profiles are combined
When nono loads a profile that extends a base, it merges the two together. Most fields fall into one of two patterns: List fields grow. Things like allowed paths, denied commands, and security groups are combined — the base list and your list are joined together. Duplicates are removed automatically. You can always add to a list, but you cannot remove something a base already includes. Single-value fields can be overridden. Things likebinary, allow_gpu, workdir, and all security.* settings work differently: if you set them in your profile they replace the base value, and if you leave them out you inherit whatever the base had.
A few fields have their own specific rules:
Extending multiple profiles
You can extend more than one profile at once by passing an array:What if two bases share a common ancestor?
If you extend profiles A and B, and both of them extend a common base C, that’s fine. List entries from C end up in the final profile once — duplicates are removed automatically. For single-value fields, the same rightmost-wins rule applies: if A and B both set the same field, B’s value is what you get (assuming B is listed second in the array). The one thing to watch out for is if A and B intentionally set the same single-value field to different values, because only one can win and it might not be obvious which. If you’re unsure what you’re actually getting, runnono profile show my-profile to see the resolved result.
The default security groups are always included
Every profile automatically receives the groups included by the built-in default profile, even without an explicit extends. Other default fields are not implicitly inherited; use "extends": "default" when you want the complete base profile.
If you want to remove a group that default includes, use groups.exclude:
Checking what your profile actually resolves to
Inheritance can stack up. Use these commands to see the final result after all merging is done:Inheritance chains are capped at 10 levels. If you hit this limit, nono will fail with a
ProfileInheritance error — that’s usually a sign the profile structure needs simplifying.Generating a Profile
Usenono profile init to scaffold a new profile:
~/.config/nono/profiles/<name>.json. Use --force to overwrite an existing file.
Minimal Skeleton
A minimal skeleton includes the core sections most profiles need:Full Skeleton
With--full, additional sections are included as empty stubs for all additive fields:
Fields that would override inherited behavior are intentionally omitted from the skeleton:
network_profile (emitting null would clear an inherited proxy profile), open_urls (would replace inherited OAuth2 origins), allow_launch_services (would override inherited browser-opening permissions), and allow_gpu (would override inherited GPU access). Add these explicitly only when you intend to change the inherited behavior.Validation
Theinit command validates inputs before writing:
- Profile name must be alphanumeric with hyphens (no leading/trailing hyphens)
--extendstarget must exist as a built-in or user profile, or as a package profile addressed by its fully-qualified package name--groupsare checked against the embedded policy groups
JSON Schema
nono ships with a JSON Schema for profile files. Use it for editor autocomplete and validation.Exporting the Schema
Editor Integration
Export the schema locally, then add a$schema field to your profile for automatic validation in editors that support JSON Schema (VS Code, IntelliJ, Neovim with LSP, etc.):
.vscode/settings.json:
Authoring Guide
nono includes an embedded authoring guide designed for LLM agents assisting with profile creation:CLAUDE.md, AGENT.md, .cursorrules):
Workflow
A typical profile authoring workflow:-
Scaffold the profile:
-
Edit the generated file in your editor (with schema autocomplete):
-
Validate the profile:
-
Inspect the resolved profile (after inheritance and group expansion):
-
Compare against a baseline:
-
Test the profile:
-
Use the profile:
Available Groups
Usenono profile groups to list all available security groups. To see details for a specific group:
groups.include field. See Profiles & Groups for the full group taxonomy and built-in group list.
Process and IPC Isolation
Thesecurity section controls process-level isolation knobs that are not filesystem path grants:
Use
nono why --scope to inspect the effective scope policy for a profile:
Common Patterns
Agent with API Credentials
CI Build Environment
Override a Deny Rule
filesystem.bypass_protection only removes the deny rule. A matching
filesystem or Unix socket field must also grant the requested access. The
unix_socket* fields add their required filesystem grant automatically.Suppressing Repeated Save Suggestions
Usefilesystem.suppress_save_prompt for paths you intentionally do not want
to grant, but also do not want to see in the save-profile prompt on every run:
[save skipped], making
it clear why they are absent from the save prompt. It only suppresses
matching save-profile suggestions. The canonical name is deliberately
explicit so it is not mistaken for allowing the denied path.
The post-run save prompt offers the same behavior interactively: choose
suppress to save all listed denied-path suggestions here instead of adding
them as read, read_file, allow, or allow_file grants. The interactive
selector ignores input for its first second and discards queued keypresses
before enabling its controls, preventing input intended for the command that
just exited from immediately confirming the default grants.
Suppressing Expected macOS System-Service Diagnostics
Usediagnostics.suppress_system_services for macOS Seatbelt operations you
intentionally deny but do not want to see in every diagnostic footer:
user-preference-read (CFPreferences probes from npm and
similar tools) and forbidden-exec-sugid. This does not grant access and does
not change sandbox enforcement — it only hides matching violations from
post-run output and save-profile prompts. Values merge additively across
profile inheritance, like other list fields.
Redacting Extra Environment Variables from Diagnostics and Audit Records
nono redacts the values of well-known secret-bearing environment variables —OPENAI_API_KEY, GITHUB_TOKEN, and similar — wherever it writes command
context into diagnostics, session records, or the audit log.
A variable the profile itself marks as secret is redacted without being listed
anywhere. Two kinds of declaration count, and both are read straight out of
the profile:
- Denied variables. An exact name under
environment.deny_varsis stripped before the child starts, which says its value is a credential. Diagnostics and the audit log are written by the supervisor, which still sees the host environment, so a denied variable is exactly the kind that can be kept from the child and still printed in a ledger. - Credential destinations. The destination of an
env_credentialsmapping, of acommand_policies.credentialsentry, of anetwork.custom_credentialsroute, and theenv_varof acredential_routesentry.
local-socket credentials) and base_url_env_var; and glob entries in
deny_vars. A pattern like ACME_* is a blast radius for stripping — a
family denied for uniformity — not a claim that every member of it holds a
secret, so only the exact entries written alongside it are derived.
That covers what the profile declares. Anything else — a variable exported by
the host shell and deliberately passed through, or one injected by tooling
outside the profile — is not something nono can infer. List those under
diagnostics.redaction.extra_env_vars:
environment.deny_vars — * matches any
run of characters and is anchored to the whole variable name — and are matched
case-insensitively. Both features resolve patterns through one matcher, so the
same pattern text selects the same set of variable names in either place. That
is a guarantee about matching, not about policy: writing a pattern under
deny_vars does not redact it. Only an exact deny_vars name is derived as a
redaction, so a wildcard deny such as ACME_* whose matches should also be
scrubbed has to be repeated here.
Values merge additively across profile inheritance, so a child profile widens
the inherited list rather than replacing it.
This is output hygiene, not enforcement: it changes what nono writes, never
what the sandboxed process can read. To stop a variable from reaching the child
at all, use environment.deny_vars — which, when the entry is an exact name,
also redacts it per the derivation above. A wildcard deny, and a variable you
deliberately pass through, are the cases this key exists for.
The list is add-only. A profile can widen redaction but cannot stop a secure
default from being redacted; removing a default still requires
[redaction].unsafe_redaction_overrides in user config. Names added through
a profile are recorded in the audit log’s redaction-policy diff, so a record
documents how its own scrubbing differed from the default.
For path denials that should stay visible but skip save prompts, use
filesystem.suppress_save_prompt instead. To disable all diagnostic footers,
use --no-diagnostics.
Tuning How Many Network Denials Are Recorded
On Linux, when nono mediates network access with seccomp (proxy mode, orlinux.af_unix_mediation = "pathname"), every denied connection attempt can
produce an audit event. A process that retries a blocked destination in a tight
loop could otherwise grow the audit trail and diagnostics without limit, so
nono records denials individually up to a budget: 50 in a burst, then 20 per
second.
Denials past the budget are still denied. They are counted, and nono writes a
single summary audit event, for example “50 network syscall denials were
enforced but not individually recorded (audit rate limit exceeded)”, so a
suppressed denial is never silent. Allowed connections never consume the
budget.
Raise the budget when you need every denial in the audit trail, or lower it to
keep records small:
rate_per_sec up to 1000 and burst up to 10000. A value outside that range
makes the profile fail to load. The ceilings exist so no profile, including one
from a package or registry, can use this setting to exhaust memory.
Child profiles override these fields one at a time, so a profile that sets only
burst keeps the inherited rate_per_sec. The --network-denial-audit-rate
and --network-denial-audit-burst flags override the profile for a single run.
This is output hygiene, not enforcement: it changes how many denials nono
writes down, never which operations are allowed.
Exclude Inherited Groups
Target Binary
User profiles can declare abinary field to specify the program that nono should execute. This makes the trailing -- <command> optional:
binary and a CLI trailing command are provided, the profile binary takes precedence and a warning is emitted.
Restrictions
Thebinary field is only honoured for user-authored profiles — profiles loaded from a filesystem path (e.g. ./my-profile.jsonc) or from the user profile directory (~/.config/nono/profiles/). Pack and built-in profiles cannot set binary; the field is silently ignored for security reasons.
Inheritance
When profiles are composed viaextends, the child’s binary overrides the parent’s. If the child does not specify binary, it inherits the parent’s value.