Docs/Concepts/Sandbox and permissions

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.

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

[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:

[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

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

/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

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

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

Next steps