Skip to content

Runners

A runner defines how agents execute. Agents decide what to do (model, persona, skills); the runner is the engine that actually launches the LLM — either as a CLI subprocess on the host or as a direct API call.

runners:
  - id: claude
    type: cli
    provider: claude

default_runner: claude

agents:
  - id: engineer
    runner: claude            # ← references the runner above
    model: claude-sonnet-4-6

Several agents can share one runner — each gets its own configured instance, so per-agent MCP servers and identity don't leak between them.

Field Description
id Unique identifier, referenced by agents[].runner and agents[].fallbacks
type Execution engine: cli (subprocess) — or a self-contained adapter name like opencode-api
provider Which CLI the cli engine drives: claude, opencode, codex, cursor
config Engine options — see CLI engine configuration
models Models this runner supports (informational)
mcps MCP servers exposed to every agent on this runner — see MCP servers

How type and provider resolve. Apiary picks the adapter by combining them as {provider}-{type} (or just type when provider is empty). The registered adapters are:

Adapter YAML Launches
claude-cli type: cli, provider: claude the claude binary
opencode-cli type: cli, provider: opencode the opencode binary
codex-cli type: cli, provider: codex the codex binary
cursor-cli type: cli, provider: cursor the agent binary (Cursor CLI)
opencode-api type: opencode-api HTTP calls to the OpenCode API

