Core0 / Nio
Core0
GitHub v2.4.4
Configuration · Reference

# Every field in config.yaml.

Nio reads `~/.nio/config.yaml` (or `$NIO_HOME/config.yaml`). Two top-level sections: `guard` for the runtime pipeline + scan rules, `collector` for OTLP telemetry. The bundled template at `config.default.yaml` has inline comments for every field.

Something not working? Run `/nio doctor` to validate the current config schema and dry-run OAuth / LLM connectivity. See Diagnostics for the full failure-surface model.

## Where it lives

- Default path: `~/.nio/config.yaml`
- Override directory: set `NIO_HOME` environment variable
- Reset to defaults: `/nio reset` or `setup.sh --reset-to-defaults`

You can edit the file directly or use `/nio config <level>` to change the protection level without opening an editor.

To apply an operator-provided config in one shot, run `/nio config import <path>`. nio runs `/nio doctor` against the incoming YAML first and refuses to overwrite if any probe fails; on success the previous file is preserved as `config.yaml.bak.<ISO-stamp>`.

## Top-level

### `agent_name`

Type: string · Default: `""` · Scope: telemetry only

User-provided telemetry identity (alias). When non-empty, it overrides the platform-derived value on `gen_ai.agent.name` in traces and log records, and lands as the `agent_name` field in `~/.nio/audit.jsonl`. Use it to split telemetry by deployment / machine / user when multiple installations share one collector backend ("alice-laptop", "ci-runner-3", "prod-scoring-east").

