Skip to main content
A provider is the agent CLI that runs inside a group’s container — the thing that actually plans, calls tools, and writes replies. By default that’s Claude Code via the Claude Agent SDK. The provider is pluggable: the agent runner selects an implementation from an open registry at startup, and the trunk ships only claude (plus a mock provider for tests).
Don’t confuse providers with tools. A tool skill like /add-ollama-tool keeps your existing provider as the agent’s brain and gives it a local model to call; a provider skill replaces the brain for an entire group.

How the provider is resolved

At spawn time the host resolves the provider name with a fixed precedence (src/container-runner.ts):
  1. sessions.agent_provider — per-session override
  2. container_configs.provider — the group’s container config
  3. 'claude'
The result is lowercased. The host then asks its provider registry whether that provider contributes extra mounts or env vars (OpenCode’s XDG dirs and OPENCODE_* passthrough; Codex’s per-group ~/.codex state dir and its own AGENTS.md surfaces). mock contributes nothing, and claude contributes only when setup has wired a custom Anthropic-compatible endpoint — ANTHROPIC_BASE_URL from .env plus a placeholder auth token for the vault gateway to overwrite. Inside the container, the runner reads provider from container.json — materialized fresh from the database at every spawn — and instantiates it from the in-container registry, passing the group’s model and effort through unchanged. Every code path that creates a session on trunk writes agent_provider = null, and ncl sessions is read-only (list/get) — so the per-session override exists in the resolver, but nothing ships that sets it. In practice you switch providers per group.

Claude (default)

The Claude provider wraps the Claude Agent SDK. Its model accepts an alias (sonnet, opus, haiku) or a full model ID, and effort is one of low, medium, high, xhigh, max — both optional, both falling back to the SDK default. It’s also the most fully integrated provider in-tree: native slash-command handling, mid-turn input streaming, and transcript rotation that archives oversized session histories before a cold resume can wedge the container. To point it at any Anthropic-compatible endpoint, set ANTHROPIC_BASE_URL in .env — the real token stays in the vault and is injected on the wire. See Credentials.

Two ways to run a different brain

The three provider skills split into two mechanisms:
  • Real provider implementations — OpenCode and Codex are TypeScript providers that live on the providers registry branch. /add-opencode and /add-codex copy the files in, wire them into the host and container barrels, and rebuild the image.
  • No provider code at all — Ollama speaks the Anthropic API natively (/v1/messages), so /add-ollama-provider keeps the Claude provider and just redirects it: env overrides plus a model setting.

OpenCode (/add-opencode)

Copies opencode.ts (host and container sides) from the providers branch, appends one import line to each provider barrel, adds @opencode-ai/sdk to the agent runner, installs the opencode-ai CLI in the Dockerfile, and copies registration/Dockerfile guard tests so the wiring stays verified. The provider keeps a shared opencode serve runtime — spawned once, reused across queries, and torn down on abort or when the config changes — and routes models through OpenCode’s own config: OpenRouter, DeepSeek, OpenAI, Google, Anthropic, OpenCode Zen. Configure via host .env: OPENCODE_PROVIDER, OPENCODE_MODEL (in provider/model form), optional OPENCODE_SMALL_MODEL, and ANTHROPIC_BASE_URL pointed at the upstream provider’s API base for non-Anthropic providers. API keys go in the OneCLI vault with a matching host pattern — never in .env. SDK and CLI versions are pinned together at 1.4.17; latest silently breaks session IDs.

Codex (/add-codex)

Copies the Codex payload from the providers branch — codex.ts and codex-app-server.ts (container side), the host contribution (src/providers/codex.ts, codex-agents-md.ts), and the setup module — wires one import into each of the three provider barrels, and adds the pinned @openai/codex CLI to the container manifest (container/cli-tools.json) rather than a hand-edited Dockerfile layer (no SDK dependency — Codex is a binary). The provider spawns one codex app-server child process per query and speaks JSON-RPC over stdio: native session resume, streaming events, and MCP tools, with conversation history kept server-side — the continuation is a thread id, so there’s no on-disk transcript. Codex owns its agent surfaces (providesAgentSurfaces), so the host skips the default Claude compose and the provider supplies its own, composed fresh on every spawn:
  • AGENTS.md — Codex’s project doc (codex-agents-md.ts), mounted read-only over the group dir. If the group was created from a template, its persona (instructions.prepend.md, the same provider-neutral file the Claude compose inlines) opens the doc. It carries a 32 KB Codex cap and degrades by dropping the largest instruction sections — the persona is exempt and never dropped — rather than blocking a spawn.
  • .agents/skills — Codex-native skill links synced to the group’s selected skills, plus any template skills mirrored in as real directories (templates stamp them on the Claude plane in .claude-shared/skills, which Codex never reads). The .agents dir is mounted read-only twice — at /workspace/agent/.agents and /home/node/.agents — because Codex only scans a workspace-level .agents/skills when the workspace is a git repo (the agent workspace isn’t one); the $HOME-level mount is what makes the skills discoverable.
  • ~/.codex — a per-group private state dir (.codex-shared), persistent across sessions so thread metadata and config.toml survive respawns.
Auth is vault-only, the same invariant as every other provider: the credential — a ChatGPT subscription token or an OpenAI API key — lives in the OneCLI vault under an api.openai.com / chatgpt.com host pattern, and the gateway injects it on the wire while the container sees only an onecli-managed stub auth.json. Nothing goes in .env, and the host strips OPENAI_API_KEY from the Codex process. Run the walk-through with pnpm exec tsx setup/index.ts --step provider-auth codex (ChatGPT browser/device login or API key) — the same path the setup picker uses. Model and effort come from the group’s container config (ncl groups config update --model/--effort), never env.

