Skip to main content
NanoClaw is a lightweight AI assistant that runs Claude Agent SDK in isolated containers. The architecture prioritizes simplicity, security through true isolation, and being small enough to understand completely.

High-level overview

NanoClaw consists of a single Node.js process that orchestrates everything:

Core components

Channel factory

NanoClaw uses a factory registry pattern for messaging channels. Each channel (WhatsApp, Telegram, Discord, Slack, Gmail) self-registers at startup. Channels with missing credentials emit a warning and are skipped — no configuration file is needed to enable or disable channels. All channels implement a common Channel interface for message handling and sending, allowing the rest of the system to be channel-agnostic.

Message router

The router (src/index.ts) is the central orchestrator:
  • Polls SQLite database every 2 seconds for new messages
  • Filters messages by registered groups only
  • Checks for trigger pattern (@{ASSISTANT_NAME})
  • Maintains cursor state to track processed messages
  • Routes messages to the appropriate group queue
The main group (typically your self-chat) doesn’t require a trigger - all messages are processed automatically.

Group queue

The GroupQueue (src/group-queue.ts) manages container lifecycle and concurrency:
  • Concurrency limiting: Maximum 5 concurrent containers by default (configurable via MAX_CONCURRENT_CONTAINERS)
  • Per-group state: Each group has a dedicated queue for messages and tasks
  • Retry logic: Exponential backoff (5s base, up to 5 retries) for failed container runs
  • Idle management: Keeps containers alive for 30 minutes (default IDLE_TIMEOUT) to handle follow-up messages
  • IPC message piping: Follow-up messages are sent to active containers via IPC files
// Queue priority order when draining:
// 1. Pending tasks (won't be re-discovered from DB)
// 2. Pending messages (can be re-fetched from DB)
// 3. Waiting groups (dequeued when slots become available)
When a container is already active for a group, new messages are piped directly to the running container via IPC instead of spawning a new one.

Container runner

The container runner (src/container-runner.ts) spawns and manages isolated agent execution: Container lifecycle:
  1. Build volume mounts based on group privileges
  2. Spawn container with Docker CLI
  3. Pass prompt and metadata via stdin JSON (credentials handled by secret injection layer, never passed here)
  4. Stream stdout/stderr for real-time output
  5. Parse output markers (---NANOCLAW_OUTPUT_START--- / ---NANOCLAW_OUTPUT_END---)
  6. Clean up automatically on exit (--rm flag)
Timeout behavior:
  • Hard timeout: CONTAINER_TIMEOUT (default 30 minutes)
  • Grace period: At least IDLE_TIMEOUT + 30s to allow graceful shutdown
  • Activity-based reset: Timeout resets on each streaming output
  • Post-output timeout: Not considered an error (idle cleanup)
Logging:
  • All container runs logged to groups/{name}/logs/container-{timestamp}.log
  • Verbose mode (LOG_LEVEL=debug) logs full input/output
  • Error runs log input metadata (prompt length, session ID) and full stderr — prompt content is not included

Task scheduler

The scheduler (src/task-scheduler.ts) runs scheduled tasks:
  • Polls database every 60 seconds for due tasks
  • Supports three schedule types:
    • cron: Cron expressions (e.g., 0 9 * * * for 9am daily)
    • interval: Millisecond intervals (e.g., 3600000 for hourly)
    • once: ISO timestamp for one-time execution
  • Tasks run in group context with full agent capabilities
  • Results can be sent to the group chat or completed silently
  • Task containers close automatically 10 seconds after producing output
  1. Scheduler finds due task from database
  2. Enqueues task in GroupQueue (respects concurrency limits)
  3. Spawns container in task mode (isScheduledTask: true)
  4. Streams output and optionally sends to chat via send_message tool
  5. Logs run to database with duration and result
  6. Calculates next run time based on schedule type
  7. Container closes after 10-second grace period

IPC watcher

