Docs/Concepts/Coding agents

Coding agents

How Fermix hands repository work to your own Codex or Claude Code CLI: the one-time approval gate, who can launch runs, sandbox posture, background execution and continuations, durable result delivery, and the configuration reference.

Coding agents let Fermix hand repository work — reviewing a pull request or a set of changes, diagnosing and fixing a bug, implementing or refactoring a feature — to a coding CLI you already run yourself: the Codex CLI (codex exec) or the Claude Code CLI (claude -p). Fermix launches the CLI inside your repository, tracks the run in the background, and brings the outcome back into the conversation when it finishes. The setup page calls the feature Coding agents; the runtime prompt and fermix doctor call the same machinery the coding harness. They are one feature.

It is off until you approve it once per machine: flip “Allow coding agents to run on this machine” on the setup Coding Agents tab, or set [fermix_core.harness] approved = true in ~/.fermix/config.toml, then restart. Until then, no coding-agent tool is offered to the model at all and Fermix simply 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. This gate is code-enforced.

Know what you are approving: a run executes in your real project checkout, on its live branch — the working directory is the repository itself, not a copy — under whatever sandbox posture your own codex/claude configuration sets. If a run is killed mid-flight (a wall-clock timeout, /stop, a cancel), it can leave partial changes behind; the symptom is an unexpectedly dirty working tree. The fix is to inspect the tree (git status) before redoing or rerunning the work — the failure notice Fermix hands the agent tells it to do exactly that, but that check is the agent’s instruction to follow, not a code gate.

When the agent uses it

When a coding agent is available, the agent’s instructions steer it to route work that means understanding or changing a codebase to a run rather than doing it itself with file edits, reserving its own direct tools for the genuinely incidental. This is steering only — the agent’s judgment, not an enforced rule — and it can always still code directly with its own capabilities and tools.

The two gates

Two keys govern the feature, and they mean different things:

  • enabled (default true) says the feature exists on this machine.
  • approved (default false) is the one-time owner consent described above.

Until both hold, the feature is a two-state switch: usable, or invisible. On an unapproved machine no harness tool is advertised — not the run tools and not the history tools (list_coding_runs, get_coding_run, cancel_coding_run) — the prompt’s whole coding-harness section is dropped, and Fermix silently does coding with its own tools. Both the advertise gate and an execute-time check are code-enforced. If something dispatches a harness tool by name anyway, the refusal is typed and names the fix:

Coding agents are not yet approved on this machine. Approve them in
Setup → Coding Agents, or set [fermix_core.harness] approved = true in config.

One deliberate nuance: all seven tools stay dispatchable by name even when hidden, so a run recorded before consent was withdrawn can still be read or cancelled on request. And a scheduled job that tries to launch a run on an unapproved machine is blocked as blocked/consent_required, with the guidance delivered to the job’s delivery target so the owner hears about it.

Vendor detection and default_vendor

The vendor CLIs are detected at daemon boot (a PATH lookup): each run tool registers only when its CLI is present, the history tools register when either is, and nothing registers with no CLI installed. Installing a CLI takes effect on the next daemon restartfermix doctor tells you when the boot-time registry no longer matches the current PATH (“restart to sync codex, claude”).

default_vendor routes visibility, not capability: with both CLIs installed and a default set, only that vendor’s run tool is advertised, and the other stays dispatchable by name — the prompt tells the model both names, so an explicit “use Claude Code for this” still works. Code-enforced visibility, instruction-level steer.

What the CLIs need

Each CLI keeps its own login — Fermix reads that state and never writes it. Detection is network-free and never touches a keychain:

Vendor CLI Login state Fermix reads
Codex codex (codex exec) ~/.codex/auth.json carrying a refresh token → authenticated; otherwise not authenticated.
Claude Code claude (claude -p) A .credentials.json → authenticated; only the ~/.claude directory present → auth unverified (macOS keeps the credential in the Keychain, which detection never reads); neither → not authenticated.

Runs execute in a wiped, reconstructed environment: the CLI is spawned via /usr/bin/env -i with only HOME, PATH, TERM, USER, and the names the vendor adapter declares — daemon secrets never leak into a child. USER is required identity (Claude Code keys its macOS Keychain lookup on the account name); when the daemon has no $USER it is resolved from the passwd database, and only if neither answers is the run refused before spawn as failed/user_unresolved. Fails loud, code-enforced.