Ollama (/add-ollama-provider)

No files copied, no rebuild of provider code. The skill first checks that ContainerConfig supports the env and blockedHosts fields (patching src/container-config.ts and src/container-runner.ts if not), then writes the group’s container config:
ANTHROPIC_BASE_URL redirects the SDK to Ollama, the placeholder key satisfies it (Ollama ignores it), NO_PROXY bypasses the OneCLI credential gateway for the host hostname, and blockedHosts resolves api.anthropic.com to 0.0.0.0 so config drift can’t silently bill your account. The model is set in the group’s shared settings.json (data/v2-sessions/<group-id>/.claude-shared/settings.json) — Claude Code reads its model from there, not from env. Pick models that handle tool calls reliably; the upstream notes recommend Gemma 4 or Qwen 3 Coder over small 3B models.

What changes on a non-Claude provider

Verified against the provider sources on the providers branch:
  • Slash commands aren’t native. OpenCode and Codex declare supportsNativeSlashCommands = false, so the poll loop formats slash commands as ordinary chat text instead of executing them.
  • No mid-turn input. Both queue follow-up messages and drain them between turns; the Claude provider streams them into the live turn.
  • No @-import expansion. Codex’s app-server doesn’t expand @-import directives, so the Codex provider composes its own AGENTS.md (group instructions plus skills, under a 32 KB cap) fresh on each spawn instead of leaning on the host’s composed CLAUDE.md.
  • No transcript rotation. Only the Claude provider implements maybeRotateContinuation — the guard against unresumable, oversized session transcripts.

Provider capability hooks

A provider implementation declares a handful of optional capabilities. The runner and host read them by name and apply them generically — never branching on a hardcoded provider name — so each is opt-in and default-off: a provider that declares none behaves exactly as before the hooks existed. The two that shape the agent’s environment:
  • Exchange archiving (onExchangeComplete). The poll loop calls this after every completed exchange — a result, a wrapping retry, or an error — handing the provider a ProviderExchange (the prompt, the result, the continuation id, and a completed / undelivered / error status). A provider whose harness already keeps an on-disk transcript (the Claude Agent SDK writes .jsonl) omits it; one that keeps no transcript implements it to archive each round-trip itself, e.g. as markdown under the agent’s conversations/ dir. It’s best-effort — the loop catches and logs anything it throws.
  • Owning the agent surfaces (providesAgentSurfaces). By default the host composes the project doc, skill-discovery links, and provider state dir, then mounts them into the container — the composed CLAUDE.md, the skill fragments, and the provider-neutral instructions.prepend.md staging. A provider can declare at registration time that it owns these surfaces; the host then skips all of that, and the provider’s own config fn composes and mounts its equivalents (it receives the group dir and the resolved skill list to do so).
Persistent memory is deliberately not a hook (there is no usesMemoryScaffold flag): every provider shares one persistent memory/ tree. The runner scaffolds it unconditionally at boot in the agent’s host-backed workspace (/workspace/agent) — memory/index.md plus the memory/system/ doctrine files — writing only what’s missing, so the agent’s own edits and accumulated memory are never clobbered. Because the tree lives in the group dir, it persists across container restarts. What each provider must implement is registerMemorySessionHook, the interface method that wires the shared tree into that provider’s own harness.

Choosing a provider during setup

Setup asks which agent runtime should power your assistant. Claude is the default — press Enter and the standard Claude auth runs. Codex is offered as an inline install: pick it and setup runs its installer, rebuilds the image, and walks its vault-only auth (a ChatGPT subscription or an OpenAI API key). OpenCode and Ollama aren’t in the picker; add them after setup with their /add-* skills. The pick is not an install-wide default — it sets the provider on the first agent group as a per-group database property. Every group’s provider is independent, and a spawned subagent inherits its creator’s provider so a single-provider install never spawns a child on a runtime it can’t reach.

Switching a group

Set the provider (and optionally model/effort) on the group’s container config, then restart:
Config changes are saved immediately but only take effect on the next container spawn — the restart forces it. Switching back is --provider claude (or clearing the field; the fallback is claude either way). To back the code out entirely, OpenCode and Codex each ship a REMOVE.md; Ollama needs no removal — its SKILL.md’s “Reverting to Claude” steps are just deleting the env and blockedHosts keys from the container config and the model setting from the shared settings file.

What carries across a switch

Provider-neutral state stays put: group identity, wiring, members, roles, destinations, container config, skills, MCP servers, mounts, workspace files (groups/<folder>/), and conversation archives (conversations/). Agent surfaces are composed fresh at every spawn, so nothing migrates there. Agent memory carries too. The memory/ tree is universal and provider-neutral — it lives in the group’s host-backed workspace, and every provider scaffolds and reads the same tree — so the new provider picks up the memory where the old one left off. Standing instructions (instructions.prepend.md) are equally provider-neutral. (/migrate-memory still exists, but it’s a one-time cutover for legacy v1-era stores — CLAUDE.md / CLAUDE.local.md / .seed.md — into the tree, not part of a provider switch.) One thing is provider-specific and does not carry across: in-flight conversation context. Continuations are per-provider, so the new provider starts a fresh thread; the old slot is kept, so switching back resumes where you left off. Recent context is recoverable from the conversations/ archives. Switching to a provider that isn’t installed (or a typo’d name) fails the container at boot — the host logs Container exited non-zero with a stderrTail like Unknown provider: codexx. Registered: claude, codex.

Next steps

Skills catalog

The three provider-install skills at a glance

Container config

The provider, model, and effort fields in full

Credentials

How provider API keys stay out of containers

Give agents tools

Keep the same brain and add capabilities instead
Last modified on July 26, 2026