> ## Documentation Index
> Fetch the complete documentation index at: https://nono.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Profile Authoring

> Scaffolding, schema validation, and tooling for creating custom profiles

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

```jsonc theme={null}
{
  // Extend the official Claude Code profile
  "extends": "nolabs-ai/claude",
  "meta": {
    "name": "my-claude",
    "description": "Custom Claude profile with extra tooling access",
  },
  "filesystem": {
    "allow": ["/opt/my-tools"], // custom tooling directory
    "read": ["/etc/my-app"],
  },
}
```

> [!TIP]
>
> When both `my-profile.jsonc` and `my-profile.json` exist, the `.jsonc` file takes priority.

> [!TIP]
>
> For an overview of what profiles are and how they compose with groups, see [Profiles & Groups](https://nono.sh/docs/cli/features/profiles-groups.md).

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

```json theme={null}
{
  "extends": "default",
  "filesystem": {
    "allow": ["/opt/my-tools"]
  }
}
```

This profile inherits everything from `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 like `binary`, `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:

| Field | How it works |
| - | - |
| `network.block` | Sticks once set. If any base sets this to `true`, your profile cannot set it back to `false`. |
| `open_urls` | Your value replaces the base entirely. Leave it out to inherit the base value. |
| `network.credentials` | Leave it out to inherit. Set it to `[]` to explicitly disable inherited credentials. Otherwise your list is combined with the base. |
| `network.network_profile` | Leave it out to inherit. Set it to `null` to explicitly clear the inherited proxy profile. |
| `hooks`, `env_credentials`, `environment.set_vars` | Merged as key-value maps. Your keys win over the base on any conflict. |

### Extending multiple profiles

You can extend more than one profile at once by passing an array:

```json theme={null}
{ "extends": ["team-base", "python-tools"] }
```

The profiles are merged left-to-right, then your profile is applied on top. So for any single-value field where the bases conflict, the **rightmost** one wins. List fields are always combined from all of them.

```
team-base  →  python-tools  →  your profile
                              (your settings win)
```

### 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, run `nono 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`:

```json theme={null}
{
  "groups": {
    "exclude": ["dangerous_commands"]
  }
}
```

### Checking what your profile actually resolves to

Inheritance can stack up. Use these commands to see the final result after all merging is done:

```bash theme={null}
# Show the fully resolved profile
nono profile show my-profile

# See exactly what changed compared to default
nono profile diff default my-profile
```

> [!NOTE]
>
> 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

Use `nono profile init` to scaffold a new profile:

```bash theme={null}
# Minimal skeleton
nono profile init my-agent

# With inheritance and groups
nono profile init my-agent --extends default --groups deny_credentials

# With a description
nono profile init my-agent --extends default --description "Profile for my agent"

# Full skeleton with all sections
nono profile init my-agent --full

# Output to a specific path instead of ~/.config/nono/profiles/
nono profile init my-agent --output ./my-agent.json
```

By default, the profile is written to `~/.config/nono/profiles/<name>.json`. Use `--force` to overwrite an existing file.

### Minimal Skeleton

A minimal skeleton includes the core sections most profiles need:

```json theme={null}
{
  "extends": "default",
  "meta": {
    "name": "my-agent",
    "description": "Profile for my agent"
  },
  "groups": {
    "include": [
      "deny_credentials"
    ]
  },
  "workdir": {
    "access": "readwrite"
  },
  "filesystem": {
    "allow": [],
    "read": []
  }
}
```

### Full Skeleton

With `--full`, additional sections are included as empty stubs for all additive fields:

```json theme={null}
{
  "meta": {
    "name": "my-agent"
  },
  "groups": {
    "include": [],
    "exclude": []
  },
  "commands": {
    "allow": [],
    "deny": []
  },
  "workdir": {
    "access": "readwrite"
  },
  "filesystem": {
    "allow": [],
    "read": [],
    "write": [],
    "allow_file": [],
    "read_file": [],
    "write_file": [],
    "unix_socket": [],
    "unix_socket_bind": [],
    "unix_socket_dir": [],
    "unix_socket_dir_bind": [],
    "unix_socket_subtree": [],
    "unix_socket_subtree_bind": [],
    "deny": [],
    "bypass_protection": [],
    "suppress_save_prompt": []
  },
  "security": {},
  "network": {
    "block": false,
    "allow_domain": [],
    "credentials": [],
    "open_port": [],
    "listen_port": [],
    "no_proxy": [],
    "custom_credentials": {}
  },
  "env_credentials": {},
  "environment": {
    "deny_vars": [],
    "set_vars": {}
  },
  "hooks": {},
  "rollback": {
    "exclude_patterns": [],
    "exclude_globs": []
  },
  "diagnostics": {
    "suppress_system_services": []
  }
}
```

> [!NOTE]
>
> 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

The `init` command validates inputs before writing:

* **Profile name** must be alphanumeric with hyphens (no leading/trailing hyphens)
* **`--extends`** target must exist as a built-in or user profile, or as a package profile addressed by its fully-qualified package name
* **`--groups`** are checked against the embedded policy groups

After creating a profile, validate it:

```bash theme={null}
nono profile validate ~/.config/nono/profiles/my-agent.json
```

## JSON Schema

nono ships with a JSON Schema for profile files. Use it for editor autocomplete and validation.

### Exporting the Schema

```bash theme={null}
# Print to stdout
nono profile schema

# Write to a file
nono profile schema --output nono-profile.schema.json
```

### 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.):

```json theme={null}
{
  "$schema": "./nono-profile.schema.json",
  "meta": {
    "name": "my-agent"
  }
}
```

In VS Code, you can also configure schema association in `.vscode/settings.json`:

```json theme={null}
{
  "json.schemas": [
    {
      "fileMatch": ["**/profiles/*.json", "**/profiles/*.jsonc"],
      "url": "./nono-profile.schema.json"
    }
  ]
}
```

## Authoring Guide

nono includes an embedded authoring guide designed for LLM agents assisting with profile creation:

```bash theme={null}
nono profile guide
```

This outputs a comprehensive reference covering every profile section, field descriptions, common patterns, variable expansion, and validation workflow. It is useful when asking an LLM to help you write a profile -- pipe or paste the guide into your conversation for context.

To make the guide automatically available to your coding agent, add a line to your project's instruction file (e.g., `CLAUDE.md`, `AGENT.md`, `.cursorrules`):

```md theme={null}
Should you need to work with nono profiles or policy, you can retrieve context using the `nono profile guide` command
```

## Workflow

A typical profile authoring workflow:

1. **Scaffold** the profile:
   ```bash theme={null}
   nono profile init my-agent --extends default --groups deny_credentials --full
   ```

2. **Edit** the generated file in your editor (with schema autocomplete):
   ```bash theme={null}
   $EDITOR ~/.config/nono/profiles/my-agent.json
   ```

3. **Validate** the profile:
   ```bash theme={null}
   nono profile validate ~/.config/nono/profiles/my-agent.json
   ```

4. **Inspect** the resolved profile (after inheritance and group expansion):
   ```bash theme={null}
   nono profile show my-agent
   ```

5. **Compare** against a baseline:
   ```bash theme={null}
   nono profile diff default my-agent
   ```

6. **Test** the profile:
   ```bash theme={null}
   nono run --profile my-agent --dry-run -- my-command
   ```

7. **Use** the profile:
   ```bash theme={null}
   nono run --profile my-agent -- my-command
   ```

## Available Groups

Use `nono profile groups` to list all available security groups. To see details for a specific group:

```bash theme={null}
nono profile groups deny_credentials
```

Groups are referenced by name in the `groups.include` field. See [Profiles & Groups](https://nono.sh/docs/cli/features/profiles-groups.md#groups) for the full group taxonomy and built-in group list.

## Process and IPC Isolation

The `security` section controls process-level isolation knobs that are not filesystem path grants:

```json theme={null}
{
  "security": {
    "signal_mode": "isolated",
    "process_info_mode": "isolated",
    "ipc_mode": "shared_memory_only"
  }
}
```

| Field | Values | Description |
| - | - | - |
| `signal_mode` | `isolated`, `allow_same_sandbox`, `allow_all` | Controls which processes the sandboxed command may signal. On Linux V6, restricted modes use Landlock signal scoping when available. |
| `process_info_mode` | `isolated`, `allow_same_sandbox`, `allow_all` | Controls visibility of other process metadata. |
| `ipc_mode` | `shared_memory_only`, `full` | Controls IPC compatibility. On Linux V6, `shared_memory_only` requests abstract UNIX socket scoping; `full` leaves abstract UNIX sockets unscoped for runtimes that require broader IPC compatibility. |

Use `nono why --scope` to inspect the effective scope policy for a profile:

```bash theme={null}
nono why --scope signal --profile my-agent
nono why --scope abstract-unix-socket --profile my-agent
```

## Common Patterns

### Agent with API Credentials

```json theme={null}
{
  "extends": "default",
  "meta": {
    "name": "api-agent",
    "description": "Agent with API access via credential injection"
  },
  "workdir": { "access": "readwrite" },
  "env_credentials": {
    "openai_api_key": "OPENAI_API_KEY"
  },
  "network": {
    "network_profile": "standard"
  }
}
```

### CI Build Environment

```json theme={null}
{
  "meta": {
    "name": "ci-build",
    "description": "Locked-down CI environment"
  },
  "groups": {
    "include": ["deny_credentials"]
  },
  "workdir": { "access": "readwrite" },
  "network": { "block": true }
}
```

### Override a Deny Rule

```json theme={null}
{
  "extends": "default",
  "meta": {
    "name": "docker-agent",
    "description": "Agent that needs Docker access"
  },
  "workdir": { "access": "readwrite" },
  "filesystem": {
    "allow": ["$HOME/.docker"],
    "bypass_protection": ["$HOME/.docker"]
  }
}
```

> [!NOTE]
>
> `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

Use `filesystem.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:

```json theme={null}
{
  "filesystem": {
    "suppress_save_prompt": ["$HOME/.copilot/settings.json"]
  }
}
```

This does not grant access and does not hide diagnostics. Suppressed denials
still appear in the diagnostic footer annotated with `[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

Use `diagnostics.suppress_system_services` for macOS Seatbelt operations you
intentionally deny but do not want to see in every diagnostic footer:

```json theme={null}
{
  "diagnostics": {
    "suppress_system_services": ["user-preference-read"]
  }
}
```

Common values include `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_vars` is
  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_credentials`
  mapping, of a `command_policies.credentials` entry, of a
  `network.custom_credentials` route, and the `env_var` of a
  `credential_routes` entry.

Declaring a variable as a credential is what makes its name secret, so
repeating it under a redaction key would only create an opportunity to typo or
forget it.

Two things are deliberately left readable, because their values are not
secrets and a ledger is worth less without them: socket paths
(`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`:

```json theme={null}
{
  "diagnostics": {
    "redaction": {
      "extra_env_vars": ["DEPLOY_TOKEN", "ACME_*", "*_SECRET"]
    }
  }
}
```

Entries use the same glob syntax as `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, or
`linux.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:

```json theme={null}
{
  "diagnostics": {
    "network_denial_audit": {
      "rate_per_sec": 100,
      "burst": 500
    }
  }
}
```

Both fields are optional and must be whole numbers from 1 up to a ceiling:
`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

```json theme={null}
{
  "extends": "default",
  "meta": {
    "name": "permissive-agent",
    "description": "Agent without deprecated startup-only command gating"
  },
  "workdir": { "access": "readwrite" },
  "groups": {
    "exclude": [
      "dangerous_commands",
      "dangerous_commands_macos",
      "dangerous_commands_linux"
    ]
  }
}
```

## Target Binary

User profiles can declare a `binary` field to specify the program that nono should execute. This makes the trailing `-- <command>` optional:

```jsonc theme={null}
{
  "extends": "nolabs-ai/claude",
  "meta": { "name": "my-claude" },
  "binary": "/home/user/.local/bin/claude"
}
```

```bash theme={null}
# No trailing command needed — the profile knows what to run
nono run --profile ./my-claude.jsonc --allow-cwd
```

If both a profile `binary` and a CLI trailing command are provided, the profile binary takes precedence and a warning is emitted.

### Restrictions

The `binary` 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 via `extends`, the child's `binary` overrides the parent's. If the child does not specify `binary`, it inherits the parent's value.

## CLI Reference

| Command | Description |
| - | - |
| `nono profile init <name>` | Generate a skeleton profile |
| `nono profile init <name> --extends <base>` | Inherit from a base profile |
| `nono profile init <name> --groups <g1>,<g2>` | Pre-populate security groups |
| `nono profile init <name> --full` | Include all optional sections |
| `nono profile init <name> --output <path>` | Write to a specific file |
| `nono profile init <name> --force` | Overwrite existing file |
| `nono profile init <name> --description <text>` | Set the profile description |
| `nono profile schema` | Output JSON Schema to stdout |
| `nono profile schema --output <path>` | Write JSON Schema to a file |
| `nono profile guide` | Print the authoring guide |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.