NanoClaw runs agents in isolated Linux containers to provide security through OS-level process and filesystem isolation. In v2, the container runtime uses a two-database IO model instead of stdin/stdout piping, and the agent-runner runs on Bun instead of Node.js.Documentation Index
Fetch the complete documentation index at: https://qwibitai-nanoclaw-8-mintlify-container-config-db-1778268498.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Runtime abstraction
All runtime-specific logic lives insrc/container-runtime.ts:
- Docker (default) — cross-platform support (macOS, Linux, Windows via WSL2)
- Apple Container (macOS only) — lightweight native runtime
CONTAINER_RUNTIME_BIN:
Apple Container vs Docker
Apple Container is Apple’s native virtualization framework (macOS 15+). It runs Linux containers without a VM layer like Docker Desktop.When to use Apple Container
- You’re on macOS 15 (Sequoia) or later
- You want to avoid installing Docker Desktop
- You want faster container startup
When to stick with Docker
- You’re on Linux or Windows (WSL2)
- You need cross-platform parity
- You’re deploying to a production server
Key differences
| Docker | Apple Container | |
|---|---|---|
| Binary | docker | container |
| Bind mounts | -v host:container:ro | --mount type=bind,source=...,target=...,readonly |
| Stop command | docker stop -t 1 name | container stop name |
| Health check | docker info | container system status |
| Platform | macOS, Linux, Windows (WSL2) | macOS 15+ only |
Switching runtimes
Run the/convert-to-apple-container skill in Claude Code. To revert, use git revert.
Container image
The agent container is built fromcontainer/Dockerfile and includes:
- Node.js 22 — base image runtime
- Bun (pinned to 1.3.12) — runs agent-runner TypeScript directly (no compilation)
- Chromium — browser automation via agent-browser
- Claude Code SDK —
@anthropic-ai/claude-codeinstalled globally via pnpm - tini — PID 1 signal forwarding (ensures outbound.db writes finalize on SIGTERM)
- pnpm (via corepack) — for global Node CLI installs
- System tools —
curl,git,ca-certificates,unzip - Optional CJK fonts —
fonts-noto-cjk(~200 MB, opt-in viaINSTALL_CJK_FONTS=true)
Key design decisions
- Source is NOT baked in —
/app/srcis a read-only bind mount from the host. Source changes never require an image rebuild. only-built-dependenciesallowlist in.npmrcforagent-browserand@anthropic-ai/claude-code- Runs as
nodeuser (non-root) with/workspace/groupas working directory - Entrypoint:
tini -> entrypoint.sh -> exec bun run /app/src/index.ts
Building the image
Per-agent-group images
Agent groups can specify custom packages in their container config (stored in thecontainer_configs DB table). The host builds a derived Docker image:
- Tag: derived from the checkout-scoped base image and agent group
- Built on top of
nanoclaw-agent-v2-<slug>:latest - Adds custom apt and npm packages
- Resulting image tag is written back to the
container_configs.image_tagcolumn
Container configuration storage
Per-agent-group runtime config lives in thecontainer_configs table in the central DB. The legacy groups/<folder>/container.json file is now a materialized view — written by the host at spawn time, read by the container at startup. The container has no idea the DB exists; nothing inside the container changed.
Schema
Thecontainer_configs table has one row per agent group, with both scalar and JSON columns:
| Column | Type | Purpose |
|---|---|---|
agent_group_id | TEXT (PK, FK) | References agent_groups.id (cascades on delete) |
provider | TEXT | Agent provider override (claude, opencode, etc.) |
model | TEXT | Model name (e.g., claude-sonnet-4-6) |
effort | TEXT | Reasoning effort hint |
image_tag | TEXT | Persisted Docker image tag for per-group builds |
assistant_name | TEXT | Display name in system prompt |
max_messages_per_prompt | INTEGER | Override for MAX_MESSAGES_PER_PROMPT |
skills | TEXT (JSON) | "all" or ["skill1", ...] |
mcp_servers | TEXT (JSON) | Record<string, McpServerConfig> |
packages_apt | TEXT (JSON) | string[] of apt packages |
packages_npm | TEXT (JSON) | string[] of npm packages |
additional_mounts | TEXT (JSON) | AdditionalMountConfig[] |
cli_scope | TEXT | disabled | group (default) | global — controls in-container ncl access |
updated_at | TEXT | ISO timestamp |
Backfill
On startup, the host runs a one-time backfill that seedscontainer_configs rows from any existing groups/<folder>/container.json files (and the legacy agent_groups.agent_provider column). The backfill is idempotent — it skips groups that already have a row.
Provider cascade
Provider resolution simplified from a 3-step cascade to 2 steps:agent_groups.agent_provider column is retained for backwards compat but no longer participates in resolution and is no longer exposed via ncl groups. Configure provider via ncl groups config update --id <group-id> --provider <name>.
Updating config
All writes go through the DB layer. Multi-word verbs accept either spaces (preferred) or dashes —ncl groups config get and ncl groups config-get both work.
ncl groups config update— change scalar fields (--provider,--model,--effort,--image-tag,--assistant-name,--max-messages-per-prompt,--cli-scope)ncl groups config add-mcp-server/config remove-mcp-server— manage MCP serversncl groups config add-package/config remove-package— manage apt/npm packages- Self-mod approvals (
install_packages,add_mcp_server) — write to DB instead of file ncl groups config get— view current config (open access; others require approval)
restartAgentGroupContainers() is invoked by self-mod approvals to apply config changes.
CLI scope
cli_scope controls what an in-container agent can do via ncl:
| Value | Behavior |
|---|---|
disabled | Agent never learns about ncl (the instructions section is excluded from the composed CLAUDE.md). The host CLI dispatcher rejects any cli_request. |
group (default) | Agent can call groups, sessions, destinations, members only, scoped to its own agent group. --id, --agent_group_id, and --group args are auto-filled. Cross-group reads are rejected post-handler; help output reflects the scope. cli_scope arg is blocked outright. |
global | Unrestricted — current behavior. Set automatically for owner agent groups by init-first-agent. |
Explicit restart
- Kills running containers for the agent group; without
--message, they come back on the next inbound message - With
--message, anon_wakerow is written toinbound.dband the container respawns immediately via theonExitcallback --rebuildforces an image rebuild before respawn (useful after package changes); package commands no longer trigger a build implicitly- Called from inside a container,
--idis auto-filled and only the calling session is restarted
on_wake flag on messages_in ensures wake messages are delivered only on the new container’s first poll iteration. Without it, the dying container — still in its SIGTERM grace window — could steal the message before exiting. killContainer accepts an onExit callback that fires after the process actually exits, guaranteeing race-free respawn.
Two-database IO model
In v2, all communication between host and container uses two SQLite databases per session. There is no stdin/stdout piping, no IPC files, and no output markers.inbound.db (host writes, container reads)
| Table | Purpose |
|---|---|
messages_in | Inbound messages, tasks, system notifications |
delivered | Tracks delivery outcomes for outbound message IDs |
destinations | Live destination map (channels and other agents) |
session_routing | Default reply routing (channel_type, platform_id, thread_id) |
outbound.db (container writes, host reads)
| Table | Purpose |
|---|---|
messages_out | Outbound messages with deliver_after and recurrence |
processing_ack | Tracks which inbound messages the container has processed |
session_state | Persistent key/value store (e.g., SDK session ID for resume) |
container_state | Tool-in-flight state for stuck-detection |
Cross-mount invariants
Three invariants are critical for correctness:journal_mode=DELETE— WAL’s mmapped-shmdoesn’t refresh across Docker mounts- Host opens-writes-closes per operation — closing invalidates the container’s page cache
- One writer per file — DELETE-mode journal unlink isn’t atomic across the mount
Container lifecycle
Spawning containers
Containers are spawned by thespawnContainer function. Wake calls are deduplicated via an in-flight promise map.
Materialize container config
The host reads the agent group’s row from the
container_configs DB table and writes it as groups/<folder>/container.json. This file is a materialized view — the DB is the source of truth, and the file is regenerated on every spawn so the runner always sees fresh config. Provider contributions are resolved from this config.Build volume mounts
Mounts are built based on the session, agent group, and validated additional mounts.
Sync skill symlinks
Skill symlinks at
/home/node/.claude/skills/ are updated to point to /app/skills/{name}. These are dangling on the host but valid inside the container.Volume mounts
| Path | Container path | Mode | Purpose |
|---|---|---|---|
| Session folder | /workspace | RW | inbound.db, outbound.db, outbox/, inbox/ |
| Agent group folder | /workspace/agent | RW | Working files |
| container.json | /workspace/agent/container.json | RO | Materialized from DB at spawn time |
| Composed CLAUDE.md | /workspace/agent/CLAUDE.md | RO | Regenerated each spawn |
| Global memory | /workspace/global | RO | Shared instructions |
| Agent-runner source | /app/src | RO | Bind mount from host |
| Container skills | /app/skills | RO | Shared skill definitions |
| Claude SDK state | /home/node/.claude | RW | SDK state + skill symlinks |
| Additional mounts | /workspace/extra/{name} | Per-config | Validated against allowlist |
| Provider mounts | Various | Per-provider | Provider-contributed |
Timeouts and stale detection
Containers have two timeout/detection mechanisms:- Container timeout — maximum runtime before force kill (default: 30 minutes)
- Stale detection — host sweep checks
.heartbeatmtime andprocessing_ackage to detect stuck containers
Container shutdown
killContainer(sessionId, reason)stops the container viadocker stop, falls back to SIGKILL- On close/error, the session is marked stopped and typing indicators are cleared
Credential injection
The OneCLI SDK’sapplyContainerConfig() configures each container’s network to route through the vault:
- Injects
HTTPS_PROXYand CA certs into Docker args - All container API calls route through the vault
- No raw API keys are passed via environment variables
- Each agent group gets its own
agentIdentifierfor credential scoping
Debugging containers
List running containers
List running containers
View container logs
View container logs
Inspect container mounts
Inspect container mounts
Execute commands in running container
Execute commands in running container
Stop a running container
Stop a running container
Related pages
- Security model — container isolation and security boundaries
- Troubleshooting — common container issues