Any other combination is rejected by apiary validate (and by the daemon's config check at startup) with an error listing the valid combinations.

CLI runners

A cli runner invokes the agent tool as a subprocess for each step, streams its stdout/stderr into the task logs, tracks its PID and heartbeats, and enforces the run timeout (settings.task_timeout). The subprocess inherits the daemon's environment plus the env overlay and the agent's git/source identity.

Credentials stay with the tool

Apiary never handles, stores, or transmits provider credentials for CLI runners. The claude / opencode / agent binaries authenticate exactly as they do when you run them by hand — Apiary just invokes them. This is what makes the cli type a good fit for personal machines where the tools are already logged in.

Providers

Each provider preconfigures the engine — binary name, default args, how the prompt and model are passed, and the MCP format. config is only for overrides; a bare provider: line is a fully working runner.

Claude (provider: claude)

Requires the claude binary on PATH.

runners:
  - id: claude
    type: cli
    provider: claude

The preset runs claude --output-format stream-json --verbose: Apiary parses Claude's structured event stream into

  • readable [assistant] / [tool→ …] / [tool← result] lines in the task logs,
  • exact token counts and cost_usd per run (reported, not estimated),
  • rate_limit_event detection, which drives failover.

The preset also passes --dangerously-skip-permissions, because headless claude -p cannot display a permission prompt — see Tool permissions to narrow that.

OpenCode (provider: opencode)

Requires the opencode binary on PATH. The preset invokes opencode run … with the prompt as a positional argument.

runners:
  - id: opencode
    type: cli
    provider: opencode

Codex (provider: codex)

Requires the OpenAI Codex CLI (codex binary) on PATH. The preset invokes codex exec --sandbox workspace-write … with the prompt as a positional argument.

runners:
  - id: codex
    type: cli
    provider: codex

Codex discovers repository skills from .agents/skills. If you already keep skills under .claude/skills, create .agents/skills as a symlink to reuse the same SKILL.md files without copying them.

Cursor (provider: cursor)

Requires the Cursor CLI (agent binary, from cursor.com/install). The preset runs it headlessly: agent -p --output-format stream-json --force (with --force auto-approving file changes) and passes the prompt positionally.

runners:
  - id: cursor
    type: cli
    provider: cursor

Cost reporting

The Cursor CLI streams token counts but no dollar cost, so cursor runs show $0.00 out of the box. On usage-based Cursor plans the daemon can back-fill real billed amounts from the Cursor dashboard API — see settings.cursor_cost.

CLI engine configuration

All config keys, with each provider's preset defaults:

Key Description claude opencode codex cursor
command Binary to invoke (override to use a wrapper or absolute path) claude opencode codex agent
args Extra argv appended after the preset's args --output-format stream-json --verbose run exec --sandbox workspace-write -p --output-format stream-json
model_flag Flag used to pass the step's model --model --model --model --model
prompt_flag Flag used to pass the prompt -p (positional) (positional) (positional)
prompt_positional Pass the prompt as the last positional argument false true true true
turns_flag Flag used to pass a max-turns limit
permission_mode Tool-permission posture (see below) bypass bypass
allowed_tools Tool names to pre-approve
permission_flag Provider flag for a named permission mode --permission-mode
permission_bypass_args Argv that disables the permission prompt --dangerously-skip-permissions --force
allowed_tools_flag Provider flag for the allow-list --allowedTools

Prompt delivery: via prompt_flag when set, as the last positional argument when prompt_positional: true, otherwise on stdin.

Tool permissions

Apiary only ever runs agents non-interactively, so an agent cannot answer a permission prompt. A provider that gates tools behind one denies every call — Bash, Grep, and MCP alike — and the failure is quiet: the agent runs to completion, reports success, and has written nothing. Claude and Cursor both gate by default, so their presets ship with permission_mode: bypass.

permission_mode Effect
bypass (preset default for claude and cursor) Emits permission_bypass_args — the agent may use every tool without asking
default Emits no permission flags at all; the provider's own default applies
anything else Passed through to permission_flag as a provider-native mode name (claude: acceptEdits, plan, …)

A mode the provider cannot honour is rejected at config load, so the daemon fails loudly instead of starting agents that silently cannot act.

To grant a narrow set of tools instead of a blanket bypass, combine permission_mode: default with allowed_tools:

runners:
  - id: claude-triage
    type: cli
    provider: claude
    config:
      permission_mode: default
      allowed_tools: ["Read", "Grep", "mcp__atlassian__jira_add_comment"]

Bypass and untrusted input

bypass lets the agent run any command in its working directory. For runners that process issues or comments from untrusted authors, pair it with sandboxing so a successful prompt injection is contained, or narrow the runner with allowed_tools.

Working directory

Every agent subprocess starts in a working directory, and that directory is the agent's whole frame of reference: it is what pwd reports, what a bare git status inspects, and the root a relative path in a prompt resolves against. An agent that starts nowhere in particular has to find the repository first — burning turns on it, and searching the operator's home directory on the way.

Apiary resolves the directory per step, taking the first of these that is set:

# Source Scope
1 steps[].working_dir One step
2 workflows[].working_dir Every step of one workflow
3 agents[].working_dir Every step that runs one agent
4 runners[].config.working_dir Every agent on one runner
5 the directory holding apiary.yaml Fleet-wide default

The last row is the default, so an agent always starts somewhere the operator chose — usually the very repository the hive is about.

Relative paths resolve against the directory holding apiary.yaml, and a leading ~ expands to the user's home. With the config at ~/Projects/app/.apiary/apiary.yaml, working_dir: .. is the repository root.

runners:
  - id: claude-cli
    type: cli
    provider: claude
    config:
      working_dir: ~/Projects/app        # fleet-wide default

agents:
  - id: docs-writer
    runner: claude-cli
    model: sonnet
    working_dir: ~/Projects/app/docs     # this agent writes docs only

workflows:
  - id: release
    working_dir: ~/Projects/app          # every step of this workflow
    steps:
      - id: build
        agent: builder
        working_dir: ~/Projects/app/src  # this step only

Checkout per workflow

workflows[].working_dir is the knob for pointing one hive at several checkouts — a workflow per repository, each with its own directory, all sharing the same agents.

API runners

API runners skip the subprocess and POST to a chat-completions endpoint directly. The built-in adapter is opencode-api:

runners:
  - id: opencode-api
    type: opencode-api
    config:
      api_key: ${OPENCODE_API_KEY}
      # base_url: https://api.opencode.ai/v1   # optional override
    models:
      - opencode-go/deepseek-v4-pro
      - opencode-go/minimax-m3
Key Description
api_key Sent as Authorization: Bearer <key>
base_url Override the API base; /chat/completions is appended
endpoint Full URL override (takes precedence over base_url)

The request/response shape is OpenAI-compatible chat completions. API runners are the right choice for shared or headless deployments where no pre-authenticated CLI tool exists on the host.

Warning

An API runner sends the task prompt to the provider directly. CLI tools that do their own context-gathering (file reads, shell, MCP) are far more capable for implementation work — prefer cli runners for coding agents and API runners for lightweight classification or as fallback capacity.

Sandboxing agent execution

A CLI runner can run every agent subprocess inside a Docker container, isolating it from the host filesystem. Use it for runners that process issues or comments written by people you do not fully trust.

runners:
  - id: sandboxed-claude
    type: cli
    provider: claude
    config:
      command: claude
    sandbox:
      image: my-org/apiary-agent:latest   # must contain the agent binary
      # user:    "1000:1000"              # default: the daemon's uid:gid
      # network: none                     # default: bridge (agents need egress)
      extra_args: ["--memory", "4g", "--pids-limit", "512"]   # resource limits only
    env_passthrough: ["MYCORP_*"]         # beyond system + provider credentials

What it contains: host filesystem access (only the task working directory is mounted), process and capability escalation (--read-only rootfs, --cap-drop all, --security-opt no-new-privileges), and unrelated host secrets — the agent environment is allow-listed to system variables plus LLM provider credentials, with per-task credentials overlaid. Credentials are passed to the container by name, so their values never appear in the host process table.

What it does NOT contain: network exfiltration. network defaults to bridge because coding agents must reach their LLM API and git remotes, and the agent legitimately holds provider keys and a source token. A prompt-injected agent can still send data out. Add egress controls at the network layer if your trust level requires it.

Notes:

  • extra_args accepts only resource-limit and labelling flags (--memory, --cpus, --pids-limit, --ulimit, --label, …). Anything that could weaken the sandbox — --privileged, --cap-add, extra mounts, --network, --user, --read-only=false, --entrypoint — is rejected at config load.
  • HOME inside the container points at a writable tmpfs (mode=1777, exec), since the rootfs is read-only and a numeric --user has no passwd entry. Both options are set explicitly because docker merges its tmpfs defaults (nodev,noexec,relatime) rather than replacing them, and a tmpfs otherwise inherits the mountpoint's mode from the image.
  • MCP servers are not supported with a sandbox yet. Their config is written to host paths the container cannot see, so combining them is rejected at config load rather than silently starting an agent with its MCP servers missing.

MCP servers

Runners (and individual agents) can expose Model Context Protocol servers to the CLI tools they launch:

runners:
  - id: claude
    type: cli
    provider: claude
    mcps:
      - name: gitnexus
        command: npx
        args: ["-y", "gitnexus@latest", "mcp"]
        # env:                       # optional; ${VAR} expanded at load
        #   GITNEXUS_TOKEN: ${GITNEXUS_TOKEN}

agents:
  - id: qa
    runner: claude
    model: claude-sonnet-4-6
    mcps:                            # agent-scope: merged over the runner's
      - name: playwright
        command: npx
        args: ["-y", "@playwright/mcp@latest"]

Runner-level mcps apply to every agent on the runner; agent-level mcps are layered on top — same name overrides, new names are appended. Each provider receives the merged list in its own native format:

Provider Mechanism
claude temp .mcp.json passed with --mcp-config <path> (trusted, no approval prompt, no workdir mutation)
cursor merged into ~/.cursor/mcp.json, activated with --approve-mcps
opencode merged into the global opencode.json mcp block
codex merged into ~/.codex/config.toml under Apiary-managed [mcp_servers.*] tables

The MCP configs are materialized once at daemon startup, not per run.

Fallbacks: runners as failover capacity

An agent's fallbacks chain lists alternative {runner, model} pairs to try when its primary runner is rejected by a provider rate limit:

agents:
  - id: engineer
    runner: claude
    model: claude-sonnet-4-6
    fallbacks:
      - {runner: opencode, model: opencode-go/deepseek-v4-pro}
      - {runner: cursor, model: composer-2.5-fast}

Every entry must reference a defined runner; model is optional (empty uses that runner's default). Fallback adapters are instantiated and configured at startup — including their MCP setup — so failover never pays an initialization cost on the hot path. The full behavior is described in Rate Limits & Resilience.

Adding a provider

Runner adapters are small: a provider registers a factory that preconfigures one of the generic engines (subprocess or HTTP). See the Plugin API spec and src/internal/runner/providers/ for the current registrations — contributions are welcome.