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

# Profiles & Groups

> Pre-configured capability sets and composable security groups

Profiles are pre-configured capability sets that define what a sandboxed process can access. Groups are the composable building blocks that profiles are made from. Together they codify security policy so you don't have to specify flags manually every time.

> [!TIP]
>
> Use `nono profile` to inspect, compare, and validate profiles and groups from the command line. See [Profile Introspection](https://nono.sh/docs/cli/features/profile-introspection.md) for details.

## Profiles

### Why Profiles?

Manually specifying capabilities for every tool is tedious and error-prone:

```bash theme={null}
# Without profiles - verbose and easy to misconfigure
nono run --allow-cwd --read ~/.claude --read-file ~/.claude/config.json -- claude
```

Profiles simplify this:

```bash theme={null}
# With profiles - concise and auditable
nono run --profile nolabs-ai/claude -- claude
```

### Predefined Profiles vs User Profiles

Predefined profiles (shipped with nono or installed via packs) cover two things:

* **Agent packs** — profiles for specific AI coding agents (Claude Code, Codex, opencode, openclaw). These grant the exact paths each agent needs to run, including its config directory, credential stores, and runtime group.
* **Language runtimes** — profiles for common developer workflows (node-dev, python-dev, go-dev, rust-dev). These include the appropriate runtime group and a developer network profile.

Predefined profiles are intentionally narrow. They do not encode environment-specific paths because those vary by OS distribution, package manager, and installation method. A `java-dev` profile cannot know whether your JDK config lives in `/etc/java-openjdk`, `/etc/java21-openjdk`, or somewhere else entirely — that depends on your system.

**Environment-specific additions belong in a user profile.** Write a profile that extends the closest predefined profile and adds only what your environment requires:

```json theme={null}
{
  "meta": {
    "name": "java-dev-local",
    "version": "1.0.0",
    "description": "Java / Maven profile for this machine's JDK install"
  },
  "extends": "java-dev",
  "filesystem": {
    "read": [
      "/etc/java21-openjdk"
    ]
  },
  "network": {
    "allow_domain": [
      "repo.maven.apache.org",
      "oss.sonatype.org"
    ]
  }
}
```

Save this to `~/.config/nono/profiles/java-dev-local.json` and use it with:

```bash theme={null}
nono run --profile java-dev-local -- mvn package
```

This keeps the predefined profile stable and auditable while giving you full control over your local environment's requirements. See [Creating User Profiles](#creating-user-profiles) below for the full workflow.

### Profile Sources

Profiles can come from three sources:

| Source | Location | Trust Level |
| - | - | - |
| Built-in profiles | Compiled into nono | Base - audited defaults |
| Package profiles | Installed packs, addressed as `<namespace>/<package>` | Base - signed package defaults |
| User profiles | `~/.config/nono/profiles/*.json` or `*.jsonc` | User-defined |

Bare names resolve user profiles first and built-ins second, so a user profile can override a built-in with the same name. Package profiles occupy a separate fully-qualified namespace and cannot be shadowed by a bare user profile. CLI flags are applied after the selected profile and always take precedence.

> [!NOTE]
>
> nono resolves config paths via `$XDG_CONFIG_HOME/nono/` on every platform. If
> you have not set `XDG_CONFIG_HOME` — typical on macOS — that is the same as
> **`~/.config/nono/`** shown above. Set `XDG_CONFIG_HOME` only when you want
> config elsewhere; profile JSON path grants should use `$XDG_CONFIG_HOME`,
> `$NONO_CONFIG`, or `$NONO_PACKAGES` rather than hardcoded `$HOME/.config/...`.

### Profile Format

Profiles use JSON format:

```json theme={null}
{
  "meta": {
    "name": "my-agent",
    "version": "1.0.0",
    "description": "Profile for my custom agent"
  },
  "workdir": {
    "access": "readwrite"
  },
  "groups": {
    "include": ["node_runtime", "python_runtime", "git_config"],
    "exclude": []
  },
  "commands": {
    "allow": [],
    "deny": []
  },
  "filesystem": {
    "allow": ["$HOME/.config/my-agent"],
    "read": [],
    "write": [],
    "allow_file": [],
    "read_file": [],
    "write_file": [],
    "deny": [],
    "bypass_protection": [],
    "suppress_save_prompt": [],
    "unix_socket": [],
    "unix_socket_bind": [],
    "unix_socket_dir": [],
    "unix_socket_dir_bind": [],
    "unix_socket_subtree": [],
    "unix_socket_subtree_bind": []
  },
  "network": {
    "block": false
  },
  "session_hooks": {
    "before": {
      "script": "/path/to/setup.sh",
      "timeout_secs": 30
    },
    "after": {
      "script": "/path/to/cleanup.sh",
      "timeout_secs": 10
    }
  }
}
```

## Session Hooks

Session hooks are scripts that run before or after the sandboxed process, outside the sandbox with host privileges. This is the mechanism for custom instrumentation around a run — for example, replacing an external wrapper script that sets up or tears down per-session state. There are exactly two events, `before` and `after`; there is no mid-session or per-tool-call hook here. (That's a different, unrelated feature: see [Hooks](#hooks-pack-declared-legacy-profile-field) below, which packs use to install an *agent's own* hook system, e.g. Claude Code's tool-call hooks.)

### Configuration

Both `before` and `after` are optional. Each hook specifies a script path and an optional timeout:

```json theme={null}
{
  "session_hooks": {
    "before": { "script": "/path/to/setup.sh", "timeout_secs": 30 },
    "after":  { "script": "/path/to/cleanup.sh", "timeout_secs": 10 }
  }
}
```

* `script` (required): absolute path to an executable script. It must resolve to a regular file, be owned by you or root, and not sit in a world-writable directory — this rejects `/tmp` and similar shared directories even when the sticky bit is set. Put session hook scripts somewhere private, e.g. under `$XDG_CONFIG_HOME/nono/hooks/`.
* `timeout_secs` (optional): kills the whole script process group if it exceeds this duration.

Both hook types also receive `NONO_HOOK_TYPE` (`before` or `after`), in addition to the variables listed below.

> [!NOTE]
>
> **Hooks fail open.** An invalid script path, a non-zero exit, or a timeout never blocks the sandboxed process — nono logs a warning and continues. A timeout discards any environment variables the before-hook had already written; a plain non-zero exit does not — variables written before `exit` still take effect.

### Before hook

Runs before the sandboxed process starts. The script receives `NONO_SESSION_ID`, `NONO_WORKDIR`, and `NONO_ENV_FILE` -- a temporary file where `KEY=VALUE` lines (blank lines and `#` comments are skipped) can be written to export environment variables to the sandboxed process.

```sh theme={null}
#!/bin/sh
mkdir -p "/tmp/$NONO_SESSION_ID"
echo "TMPDIR=/tmp/$NONO_SESSION_ID" >> "$NONO_ENV_FILE"
```

A small set of dangerous variables (e.g. `LD_PRELOAD`) is filtered out of the before-hook's exports before they reach the sandboxed process, regardless of what the script writes to `NONO_ENV_FILE`.

### After hook

Runs after the sandboxed process exits. Receives `NONO_SESSION_ID`, `NONO_WORKDIR`, and `NONO_EXIT_CODE`. Cleanup only -- does not export environment variables.

```sh theme={null}
#!/bin/sh
rm -rf "/tmp/$NONO_SESSION_ID"
```

## AF\_UNIX Socket Grants

The `unix_socket*` filesystem fields grant `connect(2)` (and optionally
`bind(2)`) on pathname AF\_UNIX sockets. Abstract-namespace and unnamed
sockets are never grantable — only filesystem-backed socket paths.

| Field | Grants | Implied fs grant |
| - | - | - |
| `unix_socket` | `connect` only, single socket file | Read on the file |
| `unix_socket_bind` | `connect` + `bind`, single socket file | ReadWrite on the file (exists) or on the parent directory (pending — bind will create the file) |
| `unix_socket_dir` | `connect` only, any direct child of a directory | Read on the directory (recursive) |
| `unix_socket_dir_bind` | `connect` + `bind`, any direct child of a directory | ReadWrite on the directory (recursive) |
| `unix_socket_subtree` | `connect` only, any descendant of a directory | Read on the directory (recursive) |
| `unix_socket_subtree_bind` | `connect` + `bind`, any descendant of a directory | ReadWrite on the directory (recursive) |

`unix_socket_dir*` forms are **non-recursive at the socket layer**: only
sockets directly inside the named directory are covered. Use
`unix_socket_subtree*` when a tool creates sockets below nested
subdirectories. The implied filesystem grant is recursive (Landlock's
only expressible granularity), so socket scope is enforced separately by
the supervisor (Linux) or Seatbelt path emission (macOS).

macOS enforces pathname socket access directly through Seatbelt, including the
direct-child versus subtree distinction.

On Linux, pathname socket mediation is off by default. In that mode,
`connect(2)` and `bind(2)` are not restricted by nono's filesystem policy, and
`--block-net` or `--network-profile` does not change that. Set
`linux.af_unix_mediation` to `"pathname"` to enable a default-deny socket
allowlist; the process must then have a matching `unix_socket*` grant.

## Working Directory

The `workdir` section controls whether and how the current working directory is automatically shared with the sandboxed process.

| Value | Meaning |
| - | - |
| `"none"` | No automatic CWD access (default if section omitted) |
| `"read"` | Read-only access to CWD |
| `"write"` | Write-only access to CWD |
| `"readwrite"` | Full read+write access to CWD |

When a profile specifies a workdir access level, nono will prompt the user to confirm CWD sharing (unless `--allow-cwd` is used to skip the prompt). `--allow-cwd` grants access to the directory at that profile's workdir level — so with `"none"` (the `default` profile) it grants nothing. Pair `--allow-cwd` with a profile that sets a workdir level, or grant the directory explicitly with `--allow`/`--read`.

### Filesystem Overrides and Group Composition

Fine-grained additive and subtractive composition lives across three canonical sections:

* `filesystem` — path-based allow, deny, and deny-exemption entries
* `groups` — policy group inclusion (`include`) and removal (`exclude`)
* `commands` — startup-time command allow/deny (deprecated since v0.33.0; not enforced on child processes)

This is the primary mechanism for surgically customizing inherited profiles without redefining everything from scratch.

| Field | Section | Description |
| - | - | - |
| `include` | `groups` | Policy group names to apply on top of inherited defaults |
| `exclude` | `groups` | Remove groups from the resolved group set (exclusion wins over addition) |
| `read` | `filesystem` | Additional read-only directories to allow |
| `write` | `filesystem` | Additional write-only directories to allow |
| `allow` | `filesystem` | Additional read+write directories to allow |
| `deny` | `filesystem` | Additional deny rules to apply (block read and write content). On macOS, Unix socket paths also emit a `network-outbound` deny so that `connect(2)` to the socket is blocked. |
| `deny` | `commands` | Deprecated in v0.33.0. Startup-only command denylist extension. Not enforced for child processes; prefer `filesystem.deny` and narrower grants. |
| `bypass_protection` | `filesystem` | Paths exempted from deny groups. **Does not grant access** — a matching filesystem or Unix socket field must also grant access. |
| `suppress_save_prompt` | `filesystem` | Paths whose runtime denials should not be offered in save-profile prompts. **Does not grant access** and does not hide diagnostics. |
| `suppress_system_services` | `diagnostics` | macOS Seatbelt operation names to hide from diagnostic footers and save-profile prompts. **Does not grant access** and does not change enforcement. |

#### Adding a Deny Rule to an Inherited Profile

Block a specific path that a base profile would otherwise allow:

```json theme={null}
{
  "meta": {
    "name": "claude-deny-git",
    "version": "1.0.0"
  },
  "extends": "nolabs-ai/claude",
  "filesystem": {
    "deny": ["$HOME/.gitconfig"]
  }
}
```

```bash theme={null}
nono run --profile claude-deny-git -- claude
```

#### Blocking Container Access (Docker, Podman, kubectl)

Access to a container runtime socket grants the authority of the daemon behind
it. A rootful Docker daemon usually has root access to the host and runs outside
nono's process sandbox.

On macOS, deny the socket path. nono emits both filesystem and Seatbelt
`network-outbound` denials:

```json theme={null}
{
  "extends": "nolabs-ai/claude",
  "meta": { "name": "no-docker", "version": "1.0.0" },
  "filesystem": {
    "deny": ["/var/run/docker.sock"]
  }
}
```

On Linux, Landlock filesystem rules do not mediate `connect(2)` to pathname
Unix sockets. Enable pathname AF\_UNIX mediation and do not grant the container
runtime socket:

```json theme={null}
{
  "extends": "nolabs-ai/claude",
  "meta": { "name": "no-docker", "version": "1.0.0" },
  "linux": {
    "af_unix_mediation": "pathname"
  }
}
```

With mediation enabled, pathname sockets are default-deny and only explicit
`filesystem.unix_socket*` entries are allowed. `--block-net`, Linux network
namespaces, and `filesystem.deny` do not replace this control.

If a sensitive workload requires Docker, run it inside a separate container or
MicroVM with its own daemon. Mounting the host's rootful Docker socket preserves
the same host authority. `commands.deny` is deprecated startup-only gating and
must not be used as socket enforcement.

#### Adding Write-Only Access

Grant write-only access to a directory without granting read:

```json theme={null}
{
  "meta": {
    "name": "write-only-cache",
    "version": "1.0.0"
  },
  "extends": "default",
  "filesystem": {
    "write": ["$HOME/.cache/my-agent-cache"]
  },
  "workdir": {
    "access": "none"
  }
}
```

#### Removing Groups from an Inherited Profile

Remove the deprecated startup-only command-gating group to allow `rm`, `chmod`, etc.:

```json theme={null}
{
  "meta": {
    "name": "no-dangerous-commands",
    "version": "1.0.0"
  },
  "extends": "default",
  "groups": {
    "exclude": [
      "dangerous_commands",
      "dangerous_commands_linux",
      "dangerous_commands_macos"
    ]
  }
}
```

#### Overriding a Deny Rule for a Specific Path

Some deny groups (like `deny_credentials`) are marked as required and cannot be excluded. Use `filesystem.bypass_protection` to punch a targeted hole through a deny group for a specific path. The path must also be explicitly granted via another `filesystem` entry — `bypass_protection` only removes the deny rule, it does not implicitly grant access.

```json theme={null}
{
  "meta": {
    "name": "docker-agent",
    "version": "1.0.0"
  },
  "extends": "nolabs-ai/claude",
  "filesystem": {
    "allow": ["$HOME/.docker"],
    "bypass_protection": ["$HOME/.docker"]
  }
}
```

This allows access to `~/.docker` even though `deny_credentials` blocks it by default. The deny override does not implicitly grant access — the matching `filesystem.allow` entry is required.

This is equivalent to the CLI flag `--bypass-protection`:

```bash theme={null}
nono run --profile nolabs-ai/claude --allow ~/.docker --bypass-protection ~/.docker -- claude
```

#### Suppressing Save Suggestions Without Granting Access

Use `filesystem.suppress_save_prompt` when a path is expected to be denied and
you do not want nono to keep offering it as a profile addition:

```json theme={null}
{
  "extends": "nolabs-ai/claude",
  "meta": { "name": "claude-local", "version": "1.0.0" },
  "filesystem": {
    "suppress_save_prompt": ["$HOME/.copilot/settings.json"]
  }
}
```

`filesystem.suppress_save_prompt` only suppresses the save-profile suggestion
for matching denials. The sandbox still denies the path, and the diagnostic
footer still shows the denial, annotated with `[save skipped]` so it is clear
why the path does not appear in the save prompt.

The interactive save prompt applies this to every listed path suggestion when
you choose `suppress`; it does not create any allow/read/write grants.

#### Suppressing Expected macOS System-Service Diagnostics

Use `diagnostics.suppress_system_services` when a macOS Seatbelt operation is
expected to be denied and you do not want nono to repeat the diagnostic footer
(or offer a save-profile prompt) on every run:

```json theme={null}
{
  "extends": "default",
  "meta": { "name": "node-local", "version": "1.0.0" },
  "diagnostics": {
    "suppress_system_services": ["user-preference-read"]
  }
}
```

This is macOS-only. Values are Seatbelt operation names from the sandbox log,
such as `user-preference-read` (CFPreferences / NSUserDefaults probes from npm
and other Node tools) or `forbidden-exec-sugid`. Suppression is output-only:
the sandbox still denies the operation.

Unlike `filesystem.suppress_save_prompt`, this hides matching violations from
the diagnostic footer entirely. There is no CLI flag equivalent yet; add the
operation name to your profile. To silence all post-run diagnostics instead,
use `--no-diagnostics`.

> [!NOTE]
>
> `filesystem`, `groups`, `commands`, and `diagnostics` fields are additive across inheritance. A child profile's `filesystem.deny` is merged with the base profile's `filesystem.deny`, `groups.exclude` from both levels are combined, and `diagnostics.suppress_system_services` entries from both profiles are appended and deduplicated.

### Network Configuration

The `network` section controls network access and credential injection.

```json theme={null}
{
  "network": {
    "block": false,
    "network_profile": "claude-code",
    "allow_domain": ["my-internal-api.example.com"],
    "open_port": [3000],
    "open_port_range": [[3001, 3010]],
    "listen_port": [8080],
    "listen_port_range": [[9000, 9010]],
    "no_proxy": ["redis"],
    "credentials": ["openai", "anthropic"],
    "upstream_proxy": "squid.corp:3128",
    "upstream_bypass": ["git.internal.corp", "*.dev.local"],
    "custom_credentials": {
      "telegram": {
        "upstream": "https://api.telegram.org",
        "credential_key": "telegram_bot_token",
        "inject_header": "Authorization",
        "credential_format": "Bearer {}"
      }
    }
  }
}
```

| Field | Description |
| - | - |
| `block` | Block outbound IP network access (default: `false`) |
| `network_profile` | Network profile name for host filtering (e.g., `minimal`, `claude-code`, `enterprise`). Set to `null` in a child profile to clear an inherited value. |
| `allow_domain` | Additional domains to allow through the proxy |
| `open_port` | Localhost TCP ports to allow bidirectional IPC (equivalent to `--open-port`) |
| `open_port_range` | Inclusive localhost TCP port ranges to allow bidirectional IPC, e.g. `[[3001, 3010]]`. Multiple ranges are allowed; overlapping ranges are merged automatically |
| `listen_port` | TCP ports the sandboxed child may listen on (equivalent to `--listen-port`) |
| `listen_port_range` | Inclusive TCP port ranges the sandboxed child may listen on (bind only), e.g. `[[9000, 9010]]`. Multiple ranges are allowed; overlapping ranges are merged automatically |
| `no_proxy` | Additional client-side `NO_PROXY` / `no_proxy` host patterns in proxy mode; supports safe single-label local aliases, canonical IPs, and explicit suffix patterns; does not grant network access, so direct connections still require matching sandbox permissions; entries overlapping route upstreams or `allow_domain` fail configuration so proxy-enforced routes cannot be bypassed |
| `credentials` | Credential services to enable via reverse proxy (e.g., `openai`, `anthropic`) |
| `custom_credentials` | Custom credential service definitions for APIs not in the built-in list |
| `upstream_proxy` | Upstream (enterprise) proxy address, e.g., `squid.corp:3128` |
| `upstream_bypass` | Domains to bypass the upstream proxy (exact hostnames and `*.` wildcards) |

#### Custom Credentials

The `custom_credentials` field lets you define credential services for any API:

| Field | Required | Default | Description |
| - | - | - | - |
| `upstream` | Yes | - | Upstream URL (must be HTTPS, or HTTP for localhost) |
| `credential_key` | Yes | - | Keystore account name |
| `inject_header` | No | `Authorization` | Header to inject credential into |
| `credential_format` | No | `Bearer {}` | Format string (`{}` replaced with credential) |

See [Credential Injection](https://nono.sh/docs/cli/features/credential-injection.md#custom-credential-definitions) for complete documentation.

### Hooks (pack-declared, legacy profile field)

The `hooks` field is a legacy declaration surface: since v0.44.0, setting it in a profile no longer installs anything itself — nono just prints a one-line note. Actual agent-hook wiring (e.g. installing a script into `~/.claude/hooks/` and patching `settings.json`) is now done by a pack's `package.json` `wiring` directives, executed on `nono pull`/`nono resolve`. See [Package Publishing](https://nono.sh/docs/cli/features/package-publishing.md) if you're authoring a pack that needs this.

This is unrelated to **Session Hooks** above, which is a currently-active profile feature for running host-privileged setup/cleanup scripts around the sandboxed process itself.

### Rollback Exclusions

The `rollback` section controls which files are excluded from [atomic rollback](https://nono.sh/docs/cli/features/atomic-rollbacks.md) snapshots:

```json theme={null}
{
  "rollback": {
    "exclude_patterns": ["node_modules", ".next", "__pycache__", "target"],
    "exclude_globs": ["*.tmp.[0-9]*.[0-9]*"]
  }
}
```

These exclusions are combined with gitignore patterns from the working directory.

### Skip Directories

Use `skipdirs` to extend the built-in heavy-directory skip list for pre-exec trust scanning and rollback preflight:

```json theme={null}
{
  "skipdirs": ["generated", "vendor-cache"]
}
```

Entries are matched as exact path component names. They do not grant or deny sandbox access; they only prune traversal during trust discovery and rollback heuristics.

### Environment Variables

Profiles support these environment variables in path values:

| Variable | Expands To |
| - | - |
| `$WORKDIR` | Current working directory (from `--workdir` or cwd) |
| `$HOME` | User's home directory |
| `$XDG_CONFIG_HOME` | XDG config directory (default: `~/.config`; used for `~/.config/nono/` when unset) |
| `$NONO_CONFIG` | nono config root (`$XDG_CONFIG_HOME/nono`, default `~/.config/nono`) |
| `$NONO_PACKAGES` | Installed pack store (`$NONO_CONFIG/packages`) |
| `$XDG_DATA_HOME` | XDG data directory (default: `~/.local/share`) |
| `$XDG_STATE_HOME` | XDG state directory (default: `~/.local/state`) |
| `$XDG_CACHE_HOME` | XDG cache directory (default: `~/.cache`) |
| `$XDG_RUNTIME_DIR` | XDG runtime directory (no default; left unexpanded when unset) |
| `$TMPDIR` | System temporary directory |
| `$UID` | Current user ID |

### Creating User Profiles

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

```bash theme={null}
# Scaffold a profile that inherits from default with the node_runtime group
nono profile init my-agent --extends default --groups node_runtime

# Validate the generated profile
nono profile validate ~/.config/nono/profiles/my-agent.json

# Use the profile
nono run --profile my-agent -- my-agent-command
```

See [Profile Authoring](https://nono.sh/docs/cli/features/profile-authoring.md) for the full workflow, including JSON Schema integration, editor autocomplete, and the LLM authoring guide.

You can also load a profile by file path:

```bash theme={null}
nono run --profile ./profiles/my-agent.json -- my-command
```

### Overriding Pack Profiles

CLI flags always take precedence over profile settings:

```bash theme={null}
# Use the Claude package profile but block network
nono run --profile nolabs-ai/claude --block-net -- claude

# Use the Claude package profile but add a custom domain
nono run --profile nolabs-ai/claude --allow-domain custom-api.example.com -- claude

# Add extra directory access
nono run --profile nolabs-ai/claude --allow ~/other-project -- claude
```

To keep a base profile but clear an inherited network profile, set `network.network_profile` to `null` in the child:

```json theme={null}
{
  "meta": { "name": "claude-code-netopen" },
  "extends": "nolabs-ai/claude",
  "network": { "network_profile": null }
}
```

Package profiles occupy a separate, fully-qualified namespace. A bare user profile name cannot override one.

***

## Groups

Groups are named, composable collections of security rules. Profiles reference groups by name in their `groups.include` field.

### How Groups Compose

Every profile's effective capability set is built through composition:

```
((default_profile_groups + profile.groups.include) - profile.groups.exclude) + profile.filesystem.{allow,read,write} + profile.commands.allow - (deny_groups + profile.filesystem.deny + profile.commands.deny) + profile.filesystem.bypass_protection + CLI flags
```

1. **default\_profile\_groups** come from the built-in `default` profile
2. **profile.groups.include** adds additional groups on top
3. **profile.groups.exclude** removes groups from the composed set (exclusion wins)
4. **profile.filesystem.allow/read/write**, **profile.filesystem.deny**, and **profile.commands.allow/deny** apply additive overrides
5. **profile.filesystem.bypass\_protection** punches targeted holes through deny groups (requires a matching filesystem or Unix socket grant)
6. **CLI overrides** (`--allow`, `--read`, `--bypass-protection`, etc.) are applied last

`profile.filesystem.suppress_save_prompt` and `--suppress-save-prompt` are
prompt filters only; they do not participate in capability enforcement.

`profile.diagnostics.suppress_system_services` is also output-only (macOS). It
hides recurring non-filesystem Seatbelt violations from diagnostic footers and
save-profile prompts without granting the underlying operation.

Exclusions are applied after group addition. If the same group appears in both
`groups.include` and `groups.exclude`, the exclusion wins.

### Group Taxonomy

Groups use a structured allow/deny taxonomy:

#### Allow Operations

| Field | Meaning |
| - | - |
| `allow.read` | Read-only access to listed paths |
| `allow.write` | Write-only access (no read) |
| `allow.readwrite` | Both read and write access |

#### Deny Operations

| Field | Meaning |
| - | - |
| `deny.access` | Block both read and write content (metadata like `stat` still allowed) |
| `deny.unlink` | Block file deletion globally |
| `deny.commands` | Block execution of listed commands |

#### Other

| Field | Meaning |
| - | - |
| `symlink_pairs` | macOS symlink-to-target path mappings (e.g., `/etc` to `/private/etc`) |
| `platform` | Restrict group to `"macos"` or `"linux"` only |

### Built-in Groups

> [!NOTE]
>
> Adding a group to `groups.include` always applies *that group's own rules*.
> For allow groups (like `git_config`) that grants access. For deny/protection
> groups (like `dangerous_commands`, `unlink_protection`) that *adds a
> restriction* — it does not grant the denied commands or paths. To permit a
> normally-blocked command or path, put the group in `groups.exclude` instead,
> or use `filesystem.bypass_protection` for a narrower hole.

**Deny groups** (block sensitive content):

* **`deny_credentials`** — Block access to cryptographic keys, tokens, and cloud credentials ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L7))

* **`deny_keychains_macos`** — Block access to macOS keychains and password stores ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L35))

* **`deny_keychains_linux`** — Block access to Linux keyrings and password stores ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L51))

