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):
sessions.agent_provider— per-session overridecontainer_configs.provider— the group’s container config'claude'
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. Itsmodel 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
providersregistry branch./add-opencodeand/add-codexcopy 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-providerkeeps 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.agentsdir is mounted read-only twice — at/workspace/agent/.agentsand/home/node/.agents— because Codex only scans a workspace-level.agents/skillswhen 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 andconfig.tomlsurvive respawns.
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 theproviders 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
@-importexpansion. Codex’s app-server doesn’t expand@-importdirectives, so the Codex provider composes its ownAGENTS.md(group instructions plus skills, under a 32 KB cap) fresh on each spawn instead of leaning on the host’s composedCLAUDE.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 aProviderExchange(the prompt, the result, the continuation id, and acompleted/undelivered/errorstatus). 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’sconversations/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 composedCLAUDE.md, the skill fragments, and the provider-neutralinstructions.prepend.mdstaging. 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).
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:--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