> ## Documentation Index
> Fetch the complete documentation index at: https://comis-feature-skill-archive-import.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Node Permissions

> Node.js permission model integration reference for child process sandboxing

Comis integrates with the Node.js `--permission` flag to provide OS-level sandboxing for child processes spawned during skill execution. When enabled, child processes run with restricted file system and network access.

<Info>
  **Source:** `packages/core/src/config/schema-security.ts` -- `PermissionConfigSchema` Zod schema.
</Info>

## Overview

The Node.js permission model (available in Node.js 22+) restricts what a child process can access at the operating system level. This provides a defense layer beneath application-level controls like tool policies and content scanning.

**Key distinction:** Node.js's permission model is a process-level boundary. To enforce restrictions on child processes spawned during tool execution, the daemon process itself must be launched with `--permission` (children inherit via `NODE_OPTIONS`). Comis does not auto-inject these flags from config -- they must be supplied on the daemon command line. See "How It Works" below for the operator-driven flow and the MCP exception that strips `NODE_OPTIONS` for compatibility.

## Configuration

The `PermissionConfigSchema` has 3 fields:

| Field                   | Type       | Default | Description                                                                              |
| ----------------------- | ---------- | ------- | ---------------------------------------------------------------------------------------- |
| `enableNodePermissions` | `boolean`  | `false` | Enable Node.js `--permission` flag enforcement for child processes                       |
| `allowedFsPaths`        | `string[]` | `[]`    | Allowed filesystem read/write paths (passed as `--allow-fs-read` and `--allow-fs-write`) |
| `allowedNetHosts`       | `string[]` | `[]`    | Allowed network hosts for outbound connections (passed as `--allow-net`)                 |

### YAML Configuration Example

```yaml theme={null}
security:
  permission:
    enableNodePermissions: true
    allowedFsPaths:
      - /home/comis/workspace
      - /tmp/comis
    allowedNetHosts:
      - api.openai.com
      - api.anthropic.com
```

## How It Works

<Warning>
  **Operator-driven enforcement.** Comis exposes the configuration surface
  (`security.permission.*`) but does not itself launch the daemon with
  `--permission`. Node.js applies the permission model only to the process
  that was launched with the `--permission` flag (and any children that
  inherit it via `NODE_OPTIONS`). To enforce these restrictions you must:

  1. Launch the daemon under Node.js 22+ with `node --permission --allow-fs-read=... --allow-fs-write=... --allow-net=... packages/daemon/dist/daemon.js`.
  2. Mirror the same paths/hosts in `security.permission.allowedFsPaths` and `allowedNetHosts` so audit/security checks (e.g. the SSRF surface check in `packages/cli/src/security/checks/ssrf-surface.ts`) report the surface accurately.

  Setting `enableNodePermissions: true` alone does NOT pass `--permission`
  flags — it is a configuration declaration consumed today only by the
  security audit checker and admin UI. Always launch with the flags.

  Note: MCP stdio child processes are explicitly wrapped with
  `/usr/bin/env -u NODE_OPTIONS …` (see `packages/skills/src/integrations/mcp-client.ts`)
  to prevent NODE\_OPTIONS-propagated permissions from breaking unrelated
  servers; design those MCP servers under the assumption they may run
  unsandboxed.
</Warning>

When the daemon process is launched with `--permission`:

1. **File system access:** `allowedFsPaths` should mirror the `--allow-fs-read` / `--allow-fs-write` flags supplied to the daemon. Paths not in this list are inaccessible to the daemon and any child processes that inherit `NODE_OPTIONS`.

2. **Network access:** `allowedNetHosts` should mirror `--allow-net`. Only connections to listed hosts are permitted.

3. **Child process spawning:** Child Node processes inherit the daemon's permission flags via `NODE_OPTIONS` unless explicitly stripped (as the MCP launcher does).

### Permission Flag Mapping

| Config Field      | Node.js Flag                          | Effect                                                                                       |
| ----------------- | ------------------------------------- | -------------------------------------------------------------------------------------------- |
| `allowedFsPaths`  | `--allow-fs-read`, `--allow-fs-write` | Restricts file system access to listed paths (operator must pass the flags at daemon launch) |
| `allowedNetHosts` | `--allow-net`                         | Restricts outbound network to listed hosts (operator must pass the flag at daemon launch)    |
| (implicit)        | `--permission`                        | Must be set on the daemon launch command; not auto-injected by Comis                         |

## Limitations

<Warning>
  The Node.js permission model has several limitations to be aware of before enabling it.
</Warning>