* **`deny_browser_data_macos`** — Block access to macOS browser stored data (cookies, saved passwords, sessions) ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L64))

* **`deny_browser_data_linux`** — Block access to Linux browser stored data (cookies, saved passwords, sessions) ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L82))

* **`deny_macos_private`** — Block access to macOS private data (messages, mail, cookies) ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L98))

* **`deny_shell_history`** — Block access to shell command history files ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L112))

* **`deny_shell_configs`** — Block access to shell configuration files that may embed secrets ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L124))

**Protection groups**:

* **`unlink_protection`** — Block file deletion globally, override for user-writable paths ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L586))

* **`dangerous_commands`** — Cross-platform commands blocked by default to prevent destructive actions ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L593))

* **`dangerous_commands_macos`** — macOS-specific commands blocked by default ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L626))

* **`dangerous_commands_linux`** — Linux-specific commands blocked by default ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L637))

**System path groups** (grant necessary system access):

* **`system_read_macos`** — macOS system paths required for executables to function ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L145))

* **`system_read_linux_core`** — Linux core system paths required for normal CLI execution ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L192))

* **`system_write_macos`** — macOS paths requiring write for temp files and devices ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L295))

* **`system_write_linux`** — Linux paths requiring write for temp files and devices ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L309))

* **`linux_runtime_state`** — Linux runtime state paths for host session and service integration ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L267))

