# Sandbox and permissions

> Reference for Fermix's workspace sandbox: filesystem modes, command profiles, the hardline blocklist, environment passthrough, path grants, and the /sandbox command.

The sandbox is the safety layer that decides what the agent is allowed to touch on your machine. It sits under every tool call that touches the filesystem, runs a shell command, or spawns a subprocess. It decides "allowed or denied?" based on three axes: the **mode** (which filesystem roots are reachable), the **command profile** (which preset commands the agent gets), and the **hardline blocklist** (patterns refused regardless of any other setting). Additionally, outbound web fetches are filtered by a network guard that rejects private and loopback targets (loopback meaning your own machine, such as `localhost`).

## Enforcement entry points

Four public functions gate all sandbox-relevant operations:

| Function | Purpose | Called by |
|---|---|---|
| `read_path/3` | Validate a path for reading. Resolves symlinks, checks protected paths and effective roots. | `file_read`, `content_search`, `glob_search`, `git_read`, `send_attachment` |
| `write_path/3` | Same as `read_path` but for writes; also blocks writes to Fermix's own config files. | `file_write`, `file_edit`, `git_write`, `skill_create` |
| `working_dir/3` | Resolve and validate a working-directory request before a shell command runs. | `shell`, `git_*` |
| `shell_plan/3` | Full pre-execution check for shell commands: hardline classifier, working-directory validation, environment construction. | `shell` |

Every decision emits a `[:fermix, :sandbox, :decision]` telemetry event carrying the policy class, operation, agent name, and conversation key. Denied and hardline decisions are persisted to `~/.fermix/traces/YYYY-MM-DD/sandbox_event.jsonl`; allowed decisions fire the event but are not written to disk (the trace file is a record of what was refused). See [traces and telemetry](/docs/traces-and-telemetry).

## Modes

The mode controls the **effective roots**: the set of filesystem subtrees the agent can access. Set it in `~/.fermix/config.toml` under `[sandbox]`.

| Mode | Effective roots | When to use |
|---|---|---|
| `strict` | The configured `workspace_root` plus any explicit `allowed_roots` grants. Nothing else in `$HOME` is reachable. System roots (`/etc`, `/usr`, `/bin`, `/sbin`, `/System`, `/Library`) are always excluded. | Production installs, untrusted skills, or remote channels where the agent should touch only its own working copy. |
| `standard` (default) | `workspace_root`, plus the directory the daemon itself was launched from, plus the working directory the turn came from (the directory you ran `fermix ask` from) — those last two admitted only when strictly inside `$HOME` — plus any `allowed_roots` grants. The request directory is admitted only on trusted local operator turns; guests and remote senders get the workspace, the daemon's launch directory, and grants only. `$HOME` itself is never admitted as a root, and a symlinked directory is resolved before the check. | Developer laptops. The agent works where you are — the directory you invoke it from is reachable without a grant. |
| `open` | The entire `$HOME`. System roots remain excluded. | Single-user machines where the operator explicitly wants the agent to roam. Avoid combining with exposed remote channels; if you do, pair it with a strict ingress policy (ingress = who is allowed to send messages to the agent). |

