# Tool reference

> Every built-in Fermix tool with its parameters, policy class, and category, grouped by function.

This page lists the built-in tools the Fermix agent can use, with the inputs each one takes. Fermix exposes its built-in capability set (its tools) through the same registry that backs MCP tools (tools from external servers, via the Model Context Protocol) and plugin integrations. Almost every tool shown here is always available when registered; users do not install or remove them (the exceptions, `computer_use` and the coding-agent tools, are noted below). Skills are a separate layer: they wrap a subset of tools inside their own allowed-tool boundary (a skill can only use the tools it is granted) and are managed with `skill_create`, `skill_reload`, `skill_list`, `skill_run`, and `skill_view`. See [capabilities and tools](/docs/capabilities-and-tools), [skills](/docs/skills), and [sandbox](/docs/sandbox) for policy and trust details.

The table below lists every built-in tool. Most ship unconditionally; two families are conditional. `computer_use` is **experimental and off by default**: it drives the host desktop through a separate native helper program (the "computer-use helper," a small binary Fermix downloads and verifies), and it only becomes available to the model once an operator explicitly enables it and the helper is installed. The [coding agents](/docs/coding-agents) tools register only when the coding harness is enabled and the matching vendor CLI is installed, and none of them is advertised to the model until the owner approves coding agents in setup (see the section below). Detailed parameter tables follow, grouped by category.

| Tool | Category | Policy | Description |
|------|----------|--------|-------------|
| `shell` | system | exec | Run a shell command |
| `file_read` | filesystem | read\_only | Read a text file |
| `file_write` | filesystem | read\_write | Write a text file |
| `file_edit` | filesystem | read\_write | Replace an exact string span in a file |
| `request_directory_access` | system | external\_api | Ask the owner to approve sandbox access to a directory (attended operator turns only) |
| `glob_search` | search | read\_only | Find files by glob pattern |
| `content_search` | search | read\_only | Regex-search file contents |
| `git_read` | git | read\_only | Inspect git history, status, diffs |
| `git_write` | git | read\_write | Stage, commit, checkout, pull (no push) |
| `web_fetch` | web | network | Fetch a public HTTP(S) URL |
| `web_search` | web | network | Search the web (DuckDuckGo by default; configurable backend) |
| `place_search` | web | network | Structured place lookup — hours, ratings, addresses, links (advertised only when the Brave key is set) |
| `browser` | web | network | Drive a supervised local Chrome/Chromium browser |
| `computer_use` | computer | gui\_control | Drive the host desktop by screenshot and mouse/keyboard (experimental; off by default) |
| `codex_run` | harness | exec | Run a background coding task in a repository via the Codex CLI (registered when the coding harness is enabled and the Codex CLI is installed; advertised once approved) |
| `claude_code_run` | harness | exec | Run a background coding task in a repository via the Claude Code CLI (same registration and approval gate) |
| `codex_cloud_run` | harness | exec | Submit a coding task to a Codex cloud environment (registered only when `cloud_enabled = true`) |
| `list_coding_runs` | harness | read\_only | List coding runs, their status, and delivery state |
| `get_coding_run` | harness | read\_only | Fetch one coding run's full status, diagnostics, and delivery |
| `cancel_coding_run` | harness | read\_write | Cancel an active coding run by id |
| `stop_tracking_coding_run` | harness | read\_write | Stop tracking a Codex cloud run (registered only when `cloud_enabled = true`) |
| `subagents` | delegation | external\_api | Fan out work to temporary subagents |
| `skill_create` | skills | read\_write | Scaffold a new local skill |
| `skill_reload` | skills | read\_write | Reload skills from disk without a restart |
| `skill_list` | skills | read\_only | List installed skills |
| `skill_run` | skills | exec | Run an installed skill by name |
| `skill_view` | skills | exec | Show a skill's instructions and metadata |
| `model_routing_config` | config | read\_write | Read or update model-routing config |
| `tool_help` | meta | read\_only | Show docs for a registered capability |
| `memory_store` | memory | read\_write | Persist a keyed memory entry |
| `memory_recall` | memory | read\_only | Query stored memories |
| `memory_sources_list` | memory | read\_only | List configured memory sources |
| `schedule_job` | jobs | read\_write | Create a durable scheduled job |
| `list_jobs` | jobs | read\_only | List scheduled jobs |
| `update_job` | jobs | read\_write | Edit a scheduled job in place |
| `pause_job` | jobs | read\_write | Pause a scheduled job |
| `resume_job` | jobs | read\_write | Resume a paused scheduled job |
| `remove_job` | jobs | read\_write | Remove a scheduled job |
| `run_job_now` | jobs | read\_write | Run a scheduled job immediately, out of band |
| `list_job_runs` | jobs | read\_only | List a scheduled job's run history |
| `get_job_run` | jobs | read\_only | Fetch one scheduled-job run in full |
| `event_store` | reminders | read\_write | Store a personal date and its reminder plan (attended owner turns only) |
| `event_list` | reminders | read\_only | List or search stored dates and their reminders (attended owner turns, or a scheduled job the owner created) |
| `event_update` | reminders | read\_write | Edit a stored date, its plan, or its delivery channel (attended owner turns only) |
| `event_remove` | reminders | read\_write | Cancel a stored date and its unsent reminders (attended owner turns only) |
| `reminder_snooze` | reminders | read\_write | Defer one delivered reminder to a later time (attended owner turns only) |
| `generate_image` | media | external\_api | Create or edit a raster image from a prompt |
| `send_attachment` | channel | read\_only | Send a local file through the active channel |
| `react` | channel | read\_only | React to the user's message with a single emoji instead of a text reply |
| `tool_search` | meta | read\_only | Keyword search over deferred plugin/MCP tool schemas (registered when tool-schema deferral is on) |
| `tool_describe` | meta | read\_only | Return the full schema for one deferred tool (registered when tool-schema deferral is on) |
| `tool_call` | meta | read\_only | Invoke a deferred tool by name (registered when tool-schema deferral is on) |

---

## Filesystem

### `shell`

