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 resolved model and effort through to the provider.
Every code path that creates a session on trunk writes agent_provider = null, and ncl sessions is read-only (list/get/history) — 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. An unset group model inherits NANOCLAW_DEFAULT_MODEL, then the SDK default; unset effort uses 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.
Set NANOCLAW_FAST_MODE=true in the host environment or .env to opt into the Claude SDK’s faster, higher-priced serving tier. NanoClaw passes it as settings.fastMode; it is not a per-group model or effort value.
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. - Legacy local-model workflow —
/add-ollama-providerredirects the Claude provider, but its source/config edits need reconciliation with the current runtime; see the limitations below.
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, adds the opencode-ai CLI to container/cli-tools.json, and copies registration tests plus a CLI/SDK pin guard 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. Three more optional vars shape the main model’s declaration: OPENCODE_MODEL_CONTEXT_LIMIT (context window; invalid values are logged and ignored), OPENCODE_MODEL_OUTPUT_LIMIT (applied only alongside a valid context limit), and OPENCODE_MODEL_INPUT_MODALITIES (comma-separated subset of text,audio,image,video,pdf). They apply to the main model only — the small model never inherits them. 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; mismatched versions can build cleanly and fail at runtime. The server runs in /workspace/agent and explicitly loads the composed CLAUDE.md (plus the legacy CLAUDE.local.md if present). Group effort is forwarded as reasoningEffort for the main model. If a resumed session finishes without any assistant work, the provider retries once with a fresh session and the same opening prompt.
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 mounts and the provider supplies its own, composed fresh on every spawn. The composer itself is shared: Codex calls the same trunk composer every provider uses, composeGroupProjectDoc in src/project-doc-compose.ts, and only passes a different spec. The spec names the file, the shared base, the provider’s own blocks, and the byte cap.
AGENTS.md: Codex’s project document (codex-agents-md.ts), mounted read-only over the group dir. Its base iscontainer/AGENTS.mdand it carries two Codex-specific blocks. Wheneverinstructions.prepend.mdholds anything, it is inlined first as the# Personasection, exactly as it is for Claude. The Codex cap is 32 KiB against Claude’s 4 MiB. Over the cap, the composer drops the largest droppable sections and appends an# Omitted for sizesection instead of failing the spawn. The persona is never droppable..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 resolved container config. Explicit group settings use ncl groups config update --model/--effort; an unset group model can inherit NANOCLAW_DEFAULT_MODEL.
On a pinned hardened-runtime install, adding Codex works without leaving the pre-built image: its CLI lands via
container/cli-tools.json, and the rebuild applies that manifest as an overlay layer on top of the pulled image rather than starting a build from scratch. Adding OpenCode instead edits container/agent-runner/bun.lock (the pinned @opencode-ai/sdk dependency) — the agent runner’s own dependencies are baked into the base layer, so the lockfile guard refuses to overlay a changed lockfile and the rebuild drops to a local build (./container/build.sh build) instead.Ollama (/add-ollama-provider)
The skill remains in the repository, but its instructions target an older configuration path: they patch readContainerConfig and Docker argument assembly, then edit a generated group container.json. Current NanoClaw materializes that file from database-backed configuration and realizes a validated session spec through a driver. Those legacy edits are not a supported drop-in switch on current main.
Before using this skill, its integration needs to be ported and tested against the current config and admission paths. Do not treat direct edits to generated container.json as persistent configuration. Local-model tools such as Atomic Chat and Ollama are a separate option: they give an existing agent a model to call rather than replacing its provider.
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 provider-native commands are formatted as ordinary chat text./clearis handled by the shared poll loop before provider selection: it resets the current provider continuation, not workspace files, memory, or stored history. - No mid-turn input. Both queue follow-up messages and drain them between turns; the Claude provider streams them into the live turn.
- A different project document, not a different composer. Codex composes
AGENTS.mdrather thanCLAUDE.md, fromcontainer/AGENTS.mdrather thancontainer/CLAUDE.md, with two extra blocks and a 32 KiB cap instead of 4 MiB. The composition itself is the shared trunk one, and the composer emits no@-imports for any provider. - 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 document, syncs the skill-discovery links, and mounts the result: the composedCLAUDE.mdre-mounted read-only on top of the read-write group dir, plus the group’s own.claudestate directory at/home/node/.claude, read-write and shared across that group’s sessions. 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.
The agent mailbox is a separate seam
The mailbox is the storage behind an agent’s messages, tasks, and container state. Since v2.3.0 it sits behind its own registry, mirrored on both sides:AgentMailbox on the host and AgentMailbox in the runner, each with a one-slot registry and a compose.ts that installs the default implementation. The default is the SQLite pair that has always backed a session, and the on-disk layout is unchanged: data/v2-sessions/<agent-group-id>/<session-id>/{inbound,outbound}.db.
A provider implements none of it. The two seams install differently. A provider skill appends one import line to providers/index.ts. A mailbox skill swaps the contents of mailbox/compose.ts and leaves the modules barrel’s import of it intact. The host hands the runner its mailbox context in a read-only file at /app/.nanoclaw-session.json, and a missing file is treated as a pre-seam host rather than an error.
File events
ProviderEvent accepts { type: 'file'; path: string }: a provider announcing a file it produced during the turn, by absolute container path. There is no other field.
Codex is the only provider that emits one today. It snapshots the images it has generated before a turn and yields one file event per new path just before the result event. The poll loop’s event switch has no file case, so nothing acts on the path; the event still refreshes the session heartbeat like any other.
That gap is visible on Codex today. Its native client renders a generated image itself, so the model treats delivery as done and never calls send_file. Until something consumes the event, the image is announced and not delivered.
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 is installed after setup with/add-opencode; the legacy Ollama workflow has the limitations described above.
Setup also asks, earlier and separately, where the sandbox image itself comes from — see the Echo hardened runtime. That question is scoped to Claude: it’s only offered when the planned provider is Claude, and picking any other provider builds the sandbox locally instead.
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; reverting a legacy Ollama customization requires reviewing the actual source and configuration changes it applied.
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