Your credential-store context is inherited: CODEX_HOME and CLAUDE_CONFIG_DIR resolve from Fermix’s own config keys (codex_home, claude_config_dir) first, then the daemon’s environment, then the vendor default — and detection reads the same resolver, so readiness can never report on a different store than a run actually uses. If you need to skip Claude’s OAuth/keychain entirely, bare: true on claude_code_run reads ANTHROPIC_API_KEY only.

Who can launch a run

Every harness tool carries an execute-time authorization gate, checked before anything is written. It is code-enforced, and each refusal class has its own typed message:

Caller Outcome
Live attended operator turn (owner, in a chat with a reply surface) Allowed.
Scheduled job naming the exact tool in its allowed_tools Allowed — the allowlist is the authorization; a broad tool policy is deliberately not enough.
Delegated sub-agent — even a worker inside an allowlisted job Refused: coding-harness runs are not available to delegated subagents.
Guest (an allowlisted sender who is not the owner) Refused: available to the owner only.
Unattended operator context, such as a /background run Refused: needs a live attended operator turn.
Scheduled job without the tool in allowed_tools Refused: add the tool name to the job’s allowed_tools.

Voice calls never see these tools at all: advertisement requires the authorization gate to pass, and a realtime voice session has no channel reply surface, so all seven tools are excluded from voice sessions. See ingress and trust for the operator/guest distinction.

How a run executes

The working directory — and every extra path a run names (add_dirs, images, append_system_prompt_file, output_schema) — must first clear Fermix’s own workspace sandbox. A denied path returns a typed error naming the remedy: ask the owner via request_directory_access, or have them run fermix grant path <dir>. For a scheduled origin a denied working directory is additionally recorded as blocked/workspace_denied and delivered.

Once admitted, the run executes in the real checkout. The repository’s worktree root (the top of the git checkout) is a lock domain: at most one active run per worktree — a second run into the same repository is refused (“Another run is active in <root>”) — and at most 2 runs at once by default (max_active), enforced atomically.

Sandbox posture

Posture is the operator’s, not the model’s. Omitting sandbox (codex) or permission_mode (claude) emits no posture flag at all, so the run inherits your own ~/.codex/config.toml / ~/.claude/settings.json — that is the default, and it is why runs are autonomous. With nothing configured on your side, codex’s own exec default is read-only. The model can pass a confining level, but the two values that would delete the vendor sandbox are refused at the tool boundary: codex sandbox: "danger-full-access" and claude permission_mode: "bypassPermissions" (and dangerously_skip_permissions is not a parameter the model can pass at all). The rationale rides in the error: Fermix admitted the run’s directories through its own sandbox but does not confine the child at the OS level, so the vendor’s sandbox is the only confinement — removing it is not selectable. Code-enforced.

Two codex details worth knowing:

  • Every codex run passes --skip-git-repo-check. This is not a bypass — the trust question is already answered by Fermix’s sandbox gate on the working directory, and without the flag every run in a brand-new project would die instantly.
  • On a resume, posture rides a different flag (the resume command has no sandbox switch of its own), and a resumed thread otherwise inherits the policy it was started with — passing sandbox there only matters to change posture mid-thread. add_dirs and profile are refused on a resume, and ephemeral + resume is refused (“An ephemeral run writes no session files, so there is no thread to resume.”). Each refusal is its own typed error.

Prompts and artifacts

Prompts up to 200 KB (default) travel as the CLI’s command-line argument; a larger prompt spills to a brief.md file with a pointer prompt telling the CLI to read it first. Every run owns an artifact directory ~/.fermix/harness/runs/<run_id>/ (directory mode 0700, files 0600):

File Contents
prompt.md The exact prompt the run was launched with.
events.jsonl The raw vendor event stream.
result.txt The harvested result — the deliverable when the run completed, the vendor’s own error text when it failed.
brief.md Only for oversized prompts: the full brief the CLI is pointed at.

The store is bounded, with three distinct failure messages rather than one:

Bound Default On breach
Artifact store quota 5 GB New runs are refused (“The coding-harness artifact store is full…”).
Free-space floor 2 GB New runs are refused (“Disk free space … is below the coding-harness floor…”); 0 disables the probe.
Per-run event spool 64 MB Spooling stops and the run continues, flagged artifact_truncated; only genuinely runaway output roughly 4× past the cap kills the run as failed/output_limit.
Retention 30 days Older run directories are swept at boot and daily.

