# Nono Profile

> Create, edit, validate, and test nono.sh sandbox profiles (JSON/JSONC) that scope a command's filesystem, network, and credential access. Use when the user wants to sandbox a tool/command with nono, write or update a nono profile, check a profile is valid, or test what a profile actually allows/denies.

- Skill: `robin0xfull/nono-profile` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add robin0xfull/nono-profile`
- Raw SKILL.md: https://api.skillmd.com/api/skills/robin0xfull/nono-profile/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: robin0xfull (https://skillmd.com/u/robin0xfull)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/robin0xfull/nono-profile

---


# nono Profile Authoring

Note the directory of this SKILL.md when loaded — that is `$SKILL_ROOT`. Substitute the real
absolute path when running the commands below; it is not a shell variable that already exists.

Authors nono.sh sandbox profiles: scaffold one for a given command, fill in the exact filesystem/
network/credential access it needs, explain the result in plain language, then validate and test it.

**The profile format is not documented in this skill.** `nono profile guide` and `nono profile
schema` ship with the binary and are always version-matched, so read the format from there via
`scripts/profile-docs.sh` rather than from any summary. A summary would drift; the binary cannot.

## Workflow

- [ ] Step 1: Confirm nono is available — run `$SKILL_ROOT/scripts/check-nono.sh`.
  - If it reports `nono is not installed`, tell the user exactly that — "nono is not installed" —
    and stop. Don't add install instructions or any other detail about the absence.
- [ ] Step 2: Work out what the target command actually needs: which paths it reads/writes, which
  network domains it calls, which credentials/env vars it needs, and whether an existing security
  group already covers it (`$SKILL_ROOT/scripts/profile-docs.sh --groups`). Inspect the project
  (dependencies, config, source) for evidence rather than guessing, and ask the user
  (`AskUserQuestion`) to confirm anything that isn't derivable from the code.
- [ ] Step 3: Scaffold with `nono profile init <name> [--extends <base>] [--groups <g1>,<g2>] --output <path>`,
  then hand-edit the JSON/JSONC to add the specific filesystem/network/credential entries from
  Step 2. Look up every field you touch before writing it:
  - `$SKILL_ROOT/scripts/profile-docs.sh --sections` — what the guide covers
  - `$SKILL_ROOT/scripts/profile-docs.sh --section filesystem` — that section in full
  - `$SKILL_ROOT/scripts/profile-docs.sh --schema FilesystemConfig` — the exact schema entry
  - `$SKILL_ROOT/scripts/profile-docs.sh --search '<regex>'` — when you don't know the section name
- [ ] Step 4: Explain the profile in plain language, using the Output format below. This step is
  required for every profile created or edited — the point is letting the user check the claims
  against the JSON themselves, not just trusting the tool.
- [ ] Step 5: Validate — run `$SKILL_ROOT/scripts/validate-profile.sh <path> [name]`. Fix anything
  it flags and re-run until it's clean.
- [ ] Step 6: Test — run `$SKILL_ROOT/scripts/test-profile.sh --profile <name> --command "<cmd>"`, plus:
  - `--path <p> --op read|write|readwrite` — authoritative verdict on filesystem access
  - `--host <h> [--port <n>]` — authoritative verdict on network access
  - `--scope signal|abstract-unix-socket` — Landlock scope only, *not* general policy inspection
  - `--live` — actually execute the command under the profile
  - `--deny-command "<cmd>"` — confirm something that should be blocked really is

  Done when the intended command runs clean and every expected denial reports **PASS**.
  `INCONCLUSIVE` (exit 2) means the command failed without proving a denial — treat it as untested
  and fix the cause, never as a pass.

## Output format (Step 4)

```
## Profile: <name> (<path>)

Extends: <base profile chain, or "none">
Groups: <included groups> | excludes: <excluded groups, if any>

Filesystem
- Read+write: <paths>
- Read-only: <paths>
- Write-only: <paths>
- Denied: <paths, including anything a group denies that this profile doesn't override>
Workdir: <access level>

Network
- Blocked entirely: <yes/no>
- Allowed domains: <list, or "none">
- Ports — outbound: <list>, inbound: <list>
- Credentials proxied: <named services, or "none">

Other
- Security/process isolation: <signal_mode / process_info_mode / anything non-default>
- Anything else non-default worth flagging
```

Base every line on the actual resolved JSON (`nono profile show`), not just the file you wrote —
inheritance can add or override things the file itself doesn't show.

## Gotchas

Verified against the shipped guide; everything else, look up in Step 3 rather than assuming.

- **Linux rejects `deny` inside a grant.** Landlock is strictly allow-list and has no kernel deny
  primitive, so a `deny` path overlapping an `allow`/`read`/`write` path makes nono refuse to
  start — a hard error, not an override. macOS Seatbelt enforces the same profile fine. Write the
  grants narrowly enough that they never match what you want denied, instead of granting broadly
  and subtracting.
- `bypass_protection` does **not** grant access. It only lifts a group's deny rule; the path still
  needs its own entry under `allow`/`read`/`write` (or a `*_file` variant) to be reachable.
- `groups.include: []` does not mean "no deny rules" — groups marked `required` (`deny_credentials`,
  `deny_shell_history`, …) resolve in regardless. `groups.exclude` is what removes them, and it
  weakens the sandbox.
- `workdir.access` defaults to `none`, so a profile grants no working-directory access unless you
  set it — and a child setting `"none"` *inherits the base* instead of overriding it.
- `open_urls` replaces the parent's value on inheritance instead of merging, unlike every other
  array/map field. To clear it, pass an explicit empty object, not `null` (`null` inherits).
- There is no mechanism to remove an inherited filesystem path. Only `network_profile` supports
  null-clearing.
- `network.block: true` cannot be combined with proxy settings.
- The pre-#594 `policy.*` keys still load but are deprecated and removed in v1.0.0. Write canonical
  keys (`filesystem.*`, `groups.include`, `commands.allow`); `nono profile validate --strict` fails
  on the legacy ones.
- `nono profile show` and `nono run --profile` both accept a name or a file path, but a name only
  resolves once the file is discoverable (typically `~/.config/nono/profiles/<name>.json`). Pass the
  path while drafting.

## Reference

`scripts/profile-docs.sh` — all of nono's own profile documentation: `--sections`, `--section
<query>`, `--search <regex>`, `--guide`, `--schema [name]`, `--groups [name]`.