The IPC watcher (src/ipc.ts) enables container-to-host communication:
  • Watches data/ipc/{group}/messages/*.json for outbound messages
  • Watches data/ipc/{group}/tasks/*.json for task operations
  • Validates operations against group privileges (see security.mdx)
  • Atomic file writes (.tmp then rename) prevent race conditions
  • Each group has isolated IPC namespace
Available operations:
  • send_message: Send message to group chat (own chat only for non-main)
  • schedule_task, pause_task, resume_task, cancel_task, update_task: Task management
  • register_group, refresh_groups: Group management (main only)

Database

SQLite database (store/messages.db) stores:
  • messages: All messages with timestamps, sender info, and bot-message flag (queries capped at 200 per invocation)
  • chats: Chat metadata (name, last activity, channel, is_group)
  • sessions: Claude session IDs per group folder
  • registered_groups: Active groups with folder, trigger, container config, is_main flag
  • router_state: Message cursors and last processed timestamps
  • scheduled_tasks: Task definitions with schedule, context_mode (group or isolated), and status
  • task_run_logs: Task execution history with duration and results
The database is the source of truth for message history. If you delete it, agents lose access to conversation context.

Data flow

Incoming message flow

Follow-up message flow (piped to active container)

File system layout

nanoclaw
src
container
Dockerfile
agent-runner
skills
groups
main
CLAUDE.md
logs
{group-name}
data
sessions
{group}
.claude
agent-runner-src
ipc
{group}
messages
tasks
input
store
messages.db
auth

Container image

The agent container (container/Dockerfile) includes:
  • Base: node:24-slim
  • Browser: Chromium with all required dependencies
  • Tools: agent-browser CLI for browser automation
  • Runtime: @anthropic-ai/claude-code (Claude Agent SDK)
  • User: Runs as node user (uid 1000, non-root)
  • Working directory: /workspace/group (group’s folder)
The container is rebuilt by ./container/build.sh. Changes to agent-runner code require a rebuild.

Subsystems

Session management

Each group maintains an isolated Claude conversation session:
  • Sessions stored at data/sessions/{group}/.claude/
  • Include full message history and file contents read
  • Auto-compact when context gets too long
  • Settings configured per group:
    • CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 (enable subagent orchestration)
    • CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 (load memory from mounts)
    • CLAUDE_CODE_DISABLE_AUTO_MEMORY=0 (enable persistent memory)

Skills system

Shared skills in container/skills/ are synced to each group’s .claude/skills/ on startup:
  • Skills are available to all agents
  • Per-group copies allow customization without affecting others
  • Changes to shared skills require container restart to sync
  • Built-in container skills include /agent-browser (web automation), /capabilities (system introspection), and /status (health check)
  • /capabilities and /status are main-channel only — they check for the /workspace/project mount to enforce access

Agent runner customization

Each group gets a writable copy of agent-runner/src/ at data/sessions/{group}/agent-runner-src/:
  • Recompiled on every container startup via entrypoint.sh
  • Allows agents to add custom tools or modify behavior
  • Isolated from other groups (changes don’t affect them)
  • MCP servers can be added by modifying the agent runner code
The agent runner is the TypeScript code that wraps Claude Agent SDK. It handles IPC, streaming output, and tool registration.

Startup sequence

  1. Container system check: Ensure Docker is running, clean up orphaned containers
  2. Database initialization: Create tables if needed, load schema
  3. State loading: Restore message cursors, sessions, registered groups
  4. OneCLI agent sync: Ensure each registered non-main group has a corresponding OneCLI agent for per-group credential scoping (best-effort, non-blocking)
In v1.2.22+, this step syncs OneCLI agents for all registered groups. In earlier versions, this starts the built-in credential proxy on CREDENTIAL_PROXY_PORT.
  1. Remote Control restore: Re-adopt any surviving Remote Control session from a previous run
  2. Shutdown handlers: Register graceful shutdown on SIGTERM and SIGINT
  3. Channel connection: Connect to messaging channels, authenticate if needed
  4. Subsystem startup:
    • Task scheduler loop (60s interval)
    • IPC watcher (1s poll interval)
    • Message loop (2s poll interval)
  5. Recovery: Check for unprocessed messages from previous crash
  6. Ready: System begins processing messages and tasks

Graceful shutdown

On SIGTERM or SIGINT:
  1. GroupQueue enters shutdown mode (stops accepting new work)
  2. Active containers are detached (not killed)
  3. Channels disconnect gracefully
  4. Process exits with code 0
Containers are intentionally not killed during shutdown to prevent data loss from channel reconnection restarts. They’ll finish on their own via idle timeout or container timeout.
Last modified on March 24, 2026