An unprobeable filesystem is its own distinct error, and the free-space probe reads the nearest existing ancestor of the runs directory — a fresh install is never refused because the directory does not exist yet.

Background runs and what comes back

Runs are background-only — there is no inline wait path in the code. The tool returns a run id immediately (hr_ plus 12 hex characters) and the agent is instructed to end its turn rather than poll.

Chat-launched runs re-enter the conversation on their own. When one finishes, Fermix injects a system-voiced notice — [coding run hr_… finished], with vendor, status, working directory, and a bounded outcome — and the agent takes one turn to continue the request the run was for, or just report the outcome if it is already satisfied. The dispatch is code-enforced; the continue-or-report choice is the agent’s instruction. If re-ingest fails or times out (a 15-second watchdog), the outcome falls back to a plain delivered message instead of being lost.

Automatic follow-up chains are bounded at depth 3: a run launched from a continuation turn inherits depth + 1, and at the cap the outcome arrives as a plain message ending “Automatic follow-up stopped here — this run is 3 continuations deep…”. Code-enforced, never silent. And an owner-halted run never auto-continues: /stop, cancel_coding_run, or stop-tracking produce a plain message, so a follow-up turn can never relaunch work the kill switch ended.

A failed run’s message leads with the vendor’s own words — a not-logged-in message reads as exactly that, not a bare exit code — followed by the reason, a diagnostics tail, and an exact resume command (cd <cwd> && codex exec resume <sid> --json or cd <cwd> && claude --resume <sid>), or “Not resumable (ephemeral).” The failed-continuation notice also tells the agent to check the working tree before redoing anything (a timeout-killed run may have left partial changes) and then decide: rerun if transient, or do the work directly and tell the owner plainly what failed. There is no programmatic retry.

Scheduled runs never re-enter a conversation. A run a scheduled job launched delivers plainly to the job’s delivery target, frozen at launch time — a later config edit can never retarget an in-flight run.

Delivery is durable and at-least-once: one inline attempt when the run reaches a terminal status, then a background worker retries on a 30-second tick with exponential backoff (base 30 seconds, capped at 30 minutes); after 20 attempts or 24 hours (defaults) the message dead-letters (is parked undelivered) and is surfaced by list_coding_runs and fermix doctor. Every delivered message starts with [run <id>] — at-least-once means a crash right after a successful send can duplicate a message, and the id is your dedup key.

Two smaller comforts: progress: "milestones" sends throttled phase notices (at most one per 60 seconds, best-effort) to the origin chat — the default "quiet" sends none — and every completed run writes a bounded summary (at most 16,384 bytes) into memory as untrusted data, scoped to the origin conversation or parent job, never promoted into profile files. The memory write is best-effort and never fails the run.

Timeouts, restarts, and stopping

A local run has a three-tier watchdog. Only the last tier kills:

Tier Default Effect
First vendor event not seen 120 seconds Advisory notice (usually auth or configuration trouble); the run continues.
No activity between events 10 minutes Advisory notice; the run continues.
Wall clock 30 minutes The run is killed as failed/timeout. A per-run timeout_minutes can raise it to at most 240 minutes.

A malformed vendor stream fails the run as failed/protocol after 20 framing errors (default), and a single event is capped at 1,048,576 bytes (default). Run statuses are starting/running while a local run is active (submitting/polling on the cloud rail) and completed/failed/blocked/cancelled/interrupted when terminal.

Runs survive daemon restarts honestly: boot reconciliation finalizes any non-terminal local row as interrupted and delivers resume guidance, and a crashed run process is terminalized as failed/run_crashed — nothing is silently resurrected or orphaned. The whole rail restarts as one supervised unit (see supervision tree), and it is always in the tree even when the feature is disabled, so in-flight deliveries drain regardless.

/stop (see channel commands) cancels every active local coding run — including ones a scheduled job started — and reports the count. Cloud runs are skipped (no vendor cancel exists; their polling continues), and scheduled jobs themselves and realtime voice are unaffected. For one run, cancel_coding_run records owner intent and moves it to cancelled; an unknown id answers “No coding run found for that id.” and a finished run answers “That coding run has already finished.”

Doctor

