Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.nanoclaw.dev/llms.txt

Use this file to discover all available pages before exploring further.

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.

Runtime abstraction

All runtime-specific logic lives in src/container-runtime.ts:
  • Docker (default) — cross-platform support (macOS, Linux, Windows via WSL2)
  • Apple Container (macOS only) — lightweight native runtime
The runtime binary is specified by CONTAINER_RUNTIME_BIN:
export const CONTAINER_RUNTIME_BIN = 'docker';

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

DockerApple Container
Binarydockercontainer
Bind mounts-v host:container:ro--mount type=bind,source=...,target=...,readonly
Stop commanddocker stop -t 1 namecontainer stop name
Health checkdocker infocontainer system status
PlatformmacOS, 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 from container/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-code installed globally via pnpm
  • tini — PID 1 signal forwarding (ensures outbound.db writes finalize on SIGTERM)
  • pnpm (via corepack) — for global Node CLI installs
  • System toolscurl, git, ca-certificates, unzip
  • Optional CJK fontsfonts-noto-cjk (~200 MB, opt-in via INSTALL_CJK_FONTS=true)

Key design decisions

  • Source is NOT baked in/app/src is a read-only bind mount from the host. Source changes never require an image rebuild.
  • only-built-dependencies allowlist in .npmrc for agent-browser and @anthropic-ai/claude-code
  • Runs as node user (non-root) with /workspace/group as working directory
  • Entrypoint: tini -> entrypoint.sh -> exec bun run /app/src/index.ts

Building the image

./container/build.sh

Per-agent-group images

Agent groups can specify custom packages in container.json. 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

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)

TablePurpose
messages_inInbound messages, tasks, system notifications
deliveredTracks delivery outcomes for outbound message IDs
destinationsLive destination map (channels and other agents)
session_routingDefault reply routing (channel_type, platform_id, thread_id)

outbound.db (container writes, host reads)

TablePurpose
messages_outOutbound messages with deliver_after and recurrence
processing_ackTracks which inbound messages the container has processed
session_statePersistent key/value store (e.g., SDK session ID for resume)
container_stateTool-in-flight state for stuck-detection

Cross-mount invariants

Three invariants are critical for correctness:
  1. journal_mode=DELETE — WAL’s mmapped -shm doesn’t refresh across Docker mounts
  2. Host opens-writes-closes per operation — closing invalidates the container’s page cache
  3. One writer per file — DELETE-mode journal unlink isn’t atomic across the mount

Container lifecycle

Spawning containers

Containers are spawned by the spawnContainer function. Wake calls are deduplicated via an in-flight promise map.
1

Read agent group config

The host reads container.json and resolves provider contributions.
2

Build volume mounts

Mounts are built based on the session, agent group, and validated additional mounts.
3

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.
4

Compose CLAUDE.md

A composed CLAUDE.md is regenerated and mounted read-only.
5

Spawn container

Docker container is spawned with tini as PID 1.
6

Track container state

Session is marked as running in the central DB.

Volume mounts

PathContainer pathModePurpose
Session folder/workspaceRWinbound.db, outbound.db, outbox/, inbox/
Agent group folder/workspace/agentRWWorking files
container.json/workspace/agent/container.jsonRONested read-only config
Composed CLAUDE.md/workspace/agent/CLAUDE.mdRORegenerated each spawn
Global memory/workspace/globalROShared instructions
Agent-runner source/app/srcROBind mount from host
Container skills/app/skillsROShared skill definitions
Claude SDK state/home/node/.claudeRWSDK state + skill symlinks
Additional mounts/workspace/extra/{name}Per-configValidated against allowlist
Provider mountsVariousPer-providerProvider-contributed

Timeouts and stale detection

Containers have two timeout/detection mechanisms:
  1. Container timeout — maximum runtime before force kill (default: 30 minutes)
  2. Stale detection — host sweep checks .heartbeat mtime and processing_ack age to detect stuck containers

Container shutdown

  • killContainer(sessionId, reason) stops the container via docker stop, falls back to SIGKILL
  • On close/error, the session is marked stopped and typing indicators are cleared

Credential injection

The OneCLI SDK’s applyContainerConfig() configures each container’s network to route through the vault:
const onecliApplied = await onecli.applyContainerConfig(args, {
  addHostMapping: false,
  agent: agentIdentifier,
});
  • Injects HTTPS_PROXY and 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 agentIdentifier for credential scoping

Debugging containers

docker ps --filter name=nanoclaw-
docker logs nanoclaw-{session-id}
docker inspect nanoclaw-{session-id} | jq '.[0].Mounts'
docker exec -it nanoclaw-{session-id} /bin/bash
docker stop -t 1 nanoclaw-{session-id}
Last modified on April 28, 2026