# Configuration reference

> Complete reference for Fermix configuration: the config.toml file, its sections and keys, three-layer precedence, and all environment variable overrides.

All operator-editable configuration lives in `~/.fermix/config.toml` (or `$FERMIX_HOME/config.toml` if you override the home directory). The [setup wizard](/docs/setup) writes this file; the runtime reads it on startup through `FermixCore.Config` and provider-specific config modules. You can edit it by hand or re-run `fermix setup --reconfigure` to regenerate specific sections.

## Configuration precedence

Fermix resolves configuration in three layers, each overriding the one before it:

1. Compile-time defaults (baked into the release in `config/config.exs`)
2. Persisted setup snapshot from `config.toml`
3. Environment variable overrides applied at runtime (`config/runtime.exs`)

Environment variables win over everything. This means a value set in `config.toml` can be silently superseded by an env var of the same semantic, so check both when debugging unexpected behavior.

## FERMIX_HOME

`FERMIX_HOME` defaults to `~/.fermix`. The setup wizard creates the following layout under it:

```
~/.fermix/
  config.toml          # main configuration file
  auth.json            # OAuth token store (all providers; mode 0600)
  memory.db            # durable memory (SQLite)
  daemon.sock          # socket the CLI uses to talk to the daemon (the background Fermix service)
  realtime.sock        # FermixPet voice companion socket
  bootstrap/           # prompt files (main/{IDENTITY,FERMIX,SOUL,REALTIME}.md)
  memory/              # profile files rebuilt by background memory review
  skills/              # user-installed skills
  plugins/             # installed plugin store
  browser/             # browser profile
  journals/
  realtime/
  traces/              # JSONL trace files
  logs/                # rotating daemon log
  workspace/           # default sandbox workspace root
  grants/              # sandbox grant records
```

The home directory itself is mode `0700`, readable only by you: it holds `memory.db` with full conversation text, `config.toml`, and every trace file. The daemon sets that mode on each boot and each setup save, and logs the failure rather than refusing to start if it cannot. `fermix doctor`'s `home perms` check fails when the mode is anything else and prints the `chmod 700` that fixes it.

Set `FERMIX_HOME` to a different path to run a second Fermix instance (for example, a dev install alongside a production one). Each instance must have its own `config.toml` and, if using Telegram, its own bot token. See [auth and secrets](/docs/auth-and-secrets) for keychain namespacing when running multiple instances.

## `[fermix_core]`

Top-level core settings.

| Key | Default | Notes |
|-----|---------|-------|
| `profile` | `"general"` | Keychain namespace prefix. Set before running `fermix setup` on a non-default install. Changing it after setup orphans existing keychain entries. |

## `[fermix_core.agent]`

Core agent identity settings.

| Key | Default | Notes |
|-----|---------|-------|
| `name` | `"fermix"` | Public-facing agent name used in prompts and channel replies. |

The primary provider is selected by setting `primary = true` in the desired `[fermix_core.providers.<name>]` block, not via a key here. The legacy `provider` key in this section is read-only migration input (used only if no `primary` flag exists anywhere); setup no longer writes it.

## `[fermix_core.providers.<name>]`

One sub-table per provider. Valid names: `openai`, `openai_codex`, `anthropic`, `xai`, `openrouter`, `mistral`, `ollama`.

| Key | Notes |
|-----|-------|
| `primary` | `true`/`false`. Exactly one provider block must have `primary = true`; that provider handles main turns. All other configured providers become ordered fallbacks. |
| `api_key` | Plaintext API key or the `@keyring` sentinel (reads from the OS keychain). After running `fermix setup`, secrets are normally in the keychain and this key is absent or set to `@keyring`. |
| `default_model` | Model identifier. See [providers and models](/docs/providers-and-models) for the full catalog. |
| `reasoning_effort` | How much internal thinking the model does before answering (higher means more thorough but slower and pricier). One of `"none"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`. Applies only to effort-capable providers (OpenAI, OpenAI Codex, Anthropic, xAI). OpenRouter, Mistral, and Ollama blocks reject this key at config load. Anthropic has no `"none"` (floor is `"low"`). |
| `base_url` | Override the default API base URL. Useful for proxies or compatible endpoints. Not accepted in the `openai` block (writing it there fails at config load). |
| `auth_mode` | `"api_key"` or `"oauth"`. Valid only in the `anthropic` and `xai` blocks. `openai` is always API-key; `openai_codex` is always OAuth — neither block accepts this key. |
| `fast` | `openai_codex` only. `true`/`false`. When `true`, Codex requests run on the priority service tier. |

