> ## 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": "claude-code",
  "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>

<Tip>
  For an overview of what profiles are and how they compose with groups, see [Profiles & Groups](/docs/cli/features/profiles-groups).
</Tip>

## 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` profile is always included

Every profile automatically gets the `default` built-in merged in, even if you don't write `extends` at all. You don't need to add `"extends": "default"` — it's already there.

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.
</Note>

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

### 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 preset, pack, or user profile
* **`--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](/docs/cli/features/profiles-groups#groups) for the full group taxonomy and built-in group list.

<Note>
  The `groups.include` key was renamed from its former location under `security` in issue #594. The legacy key still deserializes with a deprecation warning; see `nono profile guide` for the full migration table. Legacy keys will be removed in v1.0.0.
</Note>

## 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. You must also grant access via `filesystem.allow`, `filesystem.read`, or `filesystem.write` (or the matching `*_file` variant) for the path to be accessible.
</Note>

### 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. `filesystem.ignore` is accepted as an alias,
but 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.

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

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

### 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": "claude-code",
  "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     |