**The mode affects only the filesystem, shell, and git tools — not plugins.** Switching modes changes which filesystem paths the built-in file, shell, and git tools can reach, and (as a derived posture) what `computer_use` may do on the desktop. It does **not** restrict anything else: plugins, `web_search`, memory, scheduled jobs, and provider/LLM calls are each governed by their own mechanism, not by the mode. A Slack or Notion plugin behaves identically in `strict` and in `open`. Plugins that make web requests are bounded instead by the always-on [network guard](#outbound-network-guard) and by the OAuth scope or API key you granted them — never by the sandbox mode.

**Protected paths are always blocked, regardless of mode or grant.** Even in `open` mode — and even inside a directory you granted — the agent cannot read or write:

- `~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.docker`, `~/.kube`, `~/.codex`, `~/.anthropic`
- Fermix's own `config.toml`, `auth.json`, `grants/`, `logs/`, `traces/`, `memory.db`, `daemon.sock`

### Migrated mode names

An earlier release renamed all mode identifiers. The old names raise an explicit error (with the new name in the message) instead of silently mapping:

| Old name | New name |
|---|---|
| `workspace` | `strict` |
| `developer` | `standard` |
| `trusted_local` | `open` |

## Command profiles

The command profile is the master switch for **preset command tools** — named local commands the agent can call as first-class tools, in addition to the always-on `shell` primitive. The profile only decides whether preset commands are available at all; which presets are active is a separate choice (`presets`, below).

| Profile | Effect |
|---|---|
| `bare` (default) | No preset commands. The agent gets only `shell` (gated by mode and hardline) and the pure-Elixir tools. |
| `assistant` | Preset commands are available. Turn on the presets you want (`ai_tools`, `dev_tools`). |
| `extended` | Same as `assistant` today — preset commands are available. Reserved as the "power user" label. |

Two presets ship. Enable a preset with `fermix sandbox commands enable ai_tools`, or list it under `[sandbox.commands] presets`. A preset command only registers if its executable is actually found on the host, so an install without `gh` simply won't expose a `gh` tool.

| Preset | Commands it registers | Environment passed through |
|---|---|---|
| `ai_tools` | `codex`, `claude_code` (the `claude` CLI) | `OPENAI_API_KEY` for `codex`, `ANTHROPIC_API_KEY` for `claude_code` |
| `dev_tools` | `git`, `gh`, `make` | none |

Calling such a command makes the sandbox validate the working directory and pass through the named environment variables. The agent sees `codex` or `claude_code` as tool names, not raw shell invocations.

To register a one-off command that isn't in a preset, declare it under `[sandbox.commands.<name>]` (or run `fermix sandbox grant command NAME -- CMD [ARGS...]`). Explicitly declared commands register regardless of the profile — even under `bare`.

### Migrated profile names

| Old name | New name |
|---|---|
| `none` | `bare` |
| `trusted` | `extended` |

## Hardline blocklist

The hardline classifier runs on the command string before any execution. It refuses the following patterns regardless of mode, profile, or grants:

- Recursive delete of a protected root (`rm -rf /`, `$HOME`, `/etc`, `/usr`, `/bin`, `/sbin`, `/System`, `/Library`)
- Filesystem formatting (`mkfs`)
- Block-device write via `dd of=/dev/...`
- Fork bomb (`:(){ :|:& };:`)
- Mass kill (`kill -9 -1`, `pkill -u $USER`)
- System shutdown (`shutdown`, `reboot`, `poweroff`, `systemctl poweroff`, `init 0`)
- Sudo password via stdin (`sudo -S`)

The classifier is purely heuristic and catches common foot-guns the model might generate. It is not a complete security boundary. The primary boundary is the mode plus protected-path policy.

## Environment passthrough

Child processes always inherit a small fixed baseline — `PATH`, `HOME`, `USER`, `LANG`, `SHELL`, `TMPDIR`, and any `LC_*` variables. Nothing else is passed unless you opt in through the `[sandbox.env]` block:

```toml
[sandbox.env]
mode  = "selected"        # or "all"
allow = ["EDITOR", "LANG", "TZ"]
deny  = ["AWS_SECRET_ACCESS_KEY", "AWS_ACCESS_KEY_ID"]
```

When `mode = "all"` and `allow` is empty, every parent environment variable is passed except those named in `deny`. `deny` matches variable names exactly — wildcards are not supported, so list each name you want withheld. Listing names under `allow` narrows the passthrough to just those names even under `mode = "all"`. When `mode = "selected"` (default), only names listed in `allow` are passed.

### Environment sources

Each allowed variable can pull its value from one of two sources:

| Source kind | Config | Behavior |
|---|---|---|
| `env` | `source = "env"` | Reads the named variable from the parent process environment at request time. |
| `command` | `source = "command"` | Runs a bounded OS command and captures its stdout. Used to integrate with macOS Keychain (`security`), `secret-tool`, `pass`, `op`, and similar credential helpers. |

Example using macOS Keychain:

```toml
[sandbox.env.GITHUB_TOKEN]
source     = "command"
command    = "security"
args       = ["find-generic-password", "-w", "-s", "fermix-gh"]
timeout_ms = 3000
```

The equivalent CLI form is `fermix sandbox env set GITHUB_TOKEN -- security find-generic-password -w -s fermix-gh`. The `command` source runs through an internal command runner that kills the process tree on timeout (default 3 seconds). The helper's output must be non-empty, a single line, and at most 8 KB, or the variable is rejected with a specific error.

## Path grants

Grants extend the sandbox surface on a per-path or per-command basis. A path grant adds a root to the effective roots — it is unioned with whatever the mode already provides. A command grant registers an explicit command capability regardless of the command profile. Grants only widen; they never override the hardline blocklist, the protected paths, or `blocked_roots`, each of which still wins inside a granted root.

### Agent-requested access

When a filesystem operation is denied for being outside the sandbox roots and the task genuinely needs that directory, the agent can ask you to approve it with the `request_directory_access` tool instead of just failing. You get the canonical path, the agent's reason, and the exact config diff, plus a `/confirm TOKEN` that expires in 60 seconds. Confirming persists the path to `[sandbox] allowed_roots`, and on a chat channel your original request resumes automatically.

On Telegram and Discord the prompt also carries a one-tap **Approve** button; it sends the identical confirmation and rides the same single-use, time-limited, origin-bound, owner-only path. Every other surface shows the same prompt as text. In a shared chat on a button channel the code itself is deliberately withheld — approve from the button, a direct message, or the CLI. Only this agent-requested prompt gets a button; changes you propose yourself still use the typed confirmation.

The tool is offered only on attended, top-level operator turns — guests, scheduled and cron runs, and sub-agents never get an approval path — and it refuses to prompt at all for roots the sandbox would not grant anyway (`$HOME` wholesale, Fermix's own home, `~/.ssh`, OS roots).

### CLI grant commands

```bash
fermix sandbox grant path /path/to/dir        # add a path to allowed_roots
fermix sandbox revoke path /path/to/dir       # remove it
fermix sandbox grant command NAME -- CMD ARGS # register an explicit command capability
fermix sandbox revoke command NAME            # remove it
```

`fermix grant` and `fermix revoke` are top-level shortcuts for `fermix sandbox grant` / `fermix sandbox revoke`.

The CLI has the fuller local view. `fermix sandbox status` prints the mode, workspace root, allowed- and blocked-root counts, env passthrough count, and the command profile and preset count. `fermix sandbox explain` prints the annotated effective roots plus the full allowed-root, blocked-root, and protected-path lists.

### Channel commands

The `/sandbox`, `/grant`, `/revoke`, and `/confirm` slash commands work on any connected channel. The `/sandbox` subcommands are **owner-only** (see [ingress and trust](/docs/ingress-and-trust)).

`/grant`, `/revoke`, and `/confirm` are held to a stricter bar: they require the channel owner or a local CLI/daemon caller, and never honour `command_allowlist`. A guest you allowlisted to run other slash commands can never expand the sandbox or approve a pending grant.

| Command | Effect |
|---|---|
| `/sandbox status` | Print current mode, workspace root, allowed-root count, and env passthrough count. |
| `/sandbox explain` | Print the mode, the effective roots — each annotated `(granted)` for an explicit grant or `(mode)` for a root the mode provides — and the passthrough env names. |
| `/sandbox mode MODE` | Propose a mode change (requires `/confirm TOKEN`). |
| `/sandbox env allow NAME` | Allow env variable `NAME` to pass through. |
| `/sandbox env deny NAME` | Remove env variable `NAME` from the passthrough list. |
| `/sandbox env set NAME -- CMD [ARGS...]` | Set variable `NAME` from a command source. |
| `/sandbox env unset NAME` | Alias for `env deny`. |
| `/sandbox commands enable PRESET` | Enable a command preset (requires `/confirm TOKEN`). |
| `/sandbox commands disable PRESET` | Disable a command preset immediately. |
| `/grant path PATH` | Propose adding `PATH` to allowed roots (requires `/confirm TOKEN`). |
| `/revoke path PATH` | Remove a path grant immediately. |
| `/confirm TOKEN` | Confirm a pending sandbox mutation, whether you proposed it yourself or the agent requested it. Tokens expire after 60 seconds. |

Mutations that expand the sandbox surface (mode changes, new allowed roots, enabling presets) produce a confirmation token. The `/confirm` command must come from the same channel, chat, and user within the 60-second window.

## Outbound network guard

The `web_fetch` tool (and any tool that follows redirects) passes every URL through `FermixCore.Net.Guard` before making a connection. The guard blocks:

- Non-HTTP/HTTPS schemes
- `localhost` and `*.localhost`
- `*.local` and `*.internal` hostnames
- IP literals in private ranges: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `127.0.0.0/8`, `169.254.0.0/16`, loopback IPv6 (`::1`), link-local IPv6, and multicast ranges
- DNS names that resolve to any of the above private addresses

To close the DNS-rebinding gap, the guard resolves the hostname before connecting and pins the HTTP connection to the validated IP, while preserving the original `Host` header and TLS SNI. Each redirect target is re-validated before following.

HTTP-based plugins share this same guard. A plugin's outbound API calls are validated against the identical private/loopback/metadata blocklist before connecting, so an installed plugin cannot be turned into a path to your internal network — independent of the sandbox mode.

`web_fetch` also caps response bodies at 1 MB and follows at most 5 redirects.

## Full config reference

```toml
[sandbox]
mode           = "standard"         # strict | standard | open
workspace_root = "/Users/me/.fermix/workspace"
allowed_roots  = ["/Users/me/projects/extra"]
blocked_roots  = ["/Users/me/.aws-experiment"]

[sandbox.env]
mode  = "selected"                  # selected | all
allow = ["EDITOR", "LANG", "TZ"]
deny  = []

[sandbox.commands]
profile = "bare"                    # bare | assistant | extended
presets = ["ai_tools"]              # presets to enable when profile is not "bare"
```

`blocked_roots` entries are always denied even if they fall inside an `allowed_roots` entry or inside the mode's effective roots. `allowed_roots` extends the effective roots beyond what the mode provides.

## Decision error tags

The sandbox returns typed error tuples that tools and the agent loop surface to the user:

| Tag | Meaning |
|---|---|
| `{:protected_path, path}` | Path matched a protected entry (e.g. `~/.ssh`). |
| `{:blocked_root, path}` | Path is under an operator-configured `blocked_roots` entry. |
| `{:outside_root, path}` | Path is outside every effective root for the current mode. |
| `{:too_many_symlinks, path}` | Symlink resolution exceeded 64 hops. |
| `{:hardline, reason}` | Command matched a hardline pattern. |
| `:invalid_path` / `:invalid_request` | Malformed input. |

## computer_use and the sandbox mode

**`computer_use` is experimental and off by default.** It is the most dangerous capability Fermix has — it drives your real, logged-in desktop with synthetic mouse and keyboard input, there is no rollback, and screenshots it captures are treated as untrusted content. Turn it on only when you want the agent to operate the actual machine. The relationship between computer use and the sandbox is what this section covers; for the full action set, see [capabilities and tools](/docs/capabilities-and-tools).

The `computer_use` capability drives the host desktop GUI by screenshot plus mouse and keyboard actions — one action per call, in screenshot pixel coordinates. It is host-desktop control only; there is no browser mode (web automation is the separate `browser` tool). Its **access posture is derived 1:1 from `[sandbox] mode`**: there is no separate computer-use access knob. Whichever sandbox mode is active also governs what computer_use is allowed to do:

| Sandbox mode | computer_use access posture |
|---|---|
| `strict` | Look only. Mutating actions (clicks, typing, key presses) are refused; only screenshots, mouse moves, and waits run. This is the one deterministic floor. |
| `standard` (default) | Acts freely but the agent confirms conversationally before anything irreversible. This is the agent's own judgment, not a blocking gate. |
| `open` | Acts autonomously. Only truly catastrophic actions (mass deletion, sending money, wiping data) trigger a pause for confirmation — a higher bar than standard. |

Read-only actions (screenshots, and the macOS-only accessibility reads) always run regardless of posture.

`computer_use` is operator-only and is never delegated to subagents; its `:gui_control` policy class carries no path enforcement, so all its safety comes from the access posture above plus the attended-origin gate. Host sessions additionally require an **attended origin** — an interactive chat, `fermix ask`, or the voice companion. Scheduled and cron runs fail closed, because a live desktop session must have a watching human.

### Enabling computer use

Enabling is one operator step — set `[fermix_core.computer_use] enabled = true` — and the change applies on the next daemon restart. The native helper that drives the desktop is a separate binary that ships no tools of its own, and you do not install it by hand: it is fetched from the [compux project](https://github.com/tezra-io/compux)'s own signed release and verified against a SHA-256 checksum baked into your Fermix build, then cached under `~/.fermix/plugins/compux/`. `fermix plugins` does not manage it. A daemon that has computer use enabled but is missing the matching helper — the state an upgrade lands in — downloads it on its next start. Supported platforms are **Apple Silicon (M-series) macOS** and **Linux x86_64** only; Intel Macs and other targets fail loudly.

The setup Plugins page (the **Computer Use** card) is the interactive route: it sets the flag and installs the helper on demand. See [computer use](/docs/computer-use) for the bounds on that automatic download and the full enable flow.

The tool only becomes visible to the model once **both** hold — enabled and the helper installed. Operating-system permission state is a separate diagnostic, not part of that check: run `fermix doctor` and read its **computer use** line. The classic trap on macOS is Accessibility not being granted — screenshots keep working but synthetic clicks and keystrokes are silently dropped, so it looks like the tool can only see and never act. The fix the check names is System Settings → Privacy & Security → Accessibility. On Linux, an X11 session is required (Wayland is unsupported).

The `access` posture is deliberately **not** a key under `[fermix_core.computer_use]` — it mirrors `[sandbox] mode` directly. The section's own keys are `enabled`, `display` (which monitor, default `0`), `screenshot_after` (default `true` — every mutating action returns a fresh screenshot so the model can verify the result), `max_actions` (the per-session action budget that halts a runaway, default `80`), `max_retained_screenshots` (default `3`), and the two coexistence keys `courtesy` and `courtesy_idle_ms`, which decide whether the agent steps aside when you are using the machine yourself (see [computer use](/docs/computer-use)).

## Related pages

- [Capabilities and tools](/docs/capabilities-and-tools): the full built-in tool list and capability registry.
- [Configuration](/docs/configuration): complete `config.toml` reference.
- [Ingress and trust](/docs/ingress-and-trust): channel owner and allowlist setup.
- [Traces and telemetry](/docs/traces-and-telemetry): reading sandbox decision logs.