Each provider block only accepts the keys that apply to it. An unknown or misplaced key (for example `reasoning_effort` in an `openrouter`, `mistral`, or `ollama` block) is rejected at config load and the daemon refuses to boot until it is removed. See [providers and models](/docs/providers-and-models) for the full per-provider key list.

Example:

```toml
[fermix_core.providers.openai]
api_key = "@keyring"
default_model = "gpt-5.5"
reasoning_effort = "medium"

[fermix_core.providers.anthropic]
api_key = "@keyring"
default_model = "claude-opus-4-8"
```

## `[fermix_core.personalization]`

Feeds the agent's identity prompt section.

| Key | Notes |
|-----|-------|
| `user_name` | Your name, used in greetings and self-reference. |
| `timezone` | IANA timezone string (e.g. `"America/New_York"`). |
| `communication_style` | Free-form style hint injected into the system prompt. |

## `[fermix_core.compaction]`

Controls automatic conversation compaction: when a conversation grows too long to fit the model's memory, Fermix summarizes the older parts to make room. The trigger uses the provider's reported context (prompt) token count from the turn, not a local estimate. When that count reaches `threshold * model_context_window` (a fraction of the maximum amount of text the model can consider at once), Fermix summarizes older messages and replaces the stored history with the summary plus recent turns.

| Key | Default | Notes |
|-----|---------|-------|
| `enabled` | `true` | Set to `false` to disable automatic compaction. |
| `threshold` | `0.85` | Fraction of the model context window that triggers compaction. |
| `reasoning_effort` | `"medium"` | Reasoning effort the summarization call uses, separate from the main turn's effort. |

The `/compact` channel command triggers immediate compaction on demand. `/new` clears the conversation window without affecting long-term memory, scheduled jobs, or resource revisions. See [prompt and compaction](/docs/prompt-and-compaction) for details.

## `[fermix_core.memory]`

Tuning knobs for the background memory-review ("dreaming") pass — how often it runs, how much it reads, and how it backs off after a failure. Most installs leave this section at defaults. See [memory](/docs/memory).

| Key | Default | Notes |
|-----|---------|-------|
| `review_interval_hours` | `24` | How often background memory review ("dreaming") consolidates facts and rebuilds the injected `USER.md`/`MEMORY.md` profiles. |
| `review_max_messages` | `40` | Maximum recent messages a single review pass reads. |
| `review_input_token_budget` | `4000` | Token budget for the input a review pass sends to the model. |
| `review_failure_backoff_ms` | `300000` | How long to wait before retrying after a failed review (5 minutes). |

## `[fermix_core.jobs]`

Scheduled job runtime settings. See [scheduled jobs](/docs/scheduled-jobs). `default_delivery_target` is shared with [reminders](/docs/reminders): it is a reminder's destination too, so setting it once configures both rails.

| Key | Default | Notes |
|-----|---------|-------|
| `default_delivery_mode` | | How to deliver job results. One of `"none"`, `"origin"`, `"channel"`, `"local"`. |
| `default_delivery_target.platform` | | Channel platform for delivery (e.g. `"telegram"`). Also the destination for reminders, which accept `telegram`, `slack`, `discord`, `signal`, or `whatsapp` but refuse `cli`. |
| `default_delivery_target.chat_id` | | External channel conversation id (e.g. a Telegram chat id). Not a Fermix session id. |
| `network_readiness_enabled` | `true` | When enabled, a job run that fires at host-wake time first waits on a short TCP readiness probe to the primary provider before the first model call. |

Delivery settings are copied into each job at creation time, and onto each reminder when its date is stored. Changing the default afterward retargets neither.

On the reminder side a destination has to resolve before the date is stored, and there are three outcomes. A configured `default_delivery_target` is snapshotted onto the date. With that key absent, Fermix derives your inbox from the first channel configured with an explicit owner user id — Telegram, then Signal, then WhatsApp — and storing succeeds on the first one that resolves. Storing fails loud, with setup guidance naming both remedies, only when there is neither a configured target nor an owner-configured channel to derive from; a configured target that is itself invalid (a `cli` platform, or no destination) fails the same way rather than falling through to derivation.

A `default_delivery_mode` of `"none"`, `"origin"`, or `"local"` is refused ahead of all of that: storing a date returns an invalid-delivery-mode error and no derivation is attempted, because those modes mean background delivery is off. Reminders need `"channel"`, or an unset mode alongside a configured target.

```toml
[fermix_core.jobs]
default_delivery_mode = "channel"

[fermix_core.jobs.default_delivery_target]
platform = "telegram"
chat_id = "8217352118"
```

## `[fermix_core.routing]`

Model routing overrides for sub-agents and scheduled jobs. All three keys apply independently to both `subagent_*` and `cron_*` groups.