* **Node.js 22+ required:** The `--permission` flag is not available in older Node.js versions. Comis requires Node.js 22+, so this is satisfied by default.
* **Third-party module compatibility:** Some npm packages that perform unrestricted file system or network operations will fail when permissions are enabled. Test thoroughly before enabling in production.
* **No per-tool granularity:** Permissions apply to the entire child process. You cannot grant different file system paths to different tools within the same execution.
* **Path resolution:** Paths in `allowedFsPaths` should be absolute. Relative paths may not resolve as expected in the child process context.
* **No wildcard support:** The Node.js permission model does not support glob patterns in path specifications. Each directory must be listed explicitly.

## Error Behavior

When a child process attempts an operation that violates its permissions, Node.js throws a `ERR_ACCESS_DENIED` error. This error propagates through the tool execution system and is reported to the agent as a tool failure.

Example error when a child process attempts to read a file outside `allowedFsPaths`:

```
Error [ERR_ACCESS_DENIED]: Access to this API has been restricted.
```

The daemon logs these permission violations at WARN level with the restricted operation details for security audit.

## Default Behavior

When `enableNodePermissions` is `false` (the default), child processes inherit the full permissions of the daemon process. This is the recommended setting for most deployments where the daemon runs as a dedicated system user with appropriate OS-level access controls.

Enable Node.js permissions only when you need an additional sandboxing layer -- for example, when running untrusted skills that may attempt to access files or network resources outside their intended scope.

## Recommended Paths

When enabling permissions, include at minimum:

| Path                           | Purpose                              |
| ------------------------------ | ------------------------------------ |
| Data directory (`~/.comis`)    | Database, config, logs               |
| Agent workspace directory      | File operations from tools           |
| `/tmp` (or a subdirectory)     | Temporary files for media processing |
| Node.js installation directory | Module resolution                    |

```yaml theme={null}
security:
  permission:
    enableNodePermissions: true
    allowedFsPaths:
      - /home/comis/.comis
      - /home/comis/workspace
      - /tmp/comis
      - /usr/lib/node_modules
    allowedNetHosts:
      - api.openai.com
      - api.anthropic.com
      - api.groq.com
```

## Interaction with Other Security Layers

Node.js permissions are one layer in Comis's defense-in-depth approach:

| Layer                        | Scope             | Mechanism                                              |
| ---------------------------- | ----------------- | ------------------------------------------------------ |
| **Tool Policy**              | Application-level | Controls which tools an agent can use                  |
| **Content Scanner**          | Skill-level       | Detects malicious patterns in skill bodies             |
| **SSRF Guard**               | Network-level     | Blocks requests to internal/metadata IPs               |
| **Safe Path**                | File-level        | Prevents path traversal within allowed directories     |
| **Node Permissions**         | OS-level          | Restricts child process file system and network access |
| **Budget / Circuit Breaker** | Resource-level    | Limits token spend and failure cascades                |

Node.js permissions provide the lowest-level defense: even if all application-level checks are bypassed, the OS-level restrictions prevent access to unauthorized resources.

## SecurityConfigSchema Context

The `PermissionConfigSchema` is nested within the broader `SecurityConfigSchema`:

```yaml theme={null}
security:
  logRedaction: true              # Structured log redaction (default: true)
  auditLog: true                  # Audit event logging (default: true)
  permission:                     # Node.js permission model
    enableNodePermissions: false
    allowedFsPaths: []
    allowedNetHosts: []
  actionConfirmation:             # Action confirmation gates
    requireForDestructive: true
    requireForSensitive: false
    autoApprove: []
  storage: encrypted              # Secrets store backend: encrypted (default) | file | env
```

The full `SecurityConfigSchema` is defined in `packages/core/src/config/schema-security.ts` and parsed via Zod with defaults for all fields.

## Production fd-API Disablement (Linux)

When Comis runs under `node --permission`, Node.js categorically disables the file-descriptor-based fs API family at the V8/libuv level — **this applies to the daemon process itself**, not just child processes. This is distinct from the path-restriction behavior described above and affects how the daemon writes credential files and sets file permissions.

Understanding this behavior matters if you enable `security.permission.enableNodePermissions: true` or if you run the daemon via the reference systemd unit (which passes `--permission` in `ExecStart`).

### Disabled APIs

Under `node --permission`, the following APIs are refused with `ERR_ACCESS_DENIED`:

| API                                                       | Impact                                                                             |
| --------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `fs.fsyncSync` / `fs.fdatasyncSync` / `FileHandle.sync()` | Credential file writes (secrets, OAuth profiles) skip the fsync durability barrier |
| `fs.fchmodSync`                                           | Trace files and session metadata cannot have permissions set after write           |
| `fs.fchownSync`                                           | Ownership changes on daemon-managed files are skipped                              |

Each refusal throws: `Error [ERR_ACCESS_DENIED]: ... is disabled when permission model is enabled`

### Production impact

