Skip to main content
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.
Source: packages/core/src/config/schema-security.tsPermissionConfigSchema Zod schema.

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:

YAML Configuration Example

How It Works

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

Limitations

The Node.js permission model has several limitations to be aware of before enabling it.
  • 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:
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. When enabling permissions, include at minimum:

Interaction with Other Security Layers

Node.js permissions are one layer in Comis’s defense-in-depth approach: 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:
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: 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) 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.tsfsyncSync on the lock file and parent directory (boot-critical)
  • packages/core/src/oauth/oauth-credential-store-file.tsfsyncSync on OAuth profile atomic writes
  • packages/memory/src/file-secret-store.tsfsyncSync on the secrets file atomic writes
  • packages/cli/src/sync-tooling/atomic-write.tsfsyncSync
  • packages/observability/src/shared/fs-safe.tsfchmodSync 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.
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.

Keyless local STT (whisper) engine

The keyless in-process local speech-to-text engine (whisper, run via Transformers.js) needs no new --permission flag — the daemon’s existing launch flags already cover it: 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 for the engine’s behavior and Data Directory — models/whisper/ for the cache.

Security Model

Defense-in-depth security architecture

Sandbox

Skill sandboxing implementation

Safe Path

Path traversal prevention