| Key | Notes |
|-----|-------|
| `subagent_model` | Model slug (the short identifier for a model, e.g. `gpt-5.4-mini`) for sub-agent runs. |
| `subagent_provider` | Provider for sub-agent runs. When omitted, a bare `subagent_model` runs on the primary provider (the model's catalog owner is used only when no primary is configured) — set this explicitly to run sub-agents on a non-primary provider. |
| `subagent_reasoning_effort` | Reasoning effort for sub-agent runs. |
| `cron_model` | Model slug for scheduled job runs. |
| `cron_provider` | Provider for scheduled job runs. When omitted, a bare `cron_model` runs on the primary provider (the model's catalog owner is used only when no primary is configured) — set this explicitly to run jobs on a non-primary provider. |
| `cron_reasoning_effort` | Reasoning effort for scheduled job runs. |

These can also be set at runtime with the `model_routing_config` tool or via the web setup sub-agent panel on the primary provider's configuration page.

## `[fermix_core.tools.web_search]`

Selects the search backend for the `web_search` built-in tool. When this section is absent, Fermix uses keyless DuckDuckGo, so most installs need nothing here.

| Key | Notes |
|-----|-------|
| `backend` | One of `"duckduckgo"` (default, keyless), `"tavily"`, `"exa"`, `"parallel"`, `"brave"`, `"perplexity"`, `"firecrawl"`. An unrecognized value falls back to DuckDuckGo. |
| `tavily_api_key` / `exa_api_key` / `parallel_api_key` / `brave_api_key` / `perplexity_api_key` / `firecrawl_api_key` | API key for the matching backend. These live in this section and nowhere else: after setup the value is the `@keyring` sentinel and the real secret is read from the OS keychain. Names like `TAVILY_API_KEY` are keychain entry names, not environment overrides — exporting them has no effect. |

If a configured non-DuckDuckGo backend hard-errors (bad key, no credits, transport failure), `web_search` degrades once to DuckDuckGo and logs a warning — it does not silently swallow the failure.

`brave_api_key` does double duty: it also enables the `place_search` built-in ([tool reference](/docs/tool-reference)), which is advertised only while this key resolves — whatever `backend` is selected — and whose lookups Brave bills separately from web-search calls.

## `[fermix_core.tools.tool_search]`

Controls tool-schema deferral, which is on by default. Deferral means the model is not given the full definition of every tool up front; it looks them up as needed, saving tokens. When enabled, plugin and MCP tool schemas (the detailed input descriptions) are left out of requests to the provider (names stay listed in the prompt) and three bridge tools are registered: `tool_search` (keyword search over deferred schemas), `tool_describe` (fetch one schema on demand), and `tool_call` (invoke a deferred tool by name).

| Key | Default | Notes |
|-----|---------|-------|
| `enabled` | `true` | Set to `false` to send all tool schemas inline and remove the bridge tools. |

## `[fermix_core.tools.generate_image]`

Configuration for the `generate_image` built-in tool.

| Key | Notes |
|-----|-------|
| `backend` | Image generation provider. One of `"openai"`, `"openai_codex"`, `"xai"`, `"google"`. |
| `model` | Model identifier for the chosen backend. |
| `size` | Default image size (backend-specific format). |
| `google_api_key` | API key for the Google backend, stored in the OS keychain as `@keyring` after setup. The OpenAI and xAI backends reuse that provider's chat key, and `openai_codex` needs no key at all. |

The `openai_codex` backend runs on a connected ChatGPT/Codex login instead of an image API key — billed to that subscription. It is opt-in and fails loudly if the login is missing or not entitled; it never silently falls back to another backend. The keys in the table above are the only ones this section accepts; any other key stops the daemon from booting.

## `[fermix_core.transcription]`

Speech-to-text settings for inbound voice notes and audio attachments. An unknown key, an unknown backend, or a non-positive `max_file_mb` fails config load loudly.

| Key | Default | Notes |
|-----|---------|-------|
| `backend` | `"openai"` | Speech-to-text provider. One of `"openai"`, `"xai"`, `"deepgram"`. The on-device `"local"` backend is reserved for a later phase; naming it errors clearly. |
| `model` | `"gpt-4o-mini-transcribe"` | Transcription model, a single key shared across backends — switching backend in setup snaps it to the new backend's default (OpenAI `gpt-4o-mini-transcribe`, Deepgram `nova-3`; the xAI backend is modelless and sends no model). |
| `openai_api_key` / `xai_api_key` | | Per-backend API-key override, set in this block. When unset, the OpenAI and xAI backends reuse that provider's chat key — which `OPENAI_API_KEY` or `XAI_API_KEY` can supply. The xAI (SpaceXAI) speech-to-text endpoint requires an API key — a Grok-subscription OAuth token does not work for transcription. |
| `deepgram_api_key` | | Required for the Deepgram backend, which has no chat provider to reuse. Set it in this block. |
| `max_file_mb` | `20` | Audio size cap, enforced against the declared size before download when the attachment carries one, otherwise against the downloaded file. |

Keys are stored in the OS keychain as `@keyring` after setup. Configure from the web setup **Transcription** tab or with `fermix setup --transcription-backend`/`--transcription-model`/`--transcription-api-key` (the generic key flag stores under the selected backend's slot). `fermix doctor` has a `transcription` row reporting the active backend and whether its credential resolves.

## `[fermix_core.computer_use]`

**Experimental and off by default.** Computer use lets the agent drive your real desktop by taking screenshots and moving the mouse and keyboard. It is the most dangerous capability Fermix has — ambient control of your live logged-in session with no undo, and screenshots the model must treat as untrusted content — so it ships off and must be turned on deliberately. See [capabilities and tools](/docs/capabilities-and-tools) for the full safety model.

Turning it on is one operator step: set `enabled = true` in this section and restart the daemon. Computer use needs a small native helper alongside the daemon, but you do not install it by hand. On a real daemon boot, if computer use is enabled and the helper matching your build is missing, Fermix fetches it before the readiness gates run: the helper comes from the compux project's own signed release and is verified against a SHA-256 checksum baked into your Fermix build, then cached under `~/.fermix/plugins/compux/`. That is what keeps computer use working across an upgrade that changes the helper version. It happens on daemon boot only (not during `fermix setup`), is bounded to about 30 seconds, and is fail-soft — a failed fetch leaves computer use off until the next restart or the setup card.

The setup **Plugins** page (the **Computer Use** card) is the interactive route: it installs the helper on demand and reports install and permission status. Computer use is supported only on **Apple Silicon (M-series) macOS** and **Linux x86_64**; Intel Macs are unsupported and the install fails loudly.

The capability is offered to the model only once it is **enabled and the helper binary is installed**. Operating-system permissions (macOS Screen Recording and Accessibility, or X11 versus the unsupported Wayland on Linux) are checked separately by `fermix doctor`'s `computer use` check, not by this flag — Fermix probes the Accessibility grant when a session starts, and while it is missing every mutating action is refused with an error naming the System Settings fix, while screenshots keep working.

| Key | Default | Notes |
|-----|---------|-------|
| `enabled` | `false` | Turn the feature on. Requires a daemon restart. |
| `display` | `0` | Which display to capture, by index. |
| `screenshot_after` | `true` | Return a fresh screenshot after every mutating action so the model can verify the result. |
| `max_actions` | `80` | Per-session action budget — halts a runaway loop. |
| `max_retained_screenshots` | `3` | How many recent screenshots to keep in the model's context. |
| `courtesy` | `"yield"` | Whether the agent steps aside for a present human before an action that would move the pointer or type. `"off"` disables it. Idle detection is macOS-only. Where it is unavailable the action proceeds, so this is a courtesy that keeps the agent from fighting you for the cursor, not a safety boundary. |
| `courtesy_idle_ms` | `1000` | How much input quiet counts as you being idle. |

There is no `mode` key: computer use is host-desktop control only, so a legacy `mode` entry in an old config is ignored. There is also no separate posture key — the tool's `access` posture (whether it can only look, must confirm before irreversible actions, or acts autonomously) is derived 1:1 from `[sandbox] mode`: `strict` looks only, `standard` acts but confirms irreversible actions conversationally, `open` acts autonomously. Computer use is operator-only, never delegated to sub-agents, and a host session starts only from an attended origin (an interactive chat, `fermix ask`, or the voice companion) — scheduled and cron runs cannot start it.

## `[fermix_core.harness]`

Coding agents: the agent hands a repository task to the operator's own Codex CLI or Claude Code CLI and reports the outcome back. The feature ships enabled but inert — no run can start until the owner approves coding agents once per machine on the setup **Coding Agents** tab (or by setting `approved = true` here), and until then every coding-agent tool is withheld from the model. An unknown key in this section refuses boot: config load fails loudly naming the key. Changes apply on the next daemon restart. There are no `fermix setup` flags and no environment-variable overrides for these keys.

| Key | Default | Notes |
|-----|---------|-------|
| `enabled` | `true` | The feature flag; `false` removes the feature entirely. |
| `approved` | `false` | One-time per-machine owner consent. Written by the setup Coding Agents tab. |
| `cloud_enabled` | `false` | Registers the two Codex cloud tools (`codex_cloud_run`, `stop_tracking_coding_run`). |
| `default_vendor` | unset | `"codex"` or `"claude"` only; anything else — including an empty string — fails config load. Filters which run tool is advertised. |
| `max_active` | `2` | Concurrent local runs; a launch past the bound is refused with a typed error. |
| `default_timeout_minutes` | `30` | Wall-clock cap per run; a per-run `timeout_minutes` caps at 240 minutes. |
| `inactivity_minutes` | `10` | No-activity notice threshold; advisory only, the run continues. |
| `prompt_argv_max_kb` | `200` | Prompts above this spill to a brief file instead of the command line. |
| `max_event_bytes` | `1048576` | Cap on a single vendor event (1 MiB). |
| `max_framing_errors` | `20` | Malformed-stream budget before the run fails; `0` means zero tolerance. |
| `max_run_artifact_mb` | `64` | Per-run event-spool cap; a breach truncates and the run continues. |
| `artifact_quota_gb` | `5` | Whole-store disk budget; a breach refuses new runs. |
| `min_free_gb` | `2` | Free-disk floor; `0` disables the probe. |
| `artifact_retention_days` | `30` | Run directories older than this are swept at boot and daily. |
| `delivery_max_attempts` | `20` | Result-delivery retries before dead-lettering. |
| `delivery_max_age_hours` | `24` | Result-delivery age before dead-lettering. |
| `cloud_poll_seconds` | `120` | Cloud status-poll interval. |
| `cloud_poll_max_minutes` | `90` | Cloud poll deadline; past it, tracking stops and the task URL is delivered. |
| `codex_home` | unset | Where the Codex CLI keeps its state; falls back to the daemon's `CODEX_HOME`, then the vendor default. |
| `claude_config_dir` | unset | Mirrors `CLAUDE_CONFIG_DIR`; also re-keys Claude Code's macOS Keychain entry, so set it if you log in under one. |

```toml
[fermix_core.harness]
approved = true
default_vendor = "codex"
```

See [coding agents](/docs/coding-agents).

## `[fermix_core.skill_curation]`

The background pass that reads your own recent messages, spots tasks you repeat, and proposes a [skill](/docs/skills) for them. It proposes only: nothing is written to disk without your approval, and archiving is reversible. One key, because the cadence, budgets, and staleness thresholds are internal constants rather than settings. An unknown key in this section refuses boot, naming the key.

| Key | Default | Notes |
|-----|---------|-------|
| `enabled` | `true` | `false` stops the pass; a pending proposal can still be denied but not approved. The setup wizard's **Personalization** step asks the same yes/no. Requires memory persistence — with memory off, `/skills review` and `/skills approve` report that instead of running, while the rest of the command family still works. |

```toml
[fermix_core.skill_curation]
enabled = true
```

## `[fermix_core.plugins]`

| Key | Notes |
|-----|-------|
| `enabled` | Array of plugin names that are turned on. Managed by `fermix plugins enable`/`disable` and the setup **Plugins** page rather than edited by hand. |
| `dev_local` | Path to a local plugin checkout directory (e.g. a `fermix-plugins` repo's `plugins/` subdirectory). Subdirectories load as plugins without an install step. For development use only. |

Per-plugin configuration lives under `[fermix_core.plugins.<name>]` (manifest-declared values in UPPER_SNAKE form, set with `fermix plugins config set NAME KEY VALUE` and injected into an MCP plugin's process environment).

## `[fermix_core.plugin_secrets]`

Per-plugin static `api_key` credentials, keyed by plugin name. `fermix plugins auth set NAME` prompts for the secret on the terminal without echoing it, and `fermix plugins auth set NAME --stdin` reads it from a pipe; passing the secret as an argument is refused, because an argument is visible in `ps` and lands in your shell history. Either way the secret is stored in the OS keychain and the entry here holds the `@keyring` sentinel rather than plaintext. A plugin that authenticates with a static key reports `needs_secret` until one is set. The Slack, Discord, and AgentMail plugins use this path. See [auth and secrets](/docs/auth-and-secrets).

## `[fermix_core.oauth.<provider>]`

OAuth client credentials for plugin OAuth providers. Valid provider names: `google`, `github`, `notion`, `x`, `slack`.

| Key | Notes |
|-----|-------|
| `client_id` | OAuth application client ID. |
| `client_secret` | OAuth application client secret. Stored in the OS keychain as `@keyring` after setup. |

## Logging and traces

The daemon log (`~/.fermix/logs/fermix.log`, rotated) and the JSONL trace directory (`~/.fermix/traces`, written as `YYYY-MM-DD/<type>.jsonl`) are not configured through `config.toml`. Their paths are set by the release defaults and can be overridden only with the `FERMIX_LOG_FILE` and `FERMIX_TRACE_DIR` environment variables (see the [environment variables](#environment-variables) table below). See [traces and telemetry](/docs/traces-and-telemetry).

## `[fermix_core.realtime]`

Local Realtime voice companion configuration. The keys below correspond to flags accepted by `fermix setup --reconfigure --realtime-enabled ...`. Removed keys that will raise on startup: `tool_policy`, `allow_network_tools`, `turn_detection`, `activation`, `max_buffer_chunks`, `idle_timeout_ms`, `max_input_audio_seconds_per_session`.

| Key | Notes |
|-----|-------|
| `enabled` | `true`/`false`. When `true`, the daemon opens a `0600` Unix-domain socket at `~/.fermix/realtime.sock` on startup. |
| `provider` | Only `"openai"` is supported. |
| `model` | OpenAI Realtime model identifier. |
| `voice` | Voice name (e.g. `"marin"`). |
| `reasoning_effort` | Reasoning effort for the voice agent, sent on the OpenAI Realtime `session.update` (nested under `reasoning`). One of `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`. Defaults to `"low"`. Also selectable in the web setup **Realtime** pane; there is no `fermix setup` flag for it. |
| `max_session_minutes` | Per-session duration cap. |
| `max_estimated_cost_cents_per_session` | Per-session estimated cost cap in cents. |
| `persist_transcripts` | When `true`, stores final spoken text as `voice_turn` memory rows with `source_type = "realtime"`. Defaults to `false`. |
| `screen_share` | `true`/`false`. Whether a call may watch your screen continuously — started on your request, or by the agent itself when the task you bring to the call concerns your screen (it always announces the start, and "stop watching" or hanging up ends it). Defaults to `true`, and means nothing unless [computer use](/docs/computer-use) is enabled and installed — the feed uses the same helper and the same macOS Screen Recording grant. Set it to `false` to withhold the capability, and a call is never offered it. |

There is no knob for how often frames are captured, how many are kept, or what share
of the call's cost cap the feed may spend. Those are fixed internal behavior, so the
only decision left to you is whether watching is allowed at all.

```toml
[fermix_core.realtime]
enabled = true
provider = "openai"
model = "gpt-realtime-2"
voice = "marin"
max_session_minutes = 15
max_estimated_cost_cents_per_session = 100
persist_transcripts = false
screen_share = true
```

See [realtime voice](/docs/realtime-voice).

## `[sandbox]`

Controls the sandbox (the walled-off environment that limits which files and commands a tool can touch) applied to shell tool calls. See [sandbox](/docs/sandbox).

| Key | Default | Notes |
|-----|---------|-------|
| `mode` | `"standard"` | Sandbox strictness. One of `"standard"`, `"strict"`, `"open"`. |
| `workspace_root` | `~/.fermix/workspace` | Primary workspace root inside the sandbox. |
| `allowed_roots` | `[]` | Additional filesystem roots to allow beyond the mode defaults. Approved grants persist here — `fermix grant path PATH` and an owner-confirmed `request_directory_access` request append the granted root; `fermix sandbox explain` labels these roots `(granted)`. |
| `blocked_roots` | `[]` | Roots to block regardless of mode. |

Fermix's own home directory is not a key here — it is derived from `FERMIX_HOME` and is always refused as a grant root. It also does not control what `open` mode exposes or what the credential blocklist protects; those key off the real OS home (`$HOME`), which is detected separately.

The shell command allow-set (`bare`, `assistant`, or `extended`) is configured in its own `[sandbox.commands]` sub-table via the `profile` key (default `"bare"`), not here. See [sandbox](/docs/sandbox) for command profiles and per-command tool definitions.

## `[sandbox.env]`

Controls which environment variables are visible to sandboxed processes.

| Key | Notes |
|-----|-------|
| `mode` | `"selected"` exposes only explicitly listed vars; `"all"` passes the full environment. |
| `allow` | Array of variable names to allow (used with `"selected"` mode). |
| `deny` | Array of variable names to deny (used with `"all"` mode). |

Sub-blocks `[sandbox.env.<NAME>]` declare per-key resolvers (each names a `source` of `"env"` or `"command"`). See [sandbox](/docs/sandbox) for the full env source reference.

## `[fermix_channels.<platform>]`

One sub-table per channel. Valid platform names: `telegram`, `whatsapp`, `slack`, `discord`, `signal`.

| Key | Notes |
|-----|-------|
| `enabled` | `true`/`false`. |
| `owner_user_id` | The operator's external user id on this platform. Required for `operator`-level trust. Also serves as the ingress allowlist when no explicit `allowed_user_ids` is set. |
| `allowed_user_ids` / `allowed_sender_ids` | Guest list. Users listed here receive `:guest` (read-only chat) trust. |
| `command_allowlist` | Ids permitted to run slash commands without full ingress allowlist membership. |
| `streaming` | Whether replies appear bit by bit as the model writes them, instead of all at once when finished. On by default with the mode derived from the channel: a channel that can edit its own messages (Telegram today) streams in `"draft"` mode when this key is unset, every other configured channel in `"block"` mode. `"draft"` streams into a live message that seals into section-shaped cards as it grows. `"block"` sends each completed chunk of the model's output as its own message (works on every channel). On a channel that cannot edit, an explicit `"draft"` is ignored at runtime, and `fermix doctor`'s `channel streaming` check warns; the same check notes when an explicit value overrides a different derived default (`telegram=block (explicit; unset derives draft)`), so a hand-written key never hides a newer default silently. `"off"` restores the single final message. Any other value stops config load, naming the valid values. Streaming is a no-op on a provider that does not stream. |

Per-platform credential keys:

| Platform | Keys |
|----------|------|
| Telegram | `bot_token` |
| WhatsApp | `app_secret`, `verify_token` (webhook); access token and phone number id via env vars |
| Slack | `bot_token`, `signing_secret` |
| Discord | `bot_token`, `bot_user_id` |
| Signal | `account`, `cli_path` |

After a normal `fermix setup`, channel secrets are stored in the OS keychain and these keys hold the `@keyring` sentinel or are absent. See [auth and secrets](/docs/auth-and-secrets) and [ingress and trust](/docs/ingress-and-trust).

The Telegram `bot_token` is deliberately **not** read from the environment: a single exported `TELEGRAM_BOT_TOKEN` would collide across instances, so the token lives only in each install's `config.toml` (keychain-backed). Run a second instance under its own `FERMIX_HOME` with its own bot token.

```toml
[fermix_channels.telegram]
enabled = true
owner_user_id = "123456789"
command_allowlist = ["987654321"]
```

Use `/whoami` from the target account to discover the platform user id to place in `owner_user_id` or `allowed_user_ids`.

## `[fermix_channels.acp]`

The surface that lets an [ACP client](/docs/acp) — the Zed editor, a Buzz harness — drive this daemon over a local socket. It is a channel, but not a platform: it has no credentials, no owner id, and no allowlist, because only the user account running the daemon can open the socket.

| Key | Default | Notes |
|-----|---------|-------|
| `enabled` | `true` | Whether the daemon accepts ACP clients. Set `false` to turn the surface off; the listener reflects the change on the next daemon restart. |

`enabled` is the only key this section accepts — anything else written here is dropped when the config is read. The socket path is not a setting: it is always `<FERMIX_HOME>/acp.sock`.

```toml
[fermix_channels.acp]
enabled = false
```

## `[mcp.servers.<name>]`

Outbound MCP server definitions. Each named sub-table configures one MCP server that Fermix can invoke as an external tool source.

| Key | Notes |
|-----|-------|
| `command` | Executable to run. |
| `args` | Argument list. |
| `env` | Static environment variables for the subprocess. |
| `pass_env` | List of host env var names to forward to the subprocess. |

Per-tool policy sub-blocks: `[mcp.servers.<name>.tools.<tool_name>]` accepts `policy_class` and `hidden_from_agent` to control sandbox policy and visibility per tool.

## `[mcp.inbound]`

Inbound MCP surface configuration. Currently not wired into the supervision tree. See [MCP](/docs/mcp).

## Environment variables

Secrets are normally stored in the OS keychain after `fermix setup` completes. The API-key and channel-token variables below are for headless or container deployments that inject secrets through the environment. The `FERMIX_*` operational variables are always environment-only knobs.

| Variable | Required | Description |
|----------|----------|-------------|
| `OPENAI_API_KEY` | When the `openai` provider is configured, or for Realtime voice | OpenAI API key. Realtime voice reuses this key. `openai_codex` uses OAuth, not this key. |
| `ANTHROPIC_API_KEY` | When the Anthropic provider is configured | Anthropic API key. |
| `XAI_API_KEY` | When the xAI provider is configured | xAI (Grok) API key. |
| `OPENROUTER_API_KEY` | When the OpenRouter provider is configured | OpenRouter API key. |
| `MISTRAL_API_KEY` | When the Mistral provider is configured | Mistral API key. |
| `OLLAMA_BASE_URL` | When the Ollama provider is configured | Base URL of the Ollama OpenAI-compatible endpoint (default `http://localhost:11434/v1`). |
| `XAI_BASE_URL` | No | Override the xAI API base URL. |
| `FERMIX_ANTHROPIC_AUTH_MODE` | No | Overlay the Anthropic auth mode (`api_key` or `oauth`). |
| `FERMIX_XAI_AUTH_MODE` | No | Overlay the xAI auth mode (`api_key` or `oauth`). |
| `WHATSAPP_ACCESS_TOKEN` | If WhatsApp is enabled | WhatsApp Cloud API access token. |
| `WHATSAPP_PHONE_NUMBER_ID` | If WhatsApp is enabled | WhatsApp phone number id. |
| `WHATSAPP_VERIFY_TOKEN` | If WhatsApp is enabled | Webhook verification token. |
| `WHATSAPP_APP_SECRET` | If WhatsApp is enabled | Webhook HMAC signing secret. |
| `DISCORD_BOT_TOKEN` | If Discord is enabled | Discord bot token. |
| `DISCORD_BOT_USER_ID` | If Discord is enabled | Discord bot user id. |
| `SLACK_BOT_TOKEN` | If Slack is enabled | Slack bot token. |
| `SLACK_SIGNING_SECRET` | If Slack is enabled | Slack request signing secret. |
| `SIGNAL_ACCOUNT` | If Signal is enabled | Signal phone number or account identifier. |
| `SIGNAL_CLI_PATH` | No | Override the `signal-cli` executable path (also settable via `config.toml`). |
| `FERMIX_HOME` | No | Override the config and workspace root directory. Default: `~/.fermix`. |
| `FERMIX_TRACE_DIR` | No | Override the trace output directory. Default: `$FERMIX_HOME/traces`. |
| `FERMIX_LOG_FILE` | No | Override the log file path. Default: `$FERMIX_HOME/logs/fermix.log`. |
| `FERMIX_HTTP_BIND` | No | Phoenix endpoint bind address. Default: `127.0.0.1` (loopback only). Set to `0.0.0.0` to accept network connections; doing so also exposes the setup surface, so place it behind a trusted network. |
| `FERMIX_REALTIME_ENABLED` | No | Enable the Realtime voice companion mode. |
| `FERMIX_REALTIME_PROVIDER` | No | Realtime provider. Only `openai` is supported. |
| `FERMIX_REALTIME_MODEL` | No | Override the OpenAI Realtime model. |
| `FERMIX_REALTIME_VOICE` | No | Override the Realtime voice name. |
| `FERMIX_REALTIME_MAX_SESSION_MINUTES` | No | Per-session voice duration cap. |
| `FERMIX_REALTIME_MAX_COST_CENTS` | No | Per-session estimated cost cap in cents. Short alias. |
| `FERMIX_REALTIME_MAX_ESTIMATED_COST_CENTS_PER_SESSION` | No | Long-form alias for `FERMIX_REALTIME_MAX_COST_CENTS`. |
| `FERMIX_REALTIME_PERSIST_TRANSCRIPTS` | No | Persist final voice transcripts as local memory text. |
| `PORT` | No | Phoenix HTTP port. Default: `4030`. |
| `PHX_HOST` | No | Public hostname for generated URLs. Default: `localhost`. |
| `FERMIX_TRACE_CONTENT` | No | Force trace content (prompt/response bodies) capture on (`1`) or off (`0`), independent of Opik. Off by default. |
| `FERMIX_OPIK_ENABLED` | No | Enable Opik trace export (also turns on trace content capture unless `FERMIX_TRACE_CONTENT=0`). |
| `FERMIX_OPIK_BASE_URL` | No | Opik server base URL. |
| `FERMIX_OPIK_PROJECT` | No | Opik project name. Default: `fermix`. |
| `FERMIX_OPIK_API_KEY` | No | Opik API key (never written into an installed service unit). |

Provider selection, model, reasoning effort, and the primary flag are deliberately **not** environment-settable — they are owned by `config.toml`. Environment variables override `config.toml` values at runtime (layer 3 of the precedence chain). In development and test runs, credential variables default to empty strings.

## Related pages

- [Setup](/docs/setup): first-run wizard, flags, and idempotent re-runs.
- [Auth and secrets](/docs/auth-and-secrets): keychain storage, `@keyring` sentinel, and the OAuth token store.
- [Providers and models](/docs/providers-and-models): model catalog, routing, and fallback chain.
- [Sandbox](/docs/sandbox): filesystem modes, command profiles, and env source resolvers.
- [Channels](/docs/channels): per-channel setup and ingress trust model.
- [Realtime voice](/docs/realtime-voice): voice companion setup and companion app.
- [Traces and telemetry](/docs/traces-and-telemetry): JSONL trace format and telemetry events.
- [Scheduled jobs](/docs/scheduled-jobs): job delivery targets and expiry.