* **`linux_sysfs_read`** — Linux sysfs paths for kernel and device state inspection ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L277))

* **`linux_temp_read`** — Linux shared temporary directory read access ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L286))

**Cache groups** (user-level caches and state):

* **`user_caches_macos`** — User cache, log, and preference directories for macOS programs ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L328))

* **`user_caches_linux`** — User cache directories for Linux programs (XDG) ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L342))

* **`claude_cache_linux`** — Claude Code CLI cache and MCP log directories on Linux ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L351))

**Runtime groups** (language toolchain paths):

* **`node_runtime`** — Node.js runtime and package manager paths ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L372))

* **`rust_runtime`** — Rust toolchain paths ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L426))

* **`python_runtime`** — Python runtime paths (pyenv, conda, uv) ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L415))

* **`go_runtime`** — Go toolchain paths ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L435))

* **`go_runtime_linux`** — Go build cache on Linux ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L444))

* **`go_runtime_macos`** — Go build cache on macOS ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L453))

* **`java_runtime`** — Java runtime and build tool paths (SDKMAN, Maven, Gradle) ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L462))

* **`nix_runtime`** — Nix package manager runtime paths ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L560))

* **`nix_profile_state_write`** — Write access to `~/.local/state/nix/profiles` for `nix profile` mutations such as `nix profile add` ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L577))