The `nio.platform` dimension is not overridden — backends still see both axes (`nio.platform` = the underlying CLI host like `claude-code`, `gen_ai.agent.name` = the operator's alias). When empty / unset, behaviour is identical to today: `gen_ai.agent.name` falls back to platform.

Not emitted on metrics — keeping per-deployment slicing out of metric labels avoids ballooning the backend's cardinality budget; query `nio.platform` for host-level metric slicing.

    agent_name: "alice-laptop"

## guard

### `guard.protection_level`

Type: string · Default: `balanced` · Values: `strict` | `balanced` | `permissive`

Decision aggressiveness. Determines the score thresholds for allow/confirm/deny. See Scoring → thresholds for the full matrix.

    guard:
      protection_level: balanced

### `guard.confirm_action`

Type: string · Default: `allow` · Values: `allow` | `deny` | `ask`

What to do when the guard's decision is confirm but the platform has no interactive prompt (e.g. OpenClaw). `allow` lets it through and writes a warning to the audit log; `deny` blocks; `ask` uses the platform confirm if available, else falls back to `allow`.

### `guard.file_scan_rules`

Type: object · Default: all empty arrays

Extra regex patterns added to the named scan module. Keys are module names; values are arrays of regex pattern strings. Modules:

- `shell_exec` — feeds `SHELL_EXEC` [high] and `AUTO_UPDATE` [critical]
- `remote_loader` — feeds `REMOTE_LOADER` [critical]
- `secrets` — feeds `READ_ENV_SECRETS` [med], `READ_SSH_KEYS`/`READ_KEYCHAIN`/`PRIVATE_KEY_PATTERN` [critical]
- `obfuscation` — feeds `OBFUSCATION` [high]
- `prompt_injection` — feeds `PROMPT_INJECTION` [critical]
- `exfiltration` — feeds `NET_EXFIL_UNRESTRICTED` [high], `WEBHOOK_EXFIL` [critical]
- `trojan` — feeds `TROJAN_DISTRIBUTION` [critical], `SUSPICIOUS_PASTE_URL` [high], `SUSPICIOUS_IP`/`SOCIAL_ENGINEERING` [med]

    guard:
      file_scan_rules:
        shell_exec:
          - "my_unsafe_exec\\("
        remote_loader:
          - "loadFromCDN\\("

### `guard.action_guard_rules`

Type: object

Extra patterns for Phase 2 runtime action analysis. Two field shapes:

- String fields — plain strings, matched as described per field (case-insensitive substring / prefix / domain suffix). No regex metacharacters, no escaping.
- Regex fields — names ending in `_patterns`. Each entry is either a bare regex string or the `/pattern/flags` form (flags: g i m s u y). YAML requires backslashes to be escaped — write `\\d`, `\\/`, `\\.` inside quotes.

All fields extend the built-in detection lists; they never replace them. Every match produces a Phase 2 finding at the severity shown in [brackets].

#### `dangerous_commands` [critical]

Case-insensitive substring match against the full command line. Use for fixed literal fragments to hard-block.

    action_guard_rules:
      dangerous_commands: ["format c:", "shutdown -h now"]

#### `dangerous_patterns` [critical]

Regex against the full command line. Use when you need alternation, anchors, or character classes.

    action_guard_rules:
      dangerous_patterns:
        - "/rm\\s+-rf\\s+\\//"
        - "/curl\\s+.*\\|\\s*(ba)?sh/i"

#### `sensitive_commands` [high]

Commands that read sensitive data. Case-insensitive substring match. Built-ins already cover `/etc/passwd`, `~/.ssh`, `~/.aws`, `printenv`, etc.

#### `system_commands` [high]

Word-boundary match on the program name (start of line or after a space). Add a trailing space for short names to prevent prefix hits (e.g. `"su "` not `"su"`).

#### `network_commands` [medium]

Same word-boundary rule. Flags any use regardless of target.

#### `webhook_domains` [high]

Hostnames matched exactly OR as suffix (`foo.example.com` is covered by `example.com`). No scheme, no path, no port.

    webhook_domains: ["webhook.site", "requestbin.net", "ngrok-free.app"]

#### `sensitive_paths` [critical]

Substring match with a specific convention: the matcher prepends `/` to each pattern and tests `normalized.includes("/" + pattern) || normalized.endsWith(pattern)`. Input is normalized first: `~/` expands to `/HOME/`, backslashes become forward slashes.

Two supported shapes:

- Directory fragment — DO NOT include a leading slash; the matcher adds one. Use `etc/`, `raw_files/`, `.ssh/`. Writing `/etc/` is a common mistake — it becomes `//etc/` internally and almost never matches.
- Filename suffix — matched via `endsWith` (e.g. `.env`, `id_rsa`, `credentials.json`, `.env.prod`).

Known gap

A bare relative path like `raw_files/foo.txt` (no leading `/`) does NOT match a `raw_files/` pattern. Use `sensitive_path_patterns` with `"/^raw_files\\//"` for that.

#### `sensitive_path_patterns` [critical]

Regex against the same normalized path. Use for dynamic segments, relative paths, or flag-based matching.

    sensitive_path_patterns:
      - "/^\\/abc\\/[^/]+\\/def/"   # /abc/<id>/def
      - "/^raw_files\\//"           # bare relative paths
      - "/\\.env\\.[a-z]+$/i"       # .env.prod, .env.staging

#### `secret_patterns` [high]

Regex against HTTP request body previews on `network_request` actions. Use for internal token formats the built-in detector doesn't know about.

    secret_patterns:
      - "/ACME_[A-Z0-9]{32}/"
      - "/internal-token:\\s*[\\w-]+/i"

### `guard.llm_analyser`

Type: object

Phase 5 — semantic analysis using Claude. Disabled by default. Requires an Anthropic API key. Validated by `/nio doctor`.

Field | Type | Default | Note
--- | --- | --- | ---
`enabled` | boolean | `false` | Master switch.
`api_key` | string | `""` | Anthropic API key. Empty + `enabled: true` → diagnostic.
`model` | string | `"claude-sonnet-4-20250514"` | Model identifier.
`max_input_tokens` | number | `50000` | Per-scan input budget.

### `guard.external_analyser`

Type: array · Default: `[]`

Phase 6 — external scoring API endpoints. 0..N endpoints; each runs concurrently. Each entry contributes to the weighted score average via its own `weight` field. See the Phase 6 page for the full wire model. Validated by `/nio doctor` (OAuth entries get a real `client_credentials` dry-run).

Each entry is an object:

Field | Type | Default | Note
--- | --- | --- | ---
`name` | string | (required) | Unique identifier, appears in `scores.external` and `phase_timings.external`.
`endpoint` | string | (required) | HTTPS URL of the scoring endpoint. nio always issues `GET` — encode any context the endpoint needs in the URL itself (e.g. `?agent-name=X`). Endpoint must return `{ "score": number, "reason"?: string }` — see Phase 6 → Response contract.
`headers` | object | — | Optional custom request headers. Merged over nio defaults; user entries override (including `Authorization`).
`enabled` | boolean | `true` | Set false to skip an entry without removing config.
`weight` | number | `1.0` | Aggregation weight in `final_score = Σ(wᵢ × sᵢ) / Σ(wᵢ)`.
`timeout` | number | `3000` | Per-request timeout (ms). Exceeded → diagnostic.
`auth` | object | none | Optional. `{ type: 'bearer', api_key }` or `{ type: 'oauth', oauth_url, client_id, client_secret }`. OAuth uses the `client_credentials` grant against `{oauth_url}/token` — pre-register the client with the OAuth provider. OAuth failures → diagnostic.

### `guard.allowed_commands`

Type: string[] · Default: `[]`

Phase 1 allowlist. Command prefixes treated as safe — additional to the built-in list (`ls`, `git status`, `npm test`, etc.).

    allowed_commands: ["npm run", "pnpm "]

### `guard.allowlist_mode`

Type: string · Default: `continue` · Values: `continue` | `exit`

What happens when a command matches an allowlist entry. `continue` records a hint but still runs Phases 2–6 (so external/LLM policy can't be bypassed). `exit` returns immediately — fastest path, skips later phases.

### `guard.permitted_tools`

Type: object · Default: `{ claude_code: [], codex: [], openclaw: [], hermes: [], mcp: [] }`

Phase 0 strict allowlist, per platform. When non-empty for a namespace, ONLY listed tools pass on that platform; everything else is denied. The `mcp` key is platform-agnostic and applies whenever the incoming tool is an MCP tool.

MCP entries accept either a bare local name (`HassTurnOn` — matches any MCP server; also matches Hermes's flattened single-underscore form when listed verbatim, e.g. `mcp_config_db_get_current_config`) or server-qualified (`hass__HassTurnOn` — that server only). List tool names exactly as they appear in the hook payload — no prefix stripping. Matching is case-insensitive.

The `mcp` list also catches indirect MCP invocations embedded in shell commands — Phase 0 unwraps the command (16 layers covering shells, heredocs, evals, encodings, package wrappers, ssh/docker exec, …) and runs detectors against every fragment (mcporter, curl/wget/HTTPie, language-runtime one-liners, TCP/socket multiplexers, stdio JSON-RPC pipes, package runners, etc.), mapping each hit back to `{server, tool}` via the MCP server registry. The full capture model lives at Phase 0 — Tool Gate · MCP Tool Routing.

### `guard.blocked_tools`

Type: object · Default: `{ claude_code: [], codex: [], openclaw: [], hermes: [], mcp: [] }`

Same shape as `permitted_tools`; listed tools are unconditionally blocked. `mcp` covers MCP tools on every platform in one place. Takes precedence over `permitted_tools`.

### `guard.mcp_servers`

Type: object · Default: `{}`

Manual override / addition for the MCP server registry. Phase 0 auto-discovers servers from `~/.claude.json`, Claude Desktop, `~/.hermes/config.yaml`, `~/.codex/config.toml`, and `~/.openclaw/openclaw.json`; this field is for SaaS or otherwise non-discoverable servers. Each entry's key is the server name; its value indexes the server by every reachable handle so detectors can route indirect calls back to it.

    guard:
      mcp_servers:
        hass:
          urls:        ['http://localhost:5173/mcp', 'wss://saas.example/mcp']
          sockets:     ['/tmp/mcp-hass.sock']
          binaries:    ['mcp-server-hass']
          cliPackages: ['@hass/mcp-cli']

All four arrays are optional. URLs are normalized (case-insensitive host, trailing slash stripped); origin matching covers any sub-path of the registered URL. Binary and CLI-package lookups are case-insensitive and basename-aware. See Phase 0 — Tool Gate · MCP Server Registry for full semantics.

### `guard.native_tool_mapping`

Type: object

Native tool → action type classification, per platform. Not a third allow/deny list — this is the table that decides which Phase 1–6 rule set runs for each platform-native tool. Mapped tools enter Phase 1–6 with the chosen action type. Unmapped tools follow a fallback chain in the hook engine: tools that match an MCP convention are dispatched as `mcp_tool_call` and flow through Phase 1–6 (Phase 3 runs over the JSON-serialised args; Phase 5 LLM and Phase 6 external scorers receive the same payload); any other unmapped tool is allowed through with an `UNCATEGORIZED_TOOL` audit entry, skipping Phase 1–6.

    native_tool_mapping:
      claude_code:
        Bash: exec_command
        Write: write_file
        Edit: write_file
        WebFetch: network_request
        WebSearch: network_request
      codex:
        Bash: exec_command   # Codex's only native tool — writes/reads/fetches go through shell
      openclaw:
        exec: exec_command
        write: write_file
        web_fetch: network_request
        browser: network_request
      hermes:
        terminal: exec_command
        exec: exec_command
        shell: exec_command
        write_file: write_file
        patch: write_file
        read_file: read_file
        fetch: network_request
        http_request: network_request

### `guard.scoring_weights`

Type: object

Per-phase weights for the final score aggregation. `final = Σ(wi × si) / Σ(wi)` across phases that ran.

Field | Default | Phase
--- | --- | ---
`runtime` | `1.0` | Phase 2 — pattern matching
`static` | `1.0` | Phase 3 — static rules on file content
`behavioural` | `2.0` | Phase 4 — AST dataflow
`llm` | `1.0` | Phase 5 — LLM semantic

Phase 6 weights are per-endpoint, configured on each `external_analyser[].weight` rather than here.

## collector

Telemetry. Disabled when `endpoint` is empty AND all local options are off.

See Collector Signals for the full per-instrument / per-span / per-event schema that lands in OTLP and the local audit log.

### `collector.endpoint`

Type: string · Default: `"http://localhost:4318"`

OTLP base URL. `/v1/traces`, `/v1/metrics`, `/v1/logs` are appended automatically per signal. `/nio doctor` validates the schema only — it does not probe the endpoint, since collector gateways are commonly gated by routing headers (e.g. `x-event-pipeline-id`) that a bare reachability probe wouldn't include. Export failures surface at runtime as `collector / otlp_export_failed`.

### `collector.api_key`

Type: string · Default: `""`

Bearer token for OTLP endpoint authentication.

### `collector.headers`

Type: object · Default: `{}`

Extra request headers sent to the OTLP endpoint. Values are stringified before export.

    collector:
      headers:
        x-event-pipeline-id: "YOUR_PIPELINE_ID"

### `collector.timeout`

Type: number · Default: `5000`

OTLP export timeout in milliseconds.

### `collector.protocol`

Type: string · Default: `http` · Values: `http` | `grpc`

Transport. `http` uses HTTP/JSON on port 4318; `grpc` uses gRPC on port 4317.

### `collector.metrics`

Field | Type | Default | Note
--- | --- | --- | ---
`enabled` | boolean | `true` | OTLP metrics export.

Metrics are exported via OTLP only — there is no local metrics file. Hook event audit records live in the audit log (see `collector.logs`).

### `collector.traces`

Field | Type | Default | Note
--- | --- | --- | ---
`enabled` | boolean | `true` | Spans for tool calls, turns, tasks.

### `collector.logs`

Field | Type | Default | Note
--- | --- | --- | ---
`enabled` | boolean | `true` | OTEL Logs export.
`local` | boolean | `true` | Local JSONL backup.
`path` | string | `"~/.nio/audit.jsonl"` | The file `/nio report` reads.
`max_size_mb` | number | `100` | Rotation threshold.

## Environment variables

Variable | Default | Effect
--- | --- | ---
`NIO_HOME` | `~/.nio` | Where Nio reads `config.yaml`, writes audit log + metrics.
`CLAUDE_CONFIG_DIR` | `~/.claude` | Used by `setup.sh --cc-home` resolution.
`OPENCLAW_STATE_DIR` | `~/.openclaw` | Used by `setup.sh --openclaw-home` resolution.