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. claude passes CLAUDE_CODE_AUTO_COMPACT_WINDOW through when it is set to a positive integer. A custom Anthropic-compatible endpoint (ANTHROPIC_BASE_URL from .env plus a gateway-managed placeholder token) is added by the selected gateway, not by the provider. 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 default every group that has no speed of its own to the fast speed tier. A group can override that with ncl groups config update --id <group-id> --speed standard|fast; --speed "" clears the group value so it follows the install default. Each provider declares which speed tiers it accepts (Claude and Codex accept standard and fast), and a provider that declares none accepts only the empty value. For Claude, fast is the SDK’s faster, higher-priced serving tier, which NanoClaw passes as settings.fastMode.
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 four provider skills split into two mechanisms:- Real provider implementations — OpenCode, Codex and Cursor are TypeScript providers.
/add-opencode,/add-codexand/add-cursorcarry their files in the skill’s ownpayload/folder and copy them in. Each wires them into the host, container and setup barrels and rebuilds 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 the OpenCode files (provider, config, turn, memory and auth helpers, the host contribution and provider contract, and the setup and scripts/opencode-*.ts helpers) from the skill’s own payload/ folder, appends one import line to each of the five provider and contract barrels, 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.
Settings (host .env): OPENCODE_PROVIDER, OPENCODE_MODEL (in provider/model form), optional OPENCODE_SMALL_MODEL, and OPENCODE_BASE_URL (the backend URL, or native). When OPENCODE_BASE_URL is unset, the provider falls back to ANTHROPIC_BASE_URL. OPENCODE_AUTH_MODE=chatgpt selects ChatGPT sign-in; leave it unset for API-key and local endpoints. The easiest route is pnpm exec tsx setup/index.ts --step provider-auth opencode, which writes these values and stores the credential in the selected gateway.
Model declaration (optional): three 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. Two more optional vars, OPENCODE_NATIVE_ATTACHMENT_MAX_COUNT and OPENCODE_NATIVE_ATTACHMENT_MAX_BYTES, limit already-staged structured attachments.
Credentials: API keys and ChatGPT logins go in the selected gateway’s vault (see Credentials) — never in .env. SDK and CLI versions are pinned together at 1.18.25; mismatched versions can build cleanly and fail at runtime.
At run time: the server runs in /workspace/agent and explicitly loads the composed CLAUDE.md, the legacy CLAUDE.local.md if present, and a per-turn instructions file (nanoclaw-instructions.md under $XDG_DATA_HOME) that holds the memory snapshot and the delivery reminder. Group effort is forwarded as reasoningEffort for the main model. A turn that fails after earlier steps completed returns the completed text plus one error result; the provider does not replay the prompt.
Codex (/add-codex)
Copies the Codex files from the skill’s own payload/ folder — 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 five provider and contract 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). In v2026.10.0 and earlier, /add-codex copies these files from the locked providers branch instead. 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. Each exchange that ends with a text reply is archived as markdown in the group’s conversations/ folder by core’s shared exchange archive; the skill ships no archive of its own.
Codex declares its agent surfaces in a host contract (src/provider-contracts/codex.ts), so the host skips the default mounts and realizes the Codex ones, composed fresh on every spawn. The composer itself is shared: Codex goes through the same trunk composer every provider uses, composeGroupProjectDoc in src/project-doc-compose.ts, and the contract only names the file, the instruction facts, and the byte cap.
AGENTS.md: Codex’s project document, mounted read-only over the group dir. It is composed from the same core-owned template asCLAUDE.md(container/CLAUDE.md); Codex only declares facts that are rendered into it: the override files agents must not use for memory (named in the document’s Memory section) and where its native skills live (a “Native Runtime Skills” section). 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.
auth.json with placeholder tokens. A ChatGPT login’s stand-in also carries your ChatGPT workspace id, which Codex 0.156.0 and later look up before any model request. The id is an identifier, not a credential, and the agent can read it. The gateway skill supplies it, so refresh the gateway and /add-codex together with /update-nanoclaw, not /add-codex alone on an older gateway. If the host log says OneCLI login stand-in keeps its placeholder account id (the vault’s ChatGPT logins do not all name the same workspace id, or one has no recorded id) or Iron Codex login has no recorded workspace id; sign in again (a login saved before it was recorded), the agent’s ChatGPT requests fail until you fix it. A OneCLI secret metadata refused warning is a different problem, a refused request for the vault’s secret list, and deleting logins does not fix it. On Iron, run the sign-in command below again. On OneCLI, delete the extra or stale login (onecli secrets list, then onecli secrets delete --id <id>), run the sign-in command below, and grant the new login to each agent that had the old one; if the command says the account is already connected, the vault still holds another OpenAI secret (an API key counts) and the command signs in only when none is left, so remove the one you no longer need first. The stand-in is built when a container starts, so afterwards run ncl groups restart --id <group-id> for each agent you fixed. 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 OpenCode and Codex provider sources:- 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, from the samecontainer/CLAUDE.mdtemplate, with a generated “Native Runtime Skills” section 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. Core ships a shared helper for that,archiveProviderExchangeincontainer/agent-runner/src/provider-contracts/exchange-archive.ts, which Codex and Cursor use. It’s best-effort — the loop catches and logs anything it throws. -
Declaring the agent surfaces (host contract). By default the host composes the project document, syncs the skill-discovery links, and mounts the result: the composed
CLAUDE.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 instead register a host contract that names its own project document (file name, instruction facts, size cap), state volumes, skill directories and prepared files; core then composes and mounts those. The olderprovidesAgentSurfacesflag is still accepted for payloads written before contracts existed.
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 and OpenCode are offered as inline installs. Pick one and setup runs its installer, rebuilds the sandbox image, and walks you through sign-in; the credential is stored in the selected gateway. Codex accepts a ChatGPT subscription or an OpenAI API key. OpenCode accepts ChatGPT sign-in, a local OpenAI-compatible endpoint, or an API key for OpenRouter, DeepSeek, OpenAI, Google or Anthropic. You can also runpnpm exec tsx setup/index.ts --step provider-auth opencode or /add-opencode later. 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 saved as DEFAULT_AGENT_PROVIDER in .env, so groups created later start on it (existing groups are never changed), and setup also sets it on the first agent group. Every group’s provider stays independent and can be changed with ncl groups config update --provider, 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