Run a shell command. The sandbox hardline blocklist (a built-in list of obviously dangerous commands that are always refused) runs before execution. The working directory and environment are controlled by the sandbox. Processes run in a group and are killed on timeout.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `command` | string | yes | Shell command to execute |
| `working_dir` | string | no | Working directory (defaults to the sandbox mode's working directory) |
| `timeout_ms` | integer | no | Timeout in milliseconds (default 30 000) |

### `file_read`

Read a UTF-8 text file. Path is validated through `Sandbox.read_path/3`; protected paths and paths outside the sandbox roots are rejected.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `path` | string | yes | Absolute path to the file |
| `offset` | integer | no | 1-based starting line |
| `limit` | integer | no | Number of lines to return |

### `file_write`

Write a UTF-8 text file, creating intermediate directories as needed. Path is validated through `Sandbox.write_path/3`.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `path` | string | yes | Destination path |
| `content` | string | yes | File content to write |
| `mkdir` | boolean | no | Create parent directories if they don't exist (default true) |

### `file_edit`

Replace exactly one unique string anchor in an existing file, via an atomic temp-file-and-rename that preserves the file's mode. Fails if `old_string` is missing from the file, or if it appears more than once (make the anchor longer until it is unique).

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `path` | string | yes | Path to the file |
| `old_string` | string | yes | Exact, unique text to replace |
| `new_string` | string | yes | Replacement text |

### `request_directory_access`

Ask the owner, in chat, to approve access to a directory the sandbox denies. It is for the case where a filesystem operation was rejected for being outside the sandbox roots and the task genuinely needs that directory. The tool is only offered on attended, top-level operator turns — never to guests, subagents, or scheduled and unattended runs. It refuses up front to request roots the sandbox would never grant (your whole home directory, the Fermix home, `~/.ssh`, OS roots), so the owner is never prompted for something that would be rejected anyway. The owner sees the canonical path, the stated reason, and the exact config change, and approves with a single-use `/confirm TOKEN` that expires in 60 seconds. 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. On approval the grant persists to `[sandbox] allowed_roots`, and on chat channels the original request resumes automatically.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `path` | string | yes | The directory to request access to (the path the sandbox denied) |
| `reason` | string | yes | Short, honest reason the task needs this directory, shown to the owner |

---

## Search

### `glob_search`

Find files matching a glob pattern within the sandbox roots. Results are bounded by `max_results`.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `pattern` | string | yes | Glob pattern, e.g. `"src/**/*.ex"` |
| `path` | string | no | Root directory to search from (defaults to the current directory) |
| `max_results` | integer | no | Upper bound on returned paths (default 200) |

### `content_search`

Search file contents for text (or, with `regex` set, a regular expression) without shelling out to `grep`. Binary files are skipped. Bounded by a deadline and a result cap.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `pattern` | string | yes | Text to find, or a regular expression when `regex` is true |
| `path` | string | no | Restrict search to this file or directory (defaults to the current directory) |
| `regex` | boolean | no | Interpret `pattern` as a regular expression |
| `max_results` | integer | no | Upper bound on matches (default 200) |
| `timeout_ms` | integer | no | Search deadline in milliseconds (default 30 000) |

---

## Git

### `git_read`

Run safe, read-only git subcommands: `status`, `log`, `diff`, `branch`, `show`. Any other subcommand is refused before git runs.

`branch` lists only. Arguments that would modify a ref are refused — the flags that delete, rename, copy, force, or retarget a branch (`--delete`, `--move`, `--copy`, `--force`, `--set-upstream`, `--set-upstream-to`, `--unset-upstream`, `--edit-description`), and bare operands too, because `git branch <name>` creates a ref with no flag at all. Since a bare operand is refused, a branch flag's value goes in the `=` form: `--contains=HEAD`, not `--contains HEAD`. The check covers every spelling git accepts — an unambiguous long-flag abbreviation (`--dele=x`), a short flag bundled with others (`-aD`), and a value glued to a short flag (`-uorigin/main`). So "list the branches merged into main" works; "delete the old branch" is refused, and the refusal names the rule rather than the argument that tripped it.

A shared, prefix-aware argument denylist — applied to every git tool, so abbreviated and `--flag=value` forms are caught too — blocks `--no-index`, `--git-dir`, `--work-tree`, `--exec-path`, `--output`, `--output-directory`, `--upload-pack`, `--receive-pack`, `--man-path`, and `--info-path`. Positional arguments that point outside the repository — absolute paths, `~/…`, and any path containing `..` — are refused as well. Every one of these is code-enforced and fails closed: the command does not run.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `repo` | string | no | Repository directory (defaults to the current directory) |
| `command` | string | yes | Git subcommand (`status`, `log`, `diff`, `branch`, `show`) |
| `args` | array of strings | no | Flags and arguments for the subcommand |

### `git_write`

Run mutating git subcommands: `add`, `commit`, `checkout`, and `pull`. Other subcommands (including `branch`, `merge`, and `push`) are refused; `push` is refused by name, with an error saying this tool cannot push. It enforces the same shared, prefix-aware argument denylist `git_read` does, so an argument like `git pull --upload-pack=<cmd>` cannot point git at another program.

It additionally refuses any flag that makes git read a file of the caller's choosing and fold its bytes into the repository: `--file`, `--pathspec-from-file`, and `-F` on `commit`. Only the repository path is sandbox-checked, never the arguments, so without this a commit message could be filled from a file the daemon can read and then read back out with `git_read log`. Every spelling is refused — `-F <path>`, `-F<path>`, a bundle such as `-aF <path>`, `--file=<path>`, the abbreviation `--fil=<path>`, and `--pathspec-from-file=<path>` — and the refusal tells the caller to pass the message inline with `-m`. A message that merely starts with `F` (`-mFixed the crash`) is a value, not a flag, and commits normally.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `repo` | string | no | Repository directory (defaults to the current directory) |
| `command` | string | yes | Git subcommand (`add`, `commit`, `checkout`, `pull`) |
| `args` | array of strings | no | Flags and arguments |

---

## Web

### `web_fetch`

Fetch a public HTTP(S) URL and return the body as markdown-light text. When the endpoint serves JSON, the JSON body is returned verbatim instead of being rendered as markdown-light text. A network guard blocks private and local IP ranges (so the tool cannot reach machines on your internal network) and pins the connection to the validated IP. Limits: 1 MB body cap, 5 redirects, 15 s receive timeout, 3 s connect timeout.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `url` | string | yes | Public HTTP(S) URL to fetch |

### `web_search`

Search the public web. The default backend is keyless DuckDuckGo. Configurable alternatives (Brave, DuckDuckGo, Exa, Firecrawl, Parallel, Perplexity, Tavily) are selected via `[fermix_core.tools.web_search]` in config, each with its own API-key entry. Up to 10 results are returned. If a configured non-DuckDuckGo backend becomes *unavailable* — a provider error (including out-of-credits / HTTP 402), a rate limit, a transport/network failure, or a changed-response parser error — the tool degrades once to DuckDuckGo loudly (a warning plus `degraded`/`primary_backend`/`fallback_reason` in the trace) so the broken backend stays visible. A missing or rejected API key (auth failure) and a bad query deliberately do **not** degrade — they surface so you fix them. Empty results do not trigger degradation either.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string | yes | Search query (max 1024 characters) |

When an answer draws on `web_search`, `web_fetch`, or `place_search` evidence, the agent is instructed to keep the exact returned URLs beside the claims they support (or in a short sources list) and never to invent or rebuild a link. This is an instruction-level contract on the model, not a deterministic rewriter.

### `place_search`

Look up real-world places — businesses, landmarks, addresses — and get structured records back instead of web-page snippets: name, address, coordinates, weekly opening hours, rating and review count, price range, contact details, distance, categories, one thumbnail link, and a link to the place's own page. The backend is Brave Place Search. The tool is advertised only when a Brave Search API key is configured under `[fermix_core.tools.web_search]` — the same key the Brave web-search backend uses, and Brave does not need to be the selected `web_search` backend. Without the key the tool is hidden from the model entirely, and execution re-checks the key either way. Each lookup is exactly one Brave request, billed separately from web-search calls, with no follow-up calls. Results are transient: Fermix stores no place data, and only the final answer persists as ordinary chat history. Returned links are never fetched.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string | yes | What to look for ("quiet coffee shop", "pharmacy open now"); max 400 characters |
| `location` | string | no | Area to search, as you would say it ("SoHo, New York", "10115"); mutually exclusive with coordinates |
| `latitude` / `longitude` | number | no | Explicit coordinate anchor; both must appear together |
| `radius_meters` | integer | no | Ranking bias around the coordinates (1–50000), not a hard geographic filter; valid only with coordinates |
| `count` | integer | no | Results to return, 1–10 (default 5) |
| `country` | string | no | Two-letter country code |
| `language` | string | no | Result language code |
| `units` | string | no | `metric` or `imperial` for distances |

For a "near me" question the agent fills `location` itself from the area you named in the conversation or a coarse remembered fact (a neighborhood, city, or zip — never a street address), and asks which area to search when it knows neither; that is the agent's instructed behavior, not a code gate, and the tool itself never reads memory. A lookup with no usable anchor refuses with a typed `location_required` error rather than guessing, and provider failures (bad key, rate limit, malformed response, oversized body) each surface as their own named error — the tool never falls back to `web_search`, another provider, or the browser.

### `browser`

Drive a supervised local Chrome or Chromium browser (over the Chrome DevTools Protocol) for JavaScript-rendered and interactive pages. Fermix manages the browser's lifecycle itself — launching it, capping how many tabs stay open, and closing idle profiles. This tool controls a browser process only; it does not control desktop applications or the wider GUI (that is `computer_use`). Each call runs one `action`. You target page elements primarily by the `ref` handles returned in a `snapshot` (an accessibility-tree view of the page). A CSS `selector` is used in exactly two places: `act` with `kind: "get"`, `field: "rect"` returns the viewport box `{x, y, width, height}` of the first matching element — in the same CSS-pixel space `click_coords` clicks in — which is the deterministic route onto a board, map, chart, or canvas that exposes no accessibility elements (a selector that matches nothing returns a typed `not_found` error); and `act` with `kind: "wait"`, `wait_until: "element"` waits for a selector or ref to appear. A ref click scrolls the element into view before reading its click box, so a target below the fold is clicked where it really is. A `snapshot` rebuilds the accessibility tree from the current document — it never serves the previous page after a navigation or single-page-app swap — and reports the document's `ready_state` (`loading`, `interactive`, or `complete`), so a thin snapshot reads as a page still building, not a page with nothing on it. The `screenshot` action returns the captured page as an image the model can see, not just a saved file path, and its result includes the page's `device_pixel_ratio`. Browser errors name their recovery: a blocked JavaScript dialog points to the `dialog` action, a stale ref points to taking a fresh `snapshot`, and an element with no rendered box (typically a visually hidden styled input, like a custom radio or checkbox) points to clicking its visible label ref or using `click_coords` with coordinates from `get field=rect`.

The `action` names the operation: session control (`start`, `stop`, `status`, `doctor`), tabs and navigation (`open`, `navigate`, `tabs`, `focus`, `close`), reading (`snapshot`, `screenshot`, `pdf`, `console`), interacting (`act`), and page state (`dialog`, `cookies`, `storage`, `upload`, `download`). Most interaction goes through `act`, whose `kind` selects what to do.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `action` | string | yes | Browser action to run (see above) |
| `url` | string | context-dependent | URL for `open` or `navigate` |
| `kind` | string | context-dependent | For `act`: `click`, `fill` (replace the field value), `type` (append), `submit`, `press`, `hover`, `get`, `wait`, or `click_coords` |
| `ref` | string | context-dependent | Element handle from the latest `snapshot` |
| `selector` | string | context-dependent | CSS selector: the element to measure (`get` with `field: "rect"`) or wait for (`wait` with `wait_until: "element"`) |
| `text` | string | context-dependent | Text to type or fill, wait target, or dialog prompt input |
| `field` | string | context-dependent | For `get`: `url`, `title`, `html`, `text`, `count`, `ready_state`, or `rect` (viewport box of the first `selector` match); also the field name for storage actions |
| `value` | string | context-dependent | Value for storage writes |
| `x`, `y` | number | context-dependent | Coordinates for the `click_coords` kind, in CSS-pixel page-viewport space (what `get field=rect` returns) — not `computer_use` screen pixels, and not raw browser-screenshot pixels (those are device pixels; divide by `device_pixel_ratio`) |
| `wait_until` | string | context-dependent | For the `wait` kind: `text`, `url`, `element`, or `load`, each with its required argument; there is no plain-pause mode |
| `key` | string | context-dependent | Keyboard key for the `press` kind |
| `full_page` | boolean | no | For `screenshot`: capture the whole page |
| `format` | string | no | Screenshot format: `png` or `jpeg` |
| `quality` | integer | no | JPEG screenshot quality, 1–100 |
| `interactive`, `compact`, `depth`, `include_urls` | — | no | `snapshot` shaping options |
| `profile` | string | no | Browser profile name (defaults to the configured profile) |
| `timeout_ms` | integer | no | Per-action timeout in milliseconds |

---

## Agent and skills

### `subagents`

Fan out independent work to one or more temporary subagents (short-lived helper agents the main agent spins off) running concurrently. Each subagent runs at the parent turn's trust level but with `read_write` and `gui_control` policy classes removed: workers can read, search, browse the web, use MCP/plugin tools, and run skills, but cannot directly change local Fermix state and cannot drive the desktop. The caller must synthesize the returned results. `subagents` is main-agent-only; calling it from inside a subagent is rejected.

Default concurrency cap: 4 (regular mode), 12 (`/ultra` mode). Default per-subagent timeout: 300 s (max 900 s). Default task cap: 10 per call (50 in `/ultra` mode).

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `tasks` | array | yes | List of task objects, each with `id` (string) and `task` (string goal); optional `context` string |
| `shared_context` | string | no | Context passed to every subagent |
| `max_concurrency` | integer | no | Max concurrently running subagents (default 4, max 8 in regular mode) |
| `timeout_seconds` | integer | no | Per-subagent wall-clock timeout (default 300, max 900) |
| `result_format` | string | no | `concise`, `detailed`, or `structured` (default) |
| `model` | string | no | Override model for all subagents in this call |
| `provider` | string | no | Provider for `model`; inferred from the slug when omitted |
| `reasoning_effort` | string | no | Thinking level for the subagents in this call |

### `skill_create`

Scaffold a new local skill at `~/.fermix/skills/<name>/SKILL.md` (frontmatter plus a starter `evals/evals.json`). The scaffold starts with an empty allowed-tool list; you then write the skill's instructions into the `SKILL.md` body on disk, grant it tools in the frontmatter, and pick them up with `skill_reload`. Refuses a name whose directory already exists. See [skills](/docs/skills) for the full authoring workflow.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | yes | Skill name (used as the directory name) |
| `description` | string | yes | Short description of what the skill does |

### `skill_reload`

Re-scan the skill directories and refresh the running agent's skills without restarting the daemon. Use after creating or editing a `SKILL.md` on disk, or after installing a plugin that ships skills, so the new or changed skill becomes loadable in the session. Returns what changed: added, removed, and changed names, plus any load errors.

No parameters.

### `skill_list`

List installed skills available to run via `skill_run`. Returns names and descriptions from the skill registry (discovers skills from `priv/skills`, `~/.fermix/skills`, and plugin roots).

No required parameters.

### `skill_run`

Run an installed skill by name. The run executes inside the skill's allowed-tool confinement.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | yes | Skill name as returned by `skill_list` |
| `task` | string | yes | Work request to pass to the skill |
| `context` | string | no | Optional parent context for the skill |

### `skill_view`

Show an installed skill's system prompt, allowed tools, and metadata — or, with `file`, load one of the skill's named reference files (extra Markdown files in the skill's `references/` directory that the skill body points at). Each loaded view, body or reference, is capped at 64 KiB (65,536 bytes); an oversized file is refused, not truncated. Reference reads are confined to the skill's own `references/` directory — path traversal and symlinks are refused with typed errors.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | yes | Skill name |
| `file` | string | no | Reference file name without its `.md` extension (lowercase letters, digits, underscore; max 64 characters) to load instead of the skill body |

---

## Memory

### `memory_store`

Store a fact to the agent's long-term memory, keyed and scoped to the current conversation. Fermix's background memory review later consolidates stored facts into the durable user and work profiles it injects into the prompt (see [memory](/docs/memory)).

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `key` | string | yes | Unique key for this memory (e.g. `user_timezone`, `project_name`) |
| `value` | string | yes | The value to store |

### `memory_recall`

Recall a memory by exact key, or run a keyword (full-text) search over stored memories and, optionally, conversation history. With no arguments it returns all of the current conversation's memories. The `scope` argument is a request, never an authorization: a scheduled job always searches its own job memory and a guest is always limited to the conversation it is in, whatever scope is named — the search succeeds, scoped down, rather than erroring.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `key` | string | no | Exact key to look up; omit it to return all of the conversation's memories |
| `search` | string | no | Keyword or phrase to search for |
| `scope` | string | no | Search scope: `current` (default), `owner`, or `all` |
| `source` | string | no | What to search: `memories` (default), `history`, or `all` |

### `memory_sources_list`

List configured memory sources, including scheduled jobs that back memory rows. Optional filters: `status` (string — filter by source status) and `source_type` (string — filter by source type).

---

## Scheduled jobs

See [scheduled jobs](/docs/scheduled-jobs) for delivery configuration and job lifecycle.

### `schedule_job`

Create a durable scheduled job. The model can narrow `allowed_tools` but cannot widen the capability policy class beyond the caller's trust level. Pinned `provider`/`model` must be set together.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | yes | Human-readable job name |
| `schedule` | string | yes | Interval (`every 15 minutes`), 5-field cron (`0 8 * * *`), or ISO 8601 UTC timestamp for a one-off run |
| `task` | string | yes | Work instructions for the future run; bake in any values the run needs (location, account, etc.) |
| `description` | string | no | Short description for the source catalog |
| `timezone` | string | no | IANA timezone label stored with the job |
| `expires_at` | string | no | ISO 8601 UTC datetime; Fermix marks the job expired when this is reached |
| `allowed_tools` | array of strings | no | Narrowing list of tool names; must be a subset of the caller's available tools |
| `skill_name` | string | no | Bind the run to an existing skill (confinement is intersected, never widened) |
| `provider` | string | no | Pin runs to a provider (must pair with `model`) |
| `model` | string | no | Pin runs to a model (must pair with `provider`) |
| `timeout_seconds` | integer | no | Wall-clock timeout for each run |
| `inactivity_timeout_seconds` | integer | no | Timeout when the provider/tool loop stops making progress |
| `delivery_mode` | string | no | `none`, `origin`, `channel`, or `local` |
| `delivery_target` | object | no | Delivery target for `channel` mode |

### `list_jobs`

List existing scheduled jobs. No required parameters.

### `update_job`

Edit a scheduled job's task instructions, schedule, description, skill binding, model pin, or delivery target in place, without removing and recreating it.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `job_id` | string | yes | Job id to edit |
| `task` | string | no | Replacement work instructions |
| `schedule` | string | no | Replacement schedule expression |
| `description` | string | no | Replacement short description |
| `skill_name` | string | no | Rebind to an existing skill (rejected if unknown) |
| `provider` | string | no | New provider pin (must pair with `model`) |
| `model` | string | no | New model pin (must pair with `provider`) |
| `clear_route_pin` | boolean | no | Un-pin the job's provider/model back to default routing; mutually exclusive with `provider`/`model` (set those to re-pin instead) |
| `delivery_mode` | string | no | `none`, `origin`, `channel`, or `local` |
| `delivery_target` | object | no | Replacement delivery target for `channel` mode |

### `pause_job`

Pause a running scheduled job by id.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `job_id` | string | yes | Job id to pause |

### `resume_job`

Resume a paused scheduled job by id.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `job_id` | string | yes | Job id to resume |

### `remove_job`

Remove a scheduled job permanently by id.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `job_id` | string | yes | Job id to remove |

### `run_job_now`

Run a scheduled job immediately, out of band, without waiting for its next scheduled time. The run executes through the normal scheduled-job runner (same isolation, delivery, and confinement) and the job's timed cadence is left unchanged. Use it to test a job or satisfy an on-demand request.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `job_id` | string | yes | Scheduled job id to run now |

### `list_job_runs`

List the execution history of a scheduled job: each run's status, trigger, timing, final response, and any error. Use it to confirm a job has been firing and to read what its runs produced.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `job_id` | string | yes | Scheduled job id whose runs to list |
| `status` | string | no | Filter by status: `queued`, `running`, `ok`, or `error` |
| `limit` | integer | no | Maximum runs to return, newest first (default 20, max 100) |

### `get_job_run`

Fetch one scheduled-job run in full: status, trigger, timing, the prompt snapshot it executed, token usage, output reference, final response, and any error. Use it to inspect a specific run found via `list_job_runs`.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `run_id` | string | yes | Job run id to fetch |

---

## Events and reminders

See [reminders](/docs/reminders) for reminder plans, delivery, and the boundary against scheduled jobs. Every tool in this section is offered and executable only on an attended, top-level owner turn — an interactive chat or CLI turn, or a live voice call — with one exception: `event_list` also runs inside a scheduled job the owner created, so a digest job can read upcoming dates. Guests, background work, delegated sub-agents, and coding-run continuations are refused with a typed error at both the advertisement and the dispatch step, tagged `not_attended`, and so are scheduled jobs on every tool here except `event_list`.

### `event_store`

Store a personal date and the finite reminder plan Fermix will deliver for it. The tool owns the clock, the timezone, and the delivery snapshot; the model supplies a title, a kind, and one time form. With no `[fermix_core.jobs] default_delivery_target` set, Fermix derives the owner's inbox from the first channel configured with an explicit owner id — Telegram, then Signal, then WhatsApp — and the call fails (`no_default_target`) only when there is neither a configured target nor a derivable channel. A configured target that is itself invalid fails instead of falling through to derivation, and a `default_delivery_mode` of `none`, `origin`, or `local` is refused ahead of both. It never falls back to the current chat. An ambiguous local time is refused (`ambiguous_local_time`) so the agent can ask which instant you meant. Storing the same date twice returns the existing event instead of a duplicate.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `title` | string | yes | Short event title, for example `Sarah's birthday` |
| `kind` | string | yes | One of `birthday`, `anniversary`, `appointment`, `deadline`, `event`, `follow_up`, `explicit_reminder` |
| `when` | object | yes | One tagged time form: `{"type":"date","date":"YYYY-MM-DD"}`, `{"type":"datetime","date":"YYYY-MM-DD","time":"HH:MM:SS"}` with an optional `utc_offset` when the local time is ambiguous, `{"type":"relative","amount":2,"unit":"days"}` (also `"weeks"`) with an optional `time`, or `{"type":"annual","month":9,"day":14}` for a yearly date |
| `description` | string | no | Extra detail stored with the event |
| `timezone` | string | no | IANA zone, only when the owner named one; otherwise the configured personalization timezone is used |
| `leap_day_policy` | string | no | `feb_28` or `mar_1`; required for a yearly February 29 date |
| `reminders` | array of objects | no | Explicit plan that replaces the defaults, at most 10 rules: `{"type":"days_before","days":7,"at":"09:00:00"}`, `{"type":"duration_before","minutes":60}`, or `{"type":"at_time"}` |
| `no_reminders` | boolean | no | Store the date with no notifications at all |
| `followup` | boolean | no | Mark the date for a follow-up check-in: after each reminder is delivered, a separate short agent turn may offer help or ask one question. The confirmation names the check-in when set |

### `event_list`

List or search stored dates: what is coming up, when a birthday falls, which reminders are planned, and whether one failed to deliver. With no window and no status it answers "what is coming up" and starts at today; history needs an explicit `from`/`to` or `status`. Results page with an opaque cursor passed back verbatim.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `text` | string | no | Match against title and description |
| `kind` | string | no | Restrict to one event kind |
| `status` | string | no | `active` (default), `completed`, `cancelled`, or `any`. Naming a status also drops the from-today floor, so past dates become visible |
| `from` | string | no | Earliest occurrence date, `YYYY-MM-DD`. Without it the list starts at today |
| `to` | string | no | Latest occurrence date, `YYYY-MM-DD`; at most two years after `from` |
| `limit` | integer | no | Rows per page (default 25, max 100) |
| `cursor` | string | no | Opaque next-page cursor from a prior call |

### `event_update`

Change a stored date: title, time, recurrence, timezone, or reminder plan. Fields left out keep their stored values, and a replacement plan replaces the stored one entirely. Reminders already delivered are immutable; only unsent ones are regenerated. Changing the date also requires `owner_direction`: a `when` with no quoted direction is refused (`overwrite_unconfirmed`), and a quote over 240 bytes is refused rather than trimmed (`owner_direction_too_long`). A reminder that is mid-send blocks the edit (`delivery_in_progress`) because a channel send cannot be recalled.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `event_id` | string | yes | The stored event's id, from `event_list` |
| `title` | string | no | New title |
| `description` | string | no | New description |
| `kind` | string | no | New event kind |
| `when` | object | no | New time, in the same tagged forms `event_store` accepts |
| `owner_direction` | string | with `when` | The owner's words that explicitly directed this date change, quoted near-verbatim as just the directing clause. Required whenever `when` is present; the call is refused without it. When the owner only restated a date under a name already stored there is nothing to quote — leave it out, and Fermix asks which event they mean instead of overwriting |
| `timezone` | string | no | New IANA zone for the event |
| `leap_day_policy` | string | no | `feb_28` or `mar_1` |
| `reminders` | array of objects | no | Replacement reminder plan; it replaces the stored one entirely |
| `no_reminders` | boolean | no | Drop every reminder for this date |
| `rebind_delivery_to_default` | boolean | no | Re-snapshot the current configured default delivery target and regenerate unsent reminders on it. Only on the owner's explicit request |
| `followup` | boolean | no | Set or clear the follow-up check-in for this date; changing it never requires `owner_direction` |

### `event_remove`

Cancel a stored date and its unsent reminders. Cancellation is soft: the delivered reminder history stays queryable. Omitting `event_id` is the "cancel that" path — the event behind the most recent reminder delivered into this exact conversation within the last 24 hours. With nothing delivered there the tool returns `no_recent_reminder` and the agent asks which date you meant, rather than guessing across conversations. The result carries the whole event, including its recurrence, so the reply can say that a yearly date took every future occurrence with it.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `event_id` | string | no | The stored event's id, from `event_list`. Leave it out for "cancel that" or "stop reminding me about this" |

### `reminder_snooze`

Defer one reminder to a later time. Without `reminder_id` the tool resolves the reminder Fermix most recently delivered into this exact conversation within the last 24 hours, the same lookup `event_remove` uses. It never moves the event itself — that is `event_update`. One snooze is active per reminder: re-snoozing replaces the previous one, and an identical repeat changes nothing. Both time forms are capped at 90 days out.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `snooze` | object | yes | One tagged form: `{"type":"duration","amount":2,"unit":"hours"}` relative to now, where `unit` is `minutes`, `hours`, or `days`, or `{"type":"datetime","date":"YYYY-MM-DD","time":"HH:MM:SS"}` with an optional `utc_offset` when the local time is ambiguous |
| `reminder_id` | string | no | The reminder to defer, from `event_list`. Leave it out for "snooze that" |
| `confirm_past_boundary` | boolean | no | Set only after the owner confirms a reminder that would arrive at or after the event itself |

---

## Media

### `generate_image`

Create or edit a raster image (photo, illustration, render) from a text prompt. The result is written to the sandbox `media/` directory and sent to the current chat automatically (file-only when there is no active channel, such as in a subagent or scheduled job). Not for diagrams, charts, code assets, or live data. The backend is set by `[fermix_core.tools.generate_image] backend` in config and must be one of `openai`, `openai_codex`, `xai`, or `google`; image generation must be configured (`fermix setup`) before this tool can run. `openai_codex` 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. It can generate and edit, but does not support `mask`. Editing references a source image by sandbox path or `inbound:last` (the image just sent in chat), and the `edit` operation and `mask` field are gated against the chosen backend's declared capabilities and rejected when unsupported.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `prompt` | string | yes | What to create, or for an edit, how to change the source image |
| `operation` | string | no | `generate` a new image (default) or `edit` an existing one |
| `input_image` | string | no | For `edit` only: sandbox path to the source image, or `inbound:last` / `inbound:N` for an image sent in this turn's chat |
| `mask` | string | no | Sandbox path to a PNG-alpha mask (OpenAI backend only); only its transparent regions are edited |
| `size` | string | no | Output size (e.g. `1024x1024`); defaults to the configured size |
| `model` | string | no | Model override for this call |

Each backend carries a curated model list: the setup Media tab offers exactly these, and the first is the default used when neither `[fermix_core.tools.generate_image] model` nor the tool's `model` argument names one.

| Backend | Default model | Also selectable |
|---------|---------------|-----------------|
| `openai` | `gpt-image-2` | `gpt-image-1.5` |
| `openai_codex` | `gpt-image-2` | — |
| `xai` | `grok-imagine-image-2.0` | `grok-imagine-image-quality` |
| `google` | `gemini-3.1-flash-image` | `gemini-3-pro-image`, `gemini-2.5-flash-image` |

The two SpaceXAI entries are distinct, separately priced models, not aliases of one another. On `openai_codex` the image model is fixed; the GPT-5.x model that carries the image tool is a separate config key (`router_model`), not a value for `model`.

---

## Computer use

### `computer_use`

**Experimental and off by default.** This is the most dangerous capability Fermix has — it controls your real, logged-in desktop, with no undo. It is enabled from the setup Plugins page (the "Computer Use" card) on **Apple Silicon (M-series) macOS and Linux x86_64** only; Intel Macs are not supported. Enabling downloads the native computer-use helper (fetched from its release and verified against a checksum baked into Fermix) and turns the feature on; the change applies on the next daemon restart. The tool is registered for the model only once it is both **enabled and the helper is installed**. Operating-system permission (macOS Screen Recording / Accessibility, or an X11 display on Linux) is a separate concern that `fermix doctor` reports on — it is not part of the enable gate.

Drive the **host desktop** GUI by screenshot and mouse/keyboard, one action per call. Take a `screenshot` to see the screen, then act on it (click, type, key, scroll, drag) using pixel coordinates from the latest screenshot. Every action that changes something returns a fresh screenshot the model can see, so it can verify the result and retry a missed click. For a small or dense target, the model can zoom: a `screenshot` with a `region` returns a magnified crop, and passing the same `region` on the follow-up click/drag maps the coordinates read in the crop back to the real pixel. On macOS a few read-only helpers make targeting more reliable: `inspect` reports the role and label of the on-screen element under a point (so the model can confirm it is about to click the right control before a consequential action), `elements` lists the clickable elements each with a click point, and `windows` lists the open windows — front-most first, each with its bounds already shaped as a `region` to pass to `screenshot` and follow-up clicks, so on a large display the model crops to the app instead of aiming inside a downscaled full screen. A `screenshot` with `marks: true` badges the accessibility click targets with numbered marks drawn on the image and lists them; the model then acts with `mark: N` instead of pixel coordinates and the daemon resolves the badge's exact point — no pixel estimation. Marks expire when the view changes; a stale or unknown mark is refused with a typed error, never guessed. `wait_for_change` blocks until the screen updates instead of polling with repeated screenshots, and `paste` enters long or unicode text quickly through the clipboard. There is no "browser mode" — web automation is the separate `browser` tool.

Aiming mistakes are caught rather than executed, and each of these checks is code-enforced. After a zoomed look — a `screenshot` or `elements` call that carried a `region` — a coordinate action without that same `region` is refused with the exact region to re-send, never silently read in full-screen space. Coordinates plausible on both live grids (inside the on-screen region rectangle while the current view is a magnified crop) are refused with the exact conversion arithmetic; re-sending the same action with `confirm_grid: true` after re-reading the image executes it, and an action addressed by `mark` bypasses this check because its point is copied, not read off an image. A click or drag the OS did not deliver is reported in the result as not delivered instead of success, and when the action carried a `region`, the post-action check screenshot is of that same magnified crop — verification happens in the space the model acted in. When the macOS Accessibility permission is missing, the session detects it at start and refuses every mutating action with a typed error naming the fix (System Settings → Privacy & Security → Accessibility) while screenshots keep working — one loud refusal instead of a run of silent no-ops.

The tool is operator-only and is never delegated to subagents. It also refuses to start from an unattended origin: a host desktop session can only begin from an interactive chat, `fermix ask`, or the voice companion — scheduled and cron runs fail closed.

Its `:gui_control` policy class gets no sandbox protection; safety comes instead from an **access posture derived directly from the sandbox mode** (there is no separate computer-use knob). `strict` is look-only and refuses every action that changes something; `standard` acts directly but the agent confirms with the owner before anything irreversible; `open` acts on its own but still confirms a truly catastrophic action. The live posture is folded into the `action` parameter description each turn.

You and the agent share one cursor, so there is a second, softer check on top of the posture. Before any action that would move the pointer or type — clicking, dragging, scrolling, moving the mouse, typing, pressing a key, or pasting — Fermix checks how long the machine has been idle. If you are actively using it, the agent waits a few seconds for a gap, then either takes its turn or holds the action back and tells you it stepped aside. `mouse_move` counts as disturbing here even though the table below lists it as read-only: it changes nothing on screen, but it does move your pointer. 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; it is configured with `courtesy` and `courtesy_idle_ms` in `[fermix_core.computer_use]` (see [configuration](/docs/configuration)).

To take the machine back yourself, `/pause` hands the cursor and keyboard back to you. The computer-use session stays alive and refuses further actions until you `/resume` — unlike `/stop`, which tears the session down. Both are owner-only, and `/resume` only lets the agent act again; it does not restart the task on its own.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `action` | string | yes | GUI action. Read-only: `screenshot`, `inspect`, `elements`, `windows`, `wait_for_change`, `mouse_move`, `wait`. Mutating: `left_click`, `right_click`, `double_click`, `left_click_drag`, `scroll`, `type`, `paste`, `key` |
| `x` | integer | context-dependent | X pixel for click/move/scroll/inspect, from the latest coordinate source |
| `y` | integer | context-dependent | Y pixel for click/move/scroll/inspect, from the latest coordinate source |
| `display` | integer | no | Display index; defaults to the configured display |
| `modifiers` | array of strings | no | Held modifier keys for a click (e.g. `["cmd"]`) |
| `from` | object | context-dependent | Drag start point (`{x, y}`) |
| `to` | object | context-dependent | Drag end point (`{x, y}`) |
| `direction` | string | context-dependent | Scroll direction: `up`, `down`, `left`, `right` |
| `amount` | integer | no | Scroll amount (positive) |
| `text` | string | context-dependent | Text to type or paste (for `action=type` / `action=paste`) |
| `chord` | string | context-dependent | Key chord for `action=key`, e.g. `"ctrl+s"` (supports `f1`–`f12`) |
| `ms` | integer | context-dependent | Milliseconds to wait (for `action=wait`) |
| `timeout_ms` | integer | no | Max ms to wait for a change (for `action=wait_for_change`; default 10 000) |
| `poll_ms` | integer | no | Check interval in ms (for `action=wait_for_change`; default 250) |
| `region` | object | no | Zoom rectangle `{x, y, w, h}` in the latest full-screen screenshot's pixel space. On `screenshot` it returns a magnified crop; on `elements`, points in that crop's space. Pass the same region with any follow-up action that uses coordinates from that crop or those points |
| `confirm_grid` | boolean | no | Re-send an action refused for ambiguous coordinates, after re-reading the magnified image and confirming `x`, `y` are pixels of that image |
| `marks` | boolean | no | For `screenshot`: badge the accessibility click targets with numbered marks and list them |
| `mark` | integer | no | Act on a numbered mark from the latest marks screenshot instead of `x`, `y`; the exact point is resolved daemon-side, and marks expire when the view changes |

---

## Coding agents

These tools hand repository work — reviewing changes, diagnosing and fixing bugs, implementing or refactoring features — to the Codex CLI or the Claude Code CLI installed on the machine, run inside the target repository on its live branch. The runtime prompt and `fermix doctor` call the same feature the coding harness. See [coding agents](/docs/coding-agents) for the full consent, authorization, environment, and delivery model.

The whole family is gated twice, and both gates are code-enforced. The tools exist only while the harness is enabled (`[fermix_core.harness] enabled`, default true) and the matching vendor CLI was on the daemon's `PATH` at boot — installing a CLI takes effect on the next daemon restart. And none of them is advertised to the model until the owner approves coding agents once on this machine, on the setup Coding Agents tab or with `[fermix_core.harness] approved = true`. Until then the family is absent from the prompt and Fermix does coding work with its own file and shell tools; there is no in-chat consent prompt, and Fermix can never grant the approval itself. The tools stay dispatchable by name while unadvertised, so a run recorded earlier can still be read or cancelled; a by-name launch on an unapproved machine is refused with a typed error naming the fix. With both CLIs installed and `default_vendor` set, only that vendor's run tool is advertised — the other stays callable by name. `codex_cloud_run` and `stop_tracking_coding_run` are not registered at all unless `cloud_enabled = true`.

Every harness tool is also authorized at execute time, before anything is written: the caller must be a live attended operator turn, or a scheduled job that names the exact tool in its persisted `allowed_tools`. Guests, delegated subagents, unattended runs, and voice calls are refused, each with its own typed error. Runs are background-only — there is no inline wait path: a run tool returns a run id (`hr_` plus 12 lowercase hex characters) immediately, and when the run finishes the outcome re-enters the conversation on its own. At most 2 runs execute at once (default) and one per git worktree (the repository checkout); a launch past either bound is refused with a typed error.

### `codex_run`

Run a coding task through the Codex CLI (`codex exec`) inside a repository. `cwd` and every path in `add_dirs`, `images`, and `output_schema` must clear the workspace sandbox first; a denied path is refused with a typed error naming the remedy (`request_directory_access`, or `fermix grant path <dir>`). Omitting `sandbox` sends no posture flag at all, so the run inherits the operator's own Codex configuration — with nothing configured, `codex exec` defaults to read-only. `danger-full-access` is refused at the tool boundary: Fermix admits the run's directories through its own sandbox but does not confine the child process at the OS level, so the vendor's sandbox is the only confinement. Prompts up to 200 KB (default) travel as the CLI's command-line argument; a larger prompt spills to a brief file the CLI is pointed at. On a `resume`, `add_dirs` and `profile` are refused, and a resumed thread inherits the sandbox policy it was started with unless `sandbox` is passed to change it.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `prompt` | string | yes | The coding task for Codex to carry out |
| `cwd` | string | yes | Absolute path of the repository or working directory |
| `model` | string | no | Codex model override |
| `effort` | string | no | Reasoning-effort override |
| `sandbox` | string | no | Codex sandbox mode: `read-only` or `workspace-write`. Omit to inherit the operator's configured posture; `workspace-write` writes freely inside the admitted directories without prompting (`danger-full-access` is refused) |
| `add_dirs` | array of strings | no | Extra writable directories, each sandbox-checked and locked; refused on `resume` |
| `ephemeral` | boolean | no | Write no session files (the run is not resumable); conflicts with `resume` |
| `profile` | string | no | Named Codex profile; refused on `resume` |
| `images` | array of strings | no | Image file paths to attach; sandbox-read-checked |
| `output_schema` | string | no | Path to a JSON output-schema file; sandbox-read-checked |
| `resume` | string | no | Resume a prior Codex thread by id |
| `timeout_minutes` | integer | no | Wall-clock timeout in minutes (default 30, max 240) |
| `progress` | string | no | `quiet` (default) or `milestones` — throttled progress notices to the origin chat, at most one per 60 s |

### `claude_code_run`

Run a coding task through the Claude Code CLI (`claude -p`) inside a repository. The same sandbox admission applies to `cwd`, `add_dirs`, and `append_system_prompt_file`. Omitting `permission_mode` sends no posture flag, so the run inherits the operator's own Claude Code settings; `bypassPermissions` is refused at the tool boundary for the same reason `danger-full-access` is on `codex_run`, and `dangerously_skip_permissions` is not a parameter the model can pass at all. `bare: true` skips the CLI's OAuth and keychain login and reads `ANTHROPIC_API_KEY` only.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `prompt` | string | yes | The coding task for Claude Code to carry out |
| `cwd` | string | yes | Absolute path of the repository or working directory |
| `model` | string | no | Claude model override |
| `effort` | string | no | Reasoning effort: `low`, `medium`, `high`, `xhigh`, or `max` |
| `permission_mode` | string | no | `acceptEdits`, `auto`, `manual`, `dontAsk`, or `plan`. Omit to inherit the operator's configured posture; `auto` runs with the least friction while keeping the sandbox (`bypassPermissions` is refused) |
| `allowed_tools` | array of strings | no | Tool names to allow in the run |
| `disallowed_tools` | array of strings | no | Tool names to disallow |
| `append_system_prompt` | string | no | Extra system-prompt text to append |
| `append_system_prompt_file` | string | no | Path to a file whose contents append to the system prompt; sandbox-read-checked |
| `add_dirs` | array of strings | no | Extra writable directories, each sandbox-checked and locked |
| `max_turns` | integer | no | Maximum agent turns |
| `json_schema` | string | no | JSON schema for structured output |
| `resume` | string | no | Resume a prior Claude session by id; mutually exclusive with `continue` |
| `continue` | boolean | no | Continue the most recent session |
| `bare` | boolean | no | Skip the OAuth/keychain login and read `ANTHROPIC_API_KEY` only |
| `timeout_minutes` | integer | no | Wall-clock timeout in minutes (default 30, max 240) |
| `progress` | string | no | `quiet` (default) or `milestones` — throttled progress notices to the origin chat, at most one per 60 s |

### `codex_cloud_run`

Registered only when `cloud_enabled = true` in `[fermix_core.harness]`. Submits a coding task to a pre-configured Codex cloud environment (`codex cloud exec`); nothing runs locally. Fermix tracks the task by bounded polling — every 120 s up to a 90-minute deadline by default; past the deadline, tracking stops and the task URL is delivered. The query rides one command-line argument under the same 200 KB cap; an oversized query is refused, never truncated. There is no `timeout_minutes` — the poll deadline bounds the tracking instead.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string | yes | The coding task for the Codex cloud run to carry out |
| `env_id` | string | yes | The Codex cloud environment id to run in |
| `branch` | string | no | Branch to run against (defaults to the environment's) |
| `attempts` | integer | no | Number of vendor attempts, 1–4 |

### `list_coding_runs`

List coding runs, their status, and their delivery state. Dead-lettered runs — terminal outcomes whose delivery exhausted its retries (20 attempts or 24 hours by default) — are grouped separately so the model raises them to the owner.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `status` | string | no | Status filter, e.g. `running`, `completed`, `failed` |

### `get_coding_run`

Fetch one coding run in full: vendor, rail (`local` or `cloud`), status and reason, working directory, resumability, delivery state and attempts, timestamps, token usage, exit code, and a diagnostics tail. `result_tail` carries the run's harvested text — the deliverable when it completed, the vendor's own error text when it failed — bounded to 4,096 bytes.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `run_id` | string | yes | The coding run id (`hr_…`) |

### `cancel_coding_run`

Cancel an active local coding run. The run terminalizes as `cancelled` and its outcome arrives as a plain message — a cancelled run never auto-continues the conversation. An unknown id and an already-finished run each return their own typed error. A Codex cloud run cannot be cancelled (the vendor exposes no cancel); the tool refuses and points to `stop_tracking_coding_run`.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `run_id` | string | yes | The coding run id (`hr_…`) to cancel |

### `stop_tracking_coding_run`

Registered only when `cloud_enabled = true`. Stops Fermix's tracking of an active Codex cloud run and delivers the task URL. The vendor-side task keeps running on ChatGPT; the result never claims it stopped.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `run_id` | string | yes | The cloud coding run id (`hr_…`) to stop tracking |

---

## Channel

### `send_attachment`

Send a local file through the active channel reply port. URL paths are refused; send links as text instead.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `path` | string | yes | Local file path to send |
| `kind` | string | no | `image`, `document`, `audio`, `video`, or `voice` (default `document`) |
| `caption` | string | no | Optional caption |
| `filename` | string | no | Override the filename shown to the recipient |
| `mime_type` | string | no | Override the detected MIME type |

### `react`

React to the user's current message with one fitting emoji through the active channel, instead of sending a text bubble — for pure acknowledgements ("ok", "thanks", 👍). The tool is only offered when the active channel supports message reactions; on a channel without them the model simply sends a short text acknowledgement instead. On a channel with a restricted emoji set, the `emoji` parameter is narrowed to exactly that channel's allowed emojis, so the model's choice is always valid. A delivered reaction with no accompanying text ends the turn without a further model call.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `emoji` | string | yes | A single emoji to react with, chosen to fit the message |

---

## Meta

### `model_routing_config`

Read or change which model delegated **sub-agents** run on — never the main agent's own model. It reads and writes exactly three keys in `[fermix_core.routing]`: `subagent_provider`, `subagent_model`, and `subagent_reasoning_effort` (for example, to run sub-agents on a smaller, cheaper model). Writes are validated and applied live — no daemon restart needed. (The parallel `cron_*` keys for scheduled jobs are read here but can only be set by editing `config.toml`.)

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `action` | string | yes | `read`, `set`, or `delete` |
| `key` | string | context-dependent | For `set`/`delete`: `subagent_provider`, `subagent_model`, or `subagent_reasoning_effort` |
| `value` | string | context-dependent | New value, for `set` |

### `tool_help`

Return the full documentation for one registered capability as visible to the current trust level and tool filters. Useful for the agent to inspect its own tool schema at runtime. A capability outside the caller's ceiling answers with the same "Unknown capability" error as one that does not exist, so the answer never confirms what the caller may not see.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | yes | Tool name to describe |

---

## Tool-schema deferral bridges

When tool-schema deferral is enabled (the default; disable with `[fermix_core.tools.tool_search] enabled = false`), the full definitions of plugin and MCP tools are not sent to the model up front, to save room in the prompt. Tool names remain listed in the runtime prompt. Three bridge tools are registered in their place so the agent can look up and invoke these deferred tools on demand.

### `tool_search`

Keyword search over the deferred plugin and MCP tool catalog. Use it when you know roughly what you need but not the exact tool name. Results are limited to tools the current caller could actually dispatch — a guest or a policy-narrowed run cannot discover tools outside its own ceiling.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string | yes | Keywords describing the capability you need |
| `limit` | integer | no | Maximum results to return (default 5, max 20) |

### `tool_describe`

Return the full schema for one deferred tool by name. Use after `tool_search` to read a tool's parameters before calling it. A deferred tool outside the caller's ceiling answers with the same "Unknown capability" error as one that does not exist, so the answer never confirms what the caller may not see.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | yes | Deferred tool name to describe |

### `tool_call`

Invoke a deferred tool by name. The loop unwraps the call so traces and policy enforcement see the real tool name, not `tool_call`. Direct calls to the real tool name also work when the schema has been fetched via `tool_describe`.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | yes | Deferred tool name to invoke |
| `arguments` | object | yes | Arguments object matching the tool's schema |