On a production VPS running under `--permission`, the data-dir lock's `fsync` can kill every boot attempt — every daemon start throws `ERR_ACCESS_DENIED` before the first log line. `fchmod` refusals likewise break MCP OAuth discovery (no OAuth-protected MCP server can connect) and session-metadata writes. ([Source: `packages/shared/src/fsync-permission.ts:15-22`](https://github.com/comisai/comis))

Both failure modes stem from the fd-API categorical disablement rather than missing path grants.

### How Comis guards against this

Comis guards all fd-API call sites via `isFsyncDisabledByPermissionModel` (defined in `packages/shared/src/fsync-permission.ts:33`). This function detects `ERR_ACCESS_DENIED` by error code or the categorical error message and returns `true`, allowing callers to skip the failing call gracefully rather than crashing.

Guarded call sites:

* `packages/daemon/src/wiring/data-dir-lock.ts` — `fsyncSync` on the lock file and parent directory (boot-critical)
* `packages/core/src/oauth/oauth-credential-store-file.ts` — `fsyncSync` on OAuth profile atomic writes
* `packages/memory/src/file-secret-store.ts` — `fsyncSync` on the secrets file atomic writes
* `packages/cli/src/sync-tooling/atomic-write.ts` — `fsyncSync`
* `packages/observability/src/shared/fs-safe.ts` — `fchmodSync` on trace files and session metadata
* `packages/skills/src/tools/builtin/terminal-driver/terminal-worker-entry.ts` — fd-based API

### Operator implications

* **Best-effort durability:** Under `--permission`, encrypted-mode credential file writes (secrets, OAuth profiles) are `tmp → rename` atomic but **not fsync-durable**. A kernel panic or power failure between `rename` and the next journal flush could corrupt the credential file. This risk is identical to writes on any filesystem without journaling guarantees — acceptable for most production deployments, but notable for high-availability setups with unreliable power.
* **Best-effort permissions:** Trace files and session metadata may not have the expected `0o600` mode under `--permission`. Files are still written correctly; only the post-write `chmod` is skipped.
* **Linux-only:** This behavior only occurs on production systemd deployments that pass `--permission`. macOS default setups and development installs (without `--permission`) are unaffected.
* **No action required by default:** `security.permission.enableNodePermissions` defaults to `false`. You must explicitly opt in. If you do not enable it, none of this applies.

<Note>
  If you see `ERR_ACCESS_DENIED` errors in daemon logs that are NOT related to child-process file access, check whether the operation involves one of the fd-based APIs above. The `isFsyncDisabledByPermissionModel` guard handles the known call sites, but third-party Node.js libraries loaded by the daemon may also use fd-based APIs.
</Note>

## Keyless local STT (whisper) engine

The keyless **in-process** local speech-to-text engine (whisper, run via [Transformers.js](https://huggingface.co/docs/transformers.js)) needs **no new `--permission` flag** — the daemon's existing launch flags already cover it:

| What the engine does                          | Already covered by                   | Why                                                                                                                                                                                                     |
| --------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Loads ONNX Runtime (a native addon)           | `--allow-addons`                     | The same flag that already permits `sharp` and `better-sqlite3`. ONNX Runtime is just another native addon.                                                                                             |
| Falls back to a WebAssembly ONNX backend      | (no flag)                            | `--jitless` and `MemoryDenyWriteExecute` are deliberately **not** set on the reference daemon launch — both break WebAssembly (bundled `undici` already needs WASM), so the WASM ORT fallback runs too. |
| Auto-downloads the whisper model on first use | `--allow-fs-write=${COMIS_DATA_DIR}` | The model is cached under the data dir at `~/.comis/models/whisper/`, which is inside the existing data-dir write scope — no extra `--allow-fs-write` entry is required.                                |

So enabling local STT requires **zero changes** to the daemon's permission flags. If a future hardening pass does a defensive fd-based fs op on the cached model (e.g. a post-write `chmod`), it routes through `isFsyncDisabledByPermissionModel` (the same guard documented above for `fsync`/`fchmod`), so it degrades gracefully under `--permission` rather than throwing.

The auto-downloaded model carries no secret and no credential. See [Voice — Transcription](/media/voice#transcription-speech-to-text) for the engine's behavior and [Data Directory — `models/whisper/`](/operations/data-directory#models-whisper) for the cache.

<CardGroup cols={2}>
  <Card title="Security Model" icon="shield" href="/security">
    Defense-in-depth security architecture
  </Card>

  <Card title="Sandbox" icon="box" href="/reference/sandbox">
    Skill sandboxing implementation
  </Card>

  <Card title="Safe Path" icon="folder-closed" href="/reference/safe-path">
    Path traversal prevention
  </Card>
</CardGroup>
