NanoClaw runs all agents inside containers (lightweight Linux VMs) to provide true OS-level isolation. This is the primary security boundary that makes Bash access and code execution safe.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.
Why containers?
Containers provide:- Process isolation — agent processes can’t affect the host system
- Filesystem isolation — agents only see explicitly mounted directories
- Resource limits — CPU/memory can be constrained (future)
- Non-root execution — agents run as unprivileged user
- Signal forwarding —
tinias PID 1 ensures clean shutdown
NanoClaw uses Docker by default for cross-platform compatibility. On macOS, you can use Apple Container instead (via
/convert-to-apple-container skill).Container architecture
Base image
The container is built fromcontainer/Dockerfile:
Key components:
- Base:
node:22-slim(Debian-based, minimal) - Runtime: Bun (runs agent-runner TypeScript directly — no compilation step)
- Browser: Chromium with all dependencies
- Tools:
agent-browserfor browser automation,vercelCLI,curl,git - SDK:
@anthropic-ai/claude-codeinstalled globally via pnpm - PID 1:
tinifor proper signal forwarding sooutbound.dbwrites finalize on SIGTERM - User:
node(uid 1000, non-root) - Working directory:
/workspace/group
Source code is NOT baked into the image.
/app/src is a read-only bind mount from the host. Source-only changes never require an image rebuild.Entrypoint flow
The entrypoint usestini for signal forwarding:
- tini starts as PID 1 (forwards signals cleanly)
- entrypoint.sh runs setup scripts
- Bun executes agent-runner:
exec bun run /app/src/index.ts - Agent-runner polls
inbound.dbfor messages and writes responses tooutbound.db - Container exits when stopped by the host
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 (messages, routing, destinations)
- outbound.db — container writes, host reads (responses, acknowledgments, state)
Volume mounts
Containers only see what’s explicitly mounted. The v2 mount structure is different from v1:Per-session mounts
| Mount | Container path | Mode | Purpose |
|---|---|---|---|
| Session folder | /workspace | Read-write | inbound.db, outbound.db, outbox/, .claude/ |
| Agent group folder | /workspace/agent | Read-write | Working files, CLAUDE.local.md |
| Container config | /workspace/agent/container.json | Read-only | Materialized from DB at spawn time (agent can’t modify) |
| Composed CLAUDE.md | /workspace/agent/CLAUDE.md | Read-only | Regenerated each spawn |
| CLAUDE.md fragments | /workspace/agent/.claude-fragments | Read-only | Fragment files for composition |
| Global memory | /workspace/global | Read-only | groups/global/ directory |
| Shared CLAUDE.md | /app/CLAUDE.md | Read-only | Base CLAUDE.md |
| Agent-runner source | /app/src | Read-only | Shared source (bind mount from host) |
| Container skills | /app/skills | Read-only | Shared skill definitions |
| Claude SDK state | /home/node/.claude | Read-write | SDK state + skill symlinks |
| Additional mounts | /workspace/extra/{name} | Per-config | From container config (validated against allowlist) |
Mount security
All additional mounts are validated against the allowlist at~/.config/nanoclaw/mount-allowlist.json:
- If no allowlist file exists, all additional mounts are blocked
- Blocked patterns are merged with hardcoded defaults (
.ssh,.gnupg,.aws,.kube,.docker,credentials,.env,.netrc,.npmrc,id_rsa,id_ed25519,private_key,.secret, etc.) - Paths are resolved through symlinks before validation
- A path must be under an
allowedRootand not match any blocked pattern - Read-write access requires both the mount and the allowed root to permit it; otherwise forced read-only
- Container paths must be relative, non-empty, no
.., no:(prevents Docker option injection)
Missing-file state is NOT cached — you can create the allowlist without restarting. However, parse errors are permanently cached for the process lifetime.
Container lifecycle
Wake and spawn
Container spawning is deduplicated — concurrent wake calls for the same session share a single in-flight promise:wakeContainer(session)checks for existing running container or in-flight spawnspawnContainer(session)reads agent group config, resolves provider contributions, builds mounts- Skill symlinks are synced before every spawn
- Docker container is spawned with
tinias PID 1
Execution
- Agent-runner starts:
bun run /app/src/index.ts - Polls inbound.db: discovers new messages via the poll loop
- Processes messages: runs through the configured provider (Claude by default)
- Writes outbound.db: responses, acknowledgments, task operations
- Heartbeat:
.heartbeatfile mtime updated periodically
Shutdown
- Host-initiated:
docker stopsends SIGTERM;tiniforwards to Bun process - Stale detection: host sweep detects containers with old heartbeats or stuck processing_ack
- Fallback: SIGKILL if graceful stop fails
onExitcallback:killContaineraccepts an optional callback that fires after the process actually exits, used by the restart flow to guarantee the old container is gone before the new one spawns
Even if the container crashes, all data in session databases and mounted directories persists. Only the container process itself is ephemeral.
Explicit restart
ncl groups restart --id <group-id> [--rebuild] [--message <text>] kills running containers for the agent group and lets them respawn:
- Without
--message, containers come back on the next inbound message - With
--message, anon_wakemessage is queued ininbound.dband the container respawns immediately via theonExitcallback --rebuildforces an image rebuild before respawn (useful after package changes)- 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 fresh container’s first poll iteration. This prevents the dying container — still in its SIGTERM grace window — from stealing the message before it exits.
Per-agent-group images
Agent groups can specify custom packages in their container config (thecontainer_configs DB table — materialized to container.json at spawn time). The host builds a derived Docker image with additional apt and npm packages:
- Image tag: derived from the checkout-scoped base image and agent group
- Built on top of the base
nanoclaw-agent-v2-<slug>:latestimage - Cached — only rebuilt when package lists change
- Image tag is persisted to the DB after build
Timeouts
Container timeout
- Default: 30 minutes (
CONTAINER_TIMEOUT) - Configurable: per-agent-group via container config
- Enforcement:
docker stop(SIGTERM), falls back to SIGKILL
Stale detection
The host sweep detects stuck containers by checking:.heartbeatfile modification time- Age of unacknowledged
processing_ackentries - Container state tracking in the session table
Skills and MCP servers
Skills are synced to each container via symlinks:- Symlink sync: before every spawn, skill symlinks at
/home/node/.claude/skills/are updated to point to/app/skills/{name} - Container-valid paths: symlinks are dangling on the host but valid inside the container
- Available to agent: Claude Agent SDK loads from
.claude/skills/
Built-in MCP server
Thenanoclaw MCP server provides tools for container-to-host communication via the database:
send_message— write outbound messagesschedule_task,cancel_task,pause_task,resume_task,update_task— task managementlist_tasks— view scheduled tasks
ncl groups config add-mcp-server (writes to the container_configs DB table).
Global memory injection
For non-main groups, if/workspace/global/CLAUDE.md exists, its contents are appended to the system prompt. This provides shared instructions across all agent groups.
Additional directory auto-discovery
Directories mounted at/workspace/extra/* are automatically passed to the SDK as additionalDirectories, so any CLAUDE.md files in those directories are loaded automatically.
Browser automation
Chromium runs inside the container:- Executable:
/usr/bin/chromium - CLI:
agent-browser(installed globally) - Headless: always (no display in container)
- User data: stored in group folder (persists across runs)
- Network: full access (same as host, no restrictions)
- Optional CJK fonts: install via
INSTALL_CJK_FONTS=truebuild arg (~200 MB)
Security implications
What containers protect against
- Filesystem access — agents can’t read
~/.sshor other sensitive paths - Process interference — agents can’t kill host processes or inject code
- Persistence — containers are ephemeral, no state survives unless mounted
- Privilege escalation — non-root execution limits kernel attack surface
What containers DON’T protect against
- Network access — agents have full network access (can exfiltrate data)
- Mounted directory tampering — agents can modify anything in mounted read-write directories
- Vault-based API access — containers can make authenticated API requests through the OneCLI vault (though they cannot extract real credentials)
- Resource exhaustion — no CPU/memory limits enforced (can DoS host)
Troubleshooting
Container won’t start
- Check Docker is running:
docker ps - Check image exists:
docker images | grep nanoclaw-agent - Rebuild image:
./container/build.sh
Container timeout
- Check timeout setting:
CONTAINER_TIMEOUTin.env - Check if task is legitimately slow (increase timeout)
- Review host sweep logs for stale detection
Permission errors
- Check mount paths are readable by host user
- Check uid/gid mapping
- Verify allowlist includes path (for additional mounts)
- Check symlink resolution didn’t change path