* **`user_tools`** — User-local executables, .desktop files, man pages, and shell completions ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L360))

* **`homebrew_macos`** — Homebrew installation paths on macOS ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L474))

* **`homebrew_linux`** — Homebrew (Linuxbrew) installation paths on Linux ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L485))

* **`snap_linux`** — Installed Snap package contents on Linux ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L494))

* **`mise_manager`** — Mise - dev tools, env vars, task runner ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L404))

* **`bun_runtime`** — Bun javascript runtime and package manager paths ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L387))

**Tool-specific groups** (paths for specific applications):

* **`claude_code_macos`** — Claude Code macOS-specific state and credential paths ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L504))

* **`claude_code_linux`** — Claude Code Linux-specific state paths ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L529))

* **`codex_macos`** — Codex macOS-specific state and credential paths ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json))

* **`opencode_linux`** — OpenCode binary directory (Landlock requires directory read to execute binaries) ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L395))

* **`rtk_state`** — rtk history database and persisted state under `~/.local/share/rtk` ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L407))

* **`vscode_macos`** — Visual Studio Code configuration and extension directories for macOS ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L538))

* **`vscode_linux`** — Visual Studio Code configuration and extension directories for Linux ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L548))

**Application config groups** (profile and configuration files):

* **`git_config`** — Read access to git configuration files ([source](https://github.com/nolabs-ai/nono/blob/main/crates/nono-cli/data/policy.json#L574))

> [!NOTE]
>
> Group definitions are located in `crates/nono-cli/data/policy.json`. Each
> entry above links to its exact definition; inspect the file directly to see
> the full set of rules a group applies.

### Platform-Specific Groups

Groups with a `platform` field only apply on that OS:

```json theme={null}
{
  "system_read_macos": {
    "platform": "macos",
    "allow": {
      "read": ["/System/Library", "/usr/lib"]
    }
  }
}
```

Groups without a `platform` field (like `deny_credentials`) apply on all platforms.

Platform-specific groups use `_macos` or `_linux` suffixes by convention.

### Platform Differences

**macOS (Seatbelt)** supports full deny-within-allow semantics. A group can allow `/Users` but deny `/Users/luke/.ssh` and the deny takes precedence.

**Linux (Landlock)** is strictly allow-list. Deny groups are implemented as exclusion filters - broad allow groups that overlap deny paths will generate warnings. Avoid granting access to parent directories of deny paths on Linux.

***

## Available Profiles

### default

The base profile that all other profiles extend. Provides system path access, deny groups for sensitive content, and deprecated startup-only command gating. Does not grant working directory access or any user-specific paths.

**Groups:** `deny_credentials`, `deny_keychains_macos`, `deny_keychains_linux`, `deny_browser_data_macos`, `deny_browser_data_linux`, `deny_macos_private`, `deny_shell_history`, `deny_shell_configs`, `system_read_macos`, `system_read_linux_core`, `system_write_macos`, `system_write_linux`, `user_tools`, `homebrew_macos`, `homebrew_linux`, `dangerous_commands`, `dangerous_commands_macos`, `dangerous_commands_linux`

**Network:** Allowed

**CWD:** None

### Claude Code (`nolabs-ai/claude`)

```bash theme={null}
nono run --profile nolabs-ai/claude -- claude
```

**Groups:** `claude_code_macos`, `claude_code_linux`, `user_caches_macos`, `claude_cache_linux`, `node_runtime`, `rust_runtime`, `python_runtime`, `vscode_macos`, `vscode_linux`, `nix_runtime`, `git_config`, `unlink_protection` (plus `default` groups)

**Filesystem:** `~/.claude` (read+write), `~/.claude.json` and `~/.claude.json.lock` (read+write), plus platform-specific Claude Code and VS Code paths from the matching `_macos` or `_linux` groups, and git config files from `git_config` group

**Network:** Allowed

**CWD:** Read+write

**Special:** Auto-installs Claude Code hooks. OAuth2 login support via `open_urls` (allows `https://claude.ai` and localhost). Profile opts into `allow_launch_services` for macOS browser opening.

### Codex (`nolabs-ai/codex`)

```bash theme={null}
nono run --profile nolabs-ai/codex -- codex --no-daemon --sandbox danger-full-access --ask-for-approval on-request
```

**Groups:** `codex_macos` and `user_caches_macos` on macOS; `user_caches_linux` and `linux_sysfs_read` on Linux; plus `node_runtime`, `rust_runtime`, `python_runtime`, `nix_runtime`, `git_config`, and `unlink_protection` on both platforms (in addition to `default` groups)

**Filesystem:** `~/.codex`, `~/.agents`, and `$NONO_CONFIG/profile-drafts` (read+write); platform user caches (read+write); common toolchain and git config paths (read); `/sys` (read) on Linux; and `~/Library/Keychains` (read+write, with explicit deny bypass) plus `~/Library/Preferences` (read) on macOS

**Network:** Allowed

**CWD:** Read+write

**Special:** OAuth2 login support via `open_urls` (allows `https://auth.openai.com` and localhost). Profile opts into `allow_launch_services`. Interactive sessions should use `--no-daemon` so the app server and tool execution stay inside the same nono sandbox. `--sandbox danger-full-access` leaves nono as the filesystem and network boundary, while `--ask-for-approval on-request` retains Codex's approval flow.

### OpenCode 2

```bash theme={null}
nono run --profile nolabs-ai/opencode -- opencode --standalone
```

**Groups:** `user_caches_macos`, `user_caches_linux`, `node_runtime`, `opencode_linux`, `git_config`, `unlink_protection` (plus `default` groups)

**Filesystem:** `~/.opencode` (read-only); OpenCode config, cache, data, state, and OpenTUI data (read+write); plus git config files from `git_config`

**Network:** Allowed

**CWD:** Read+write

**Special:** Use `--standalone` to keep the OpenCode 2 server inside the sandbox. The pack disables project discovery and roots instruction initialization in the worktree to prevent denied parent-directory probes from breaking startup.

### OpenCode 1

```bash theme={null}
nono run --profile nolabs-ai/opencode-v1 -- opencode
```

**Groups:** Same runtime groups as the OpenCode 2 pack

**Filesystem:** OpenCode installation, config, cache, data, state, and OpenTUI data (read+write), plus git config files

**Network:** Allowed

**CWD:** Read+write

**Special:** Do not use `--standalone`; it is an OpenCode 2 flag. The v1 and v2 packs share plugin installation paths, so remove one before switching to the other.

### OpenClaw (`nolabs-ai/openclaw`)

```bash theme={null}
nono run --profile nolabs-ai/openclaw -- openclaw
```

**Groups:** `node_runtime` (plus `default` groups)

**Filesystem:** `~/.openclaw`, `~/.config/openclaw`, `~/.local`, `$TMPDIR/openclaw-$UID` (all read+write)

**Network:** Allowed

**CWD:** Read-only

### python-dev

```bash theme={null}
nono run --profile python-dev -- my-python-app
```

**Groups:** `python_runtime` (plus `default` groups)

**Network:** Allowed, with `developer` network profile for host filtering

**CWD:** Read+write

### node-dev

```bash theme={null}
nono run --profile node-dev -- npm start
```

**Groups:** `node_runtime` (plus `default` groups)

**Network:** Allowed, with `developer` network profile for host filtering

**CWD:** Read+write

### go-dev

```bash theme={null}
nono run --profile go-dev -- go run .
```

**Groups:** `go_runtime` (plus `default` groups)

**Network:** Allowed, with `developer` network profile for host filtering

**CWD:** Read+write

### rust-dev

```bash theme={null}
nono run --profile rust-dev -- cargo run
```

**Groups:** `rust_runtime` (plus `default` groups)

**Network:** Allowed, with `developer` network profile for host filtering

**CWD:** Read+write

### java-dev

```bash theme={null}
nono run --profile java-dev -- mvn package
```

**Groups:** `java_runtime` (plus `default` groups)

**Network:** Allowed, with `developer` network profile for host filtering

**CWD:** Read+write

> [!NOTE]
>
> `java-dev` covers SDKMAN-managed JDKs, Maven, and Gradle. It does not include
> system JDK config paths (e.g. `/etc/java21-openjdk`) because these vary by
> distribution and installation method. Add them in a user profile that extends
> `java-dev` — see [Predefined Profiles vs User Profiles](#predefined-profiles-vs-user-profiles).

***

## Requesting New Profiles

If you'd like a profile for a tool not listed here:

1. Open an issue on the [nono GitHub repository](https://github.com/nolabs-ai/nono/issues)
2. Include:
   * Tool name and repository URL
   * Required filesystem access patterns
   * Network requirements
   * Any special considerations

Profiles are reviewed for security before inclusion.


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