fermix doctor has a coding harness row, skipped only when the harness is disabled and no vendor CLI is present. It reports each vendor’s CLI and version with its login state (“authenticated” / “auth unverified” / “not authenticated”), with “not installed” shown for a vendor whose CLI is absent, the consent state (“approved” / “not yet approved” — informational, never a warning by itself), run counts (active, pending delivery, dead-letter), artifact-quota usage, and the boot-versus-PATH mismatch hint. It reports fail only on a breached artifact quota, because that hard-blocks new runs; everything else — no CLI while enabled, an unauthenticated vendor, dead-letters, low or unknown free space, a mismatch — is a warning. A disabled harness with a CLI present prints the enable hint.

Traces

Each run is its own trace: lifecycle events (run_start, run_complete, run_error, progress) land in the daily trace files, and every provider and tool event inside the run carries the session id harness_<run_id>. In Opik, each run is its own root trace, correlated to the turn that launched it by origin_session_id metadata — never nested under it, so a background run that outlives its turn still exports cleanly. Prompt and result bodies attach only when content capture is on. See traces and telemetry.

The Codex cloud rail (off by default)

A separate, dormant rail submits work to a pre-configured Codex cloud environment instead of your machine. It is off by default and its two tools (codex_cloud_run, stop_tracking_coding_run) do not even register until cloud_enabled = true plus a restart; the setup tab states that cloud runs are off and only local repository runs are available.

When enabled: codex_cloud_run submits the task (codex cloud exec with an environment id, an optional branch, and 1–4 vendor attempts) and touches nothing local. Tracking is bounded polling every 120 seconds (default) with a 90-minute deadline (default) — past the deadline the run finalizes as blocked/poll_deadline with the task URL, and the poll schedule persists across restarts. There is no vendor cancel: cancel_coding_run on a cloud run refuses and points at stop_tracking_coding_run, which stops Fermix’s tracking and delivers the task URL but never claims the ChatGPT-side task stopped. The query rides a single command-line argument under the same 200 KB cap — oversize is refused as query_too_large, never truncated. A daemon dying mid-submit finalizes the run as blocked/submission_outcome_unknown with a codex cloud list pointer, and never auto-resubmits.

Configuration reference

Everything lives in one section. A typical approval by hand:

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

An unknown key in this section refuses boot — config load fails loudly naming the key, and the daemon will not start until it is fixed. Every value is validated at config load the same way. Changes apply on the next daemon restart.

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, kept out of the file when false.
cloud_enabled false Registers the two Codex cloud tools (see above).
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.
default_timeout_minutes 30 Wall clock; a per-run timeout_minutes caps at 240 minutes.
inactivity_minutes 10 Advisory no-activity notice; the run continues.
prompt_argv_max_kb 200 Prompts above this spill to brief.md.
max_event_bytes 1048576 Cap on a single vendor event (1 MiB).
max_framing_errors 20 Malformed-stream budget before failed/protocol; 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 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 Delivery retries before dead-lettering.
delivery_max_age_hours 24 Delivery age before dead-lettering.
cloud_poll_seconds 120 Cloud status-poll interval.
cloud_poll_max_minutes 90 Cloud poll deadline before blocked/poll_deadline.
codex_home unset Where the Codex CLI keeps its state (auth.json); falls back to the daemon’s CODEX_HOME, then the vendor default.
claude_config_dir unset Mirrors CLAUDE_CONFIG_DIR; also re-keys Claude’s macOS Keychain entry, so set it if you log in under one.

There are no CLI wizard prompts and no fermix setup flags for this feature — the setup Coding Agents tab and direct config editing are the two paths. And there are no environment-variable overrides for these keys.

The tools

All seven tools are described in full — parameters, requirements, failure modes — in the tool reference:

Tool Policy class Purpose
codex_run :exec Run a Codex task inside a repository (codex exec).
claude_code_run :exec Run a Claude Code task inside a repository (claude -p).
list_coding_runs :read_only List runs, their status, and delivery state — dead-letter runs grouped separately.
get_coding_run :read_only One run’s full status, diagnostics, usage, delivery state, and a result tail bounded to 4,096 bytes.
cancel_coding_run :read_write Cancel an active local run.
codex_cloud_run :exec Submit a task to a Codex cloud environment. Registers only with cloud_enabled = true.
stop_tracking_coding_run :read_write Stop tracking a cloud run. Registers only with cloud_enabled = true.

Next steps