mech.app
Dev Tools

NanoClaw: Containerized Agent Execution with Message-App Integration

How NanoClaw isolates Claude agents in Docker containers and connects them to WhatsApp, Telegram, and Slack without shared-memory risks.

Source: github.com
NanoClaw: Containerized Agent Execution with Message-App Integration

NanoClaw is a lightweight agent framework that runs Claude agents in isolated Docker containers and connects them to messaging platforms (WhatsApp, Telegram, Slack, Discord, Gmail). It positions itself as a minimal alternative to OpenClaw, trading feature breadth for a codebase small enough to audit and customize.

The project has 29,015 stars and ranks #5 on GitHub Trending for TypeScript. The author built it because OpenClaw’s 500,000 lines of code and 70+ dependencies made security review impractical. NanoClaw reduces the trusted codebase to one Node process and a handful of files, with agents running in separate Linux containers instead of shared memory.

The Shared-Memory Problem

OpenClaw runs all agents in a single Node.js process. Agents share the same JavaScript heap, event loop, and file descriptors. Security boundaries are enforced at the application level: allowlists, pairing codes, and permission checks. If an agent exploits a vulnerability in the runtime (prototype pollution, buffer overflow in a native module, or a compromised dependency), it can access other agents’ memory and credentials.

This creates three deployment risks:

  • Cross-agent contamination: A compromised agent can read secrets (API keys, OAuth tokens) from other agents in the same process.
  • Privilege escalation: An agent with limited permissions can bypass checks by manipulating shared state (global variables, prototype chains).
  • Blast radius: A crash in one agent (uncaught exception, segfault in a native module) terminates all agents in the process.

NanoClaw eliminates shared memory by running each agent in its own Docker container. Agents communicate with the host process (the “orchestrator”) via HTTP or WebSocket, not shared memory. If an agent is compromised, it cannot access other agents’ memory or file systems.

Container Isolation Architecture

NanoClaw uses Docker’s default isolation model: each container has its own filesystem, network namespace, and process tree. The orchestrator runs on the host, agents run in containers. Communication flows through a single HTTP API exposed by the orchestrator.

┌─────────────────────────────────────────────────────────┐
│ Host Machine                                            │
│                                                         │
│  ┌──────────────────────────────────────────────────┐  │
│  │ NanoClaw Orchestrator (Node.js)                  │  │
│  │ - Message routing                                │  │
│  │ - Channel adapters (Telegram, Slack, WhatsApp)   │  │
│  │ - Agent lifecycle management                     │  │
│  │ - Credential storage (OneCLI integration)        │  │
│  └────────┬─────────────────────────────────────────┘  │
│           │ HTTP/WebSocket                             │
│           │                                            │
│  ┌────────▼────────┐  ┌─────────────┐  ┌────────────┐ │
│  │ Agent Container │  │   Agent     │  │   Agent    │ │
│  │ (Docker)        │  │ Container   │  │ Container  │ │
│  │ - Claude SDK    │  │             │  │            │ │
│  │ - Task executor │  │             │  │            │ │
│  │ - Memory store  │  │             │  │            │ │
│  └─────────────────┘  └─────────────┘  └────────────┘ │
│                                                         │
└─────────────────────────────────────────────────────────┘

The orchestrator handles all external I/O (message platform APIs, credential retrieval). Agents receive tasks via HTTP POST, execute them using the Anthropic SDK, and return results. Agents do not have direct network access to message platforms or credential stores.

Container startup script (simplified):

// orchestrator/src/agent-manager.ts
import Docker from 'dockerode';

class AgentManager {
  private docker = new Docker();
  
  async launchAgent(agentId: string, config: AgentConfig): Promise<string> {
    const container = await this.docker.createContainer({
      Image: 'nanoclaw-agent:latest',
      Env: [
        `AGENT_ID=${agentId}`,
        `ORCHESTRATOR_URL=http://host.docker.internal:3000`,
        `ANTHROPIC_API_KEY=${config.anthropicKey}`
      ],
      HostConfig: {
        NetworkMode: 'bridge',
        Memory: 512 * 1024 * 1024, // 512 MB limit
        CpuQuota: 50000, // 50% of one core
        ReadonlyRootfs: true,
        Tmpfs: { '/tmp': 'rw,noexec,nosuid,size=100m' }
      }
    });
    
    await container.start();
    return container.id;
  }
}

The ReadonlyRootfs flag prevents agents from modifying their container filesystem (except /tmp). This limits persistence of malicious code across restarts. The memory and CPU limits prevent resource exhaustion attacks.

Message Platform Integration

NanoClaw supports six messaging platforms: Telegram, Discord, Slack, WhatsApp, Gmail, and a local CLI. Each platform has an adapter that translates platform-specific message formats into a unified Message type.

Adapter interface:

// orchestrator/src/adapters/base.ts
interface MessageAdapter {
  platform: 'telegram' | 'discord' | 'slack' | 'whatsapp' | 'gmail' | 'cli';
  
  // Called when a message arrives from the platform
  onMessage(callback: (msg: Message) => Promise<void>): void;
  
  // Send a reply back to the platform
  sendReply(channelId: string, text: string, metadata?: any): Promise<void>;
  
  // Platform-specific setup (OAuth, webhook registration, etc.)
  initialize(credentials: PlatformCredentials): Promise<void>;
}

interface Message {
  id: string;
  channelId: string;
  senderId: string;
  text: string;
  timestamp: Date;
  attachments?: Attachment[];
}

The orchestrator routes incoming messages to the appropriate agent based on channel configuration. Each channel is paired with one agent. If a message arrives on an unpaired channel, the orchestrator rejects it.

Example Telegram adapter (simplified):

// orchestrator/src/adapters/telegram.ts
import TelegramBot from 'node-telegram-bot-api';

class TelegramAdapter implements MessageAdapter {
  platform = 'telegram' as const;
  private bot?: TelegramBot;
  
  async initialize(credentials: PlatformCredentials) {
    this.bot = new TelegramBot(credentials.telegramToken, { polling: true });
  }
  
  onMessage(callback: (msg: Message) => Promise<void>) {
    this.bot?.on('message', async (tgMsg) => {
      const msg: Message = {
        id: String(tgMsg.message_id),
        channelId: String(tgMsg.chat.id),
        senderId: String(tgMsg.from?.id),
        text: tgMsg.text || '',
        timestamp: new Date(tgMsg.date * 1000)
      };
      await callback(msg);
    });
  }
  
  async sendReply(channelId: string, text: string) {
    await this.bot?.sendMessage(channelId, text);
  }
}

The adapter does not perform authentication or authorization. The orchestrator checks the channel pairing table before routing messages. If the channel is not paired, the message is dropped.

Credential Management

NanoClaw integrates with OneCLI for credential storage. OneCLI is a CLI tool that stores API keys in the system keychain (macOS Keychain, Windows Credential Manager, or Linux Secret Service). The orchestrator retrieves credentials at startup and injects them into agent containers via environment variables.

Credential flow:

  1. User runs nanoclaw.sh setup script.
  2. Script prompts for Anthropic API key and message platform credentials.
  3. Script stores credentials in OneCLI: onecli set anthropic.api_key <key>.
  4. Orchestrator reads credentials at startup: onecli get anthropic.api_key.
  5. Orchestrator injects credentials into agent containers as environment variables.

Agents do not have access to OneCLI or the system keychain. They receive only the credentials they need (Anthropic API key). If an agent is compromised, the attacker cannot retrieve credentials for other platforms.

Security boundary:

ComponentCredential Access
OrchestratorFull access to all credentials via OneCLI
Agent containerOnly Anthropic API key (via env var)
Message platform adapterOnly platform-specific token (via env var)
Host filesystemCredentials stored in system keychain (encrypted at rest)

The orchestrator is the trust boundary. If the orchestrator is compromised, all credentials are exposed. NanoClaw does not implement privilege separation within the orchestrator (e.g., running adapters in separate processes with restricted permissions).

State Management and Memory

Each agent has its own in-memory state store. The store is a simple key-value map persisted to disk (JSON file in the container’s /tmp directory). State is lost when the container restarts unless the orchestrator mounts a persistent volume.

State store interface:

// agent/src/memory.ts
class AgentMemory {
  private store = new Map<string, any>();
  private persistPath = '/tmp/agent-memory.json';
  
  async load() {
    if (fs.existsSync(this.persistPath)) {
      const data = JSON.parse(fs.readFileSync(this.persistPath, 'utf8'));
      this.store = new Map(Object.entries(data));
    }
  }
  
  async save() {
    const data = Object.fromEntries(this.store);
    fs.writeFileSync(this.persistPath, JSON.stringify(data, null, 2));
  }
  
  get(key: string): any {
    return this.store.get(key);
  }
  
  set(key: string, value: any) {
    this.store.set(key, value);
    this.save(); // Persist immediately
  }
}

The orchestrator can mount a Docker volume to persist state across restarts:

HostConfig: {
  Binds: [`/var/nanoclaw/agents/${agentId}:/tmp:rw`]
}

This maps the container’s /tmp directory to a host directory. State survives container restarts but is still accessible to the host. If the host is compromised, the attacker can read agent state.

Scheduled Jobs

NanoClaw supports cron-style scheduled jobs. The orchestrator maintains a job queue and triggers agents at specified intervals. Jobs are defined in a configuration file:

# config/jobs.yml
jobs:
  - name: daily-report
    agent: agent-1
    schedule: "0 9 * * *"  # 9 AM daily
    task: "Generate a summary of yesterday's messages"
  
  - name: hourly-check
    agent: agent-2
    schedule: "0 * * * *"  # Every hour
    task: "Check for new emails and reply to urgent ones"

The orchestrator uses node-cron to parse schedules and trigger jobs. When a job fires, the orchestrator sends an HTTP POST to the agent container with the task description. The agent executes the task and returns the result.

Job execution is not transactional. If the orchestrator crashes mid-job, the job is lost. If the agent crashes mid-execution, the orchestrator does not retry. For production use, you would need to add job persistence (e.g., write jobs to a database before execution) and retry logic.

Observability

NanoClaw logs all events to stdout in JSON format. Logs include:

  • Message routing (platform, channel, agent)
  • Agent lifecycle (container start, stop, crash)
  • Task execution (start time, duration, success/failure)
  • Credential access (which credentials were retrieved, when)

Example log entry:

{
  "timestamp": "2026-05-19T00:15:32.123Z",
  "level": "info",
  "event": "task_executed",
  "agentId": "agent-1",
  "taskId": "task-456",
  "duration": 2.3,
  "success": true
}

The orchestrator does not implement distributed tracing or metrics collection. If you need observability in a multi-agent deployment, you must export logs to an external system (e.g., Elasticsearch, Datadog) and build dashboards yourself.

Failure Modes

Container crashes: If an agent container crashes (OOM, segfault, unhandled exception), the orchestrator detects it via Docker’s event stream and restarts the container. In-flight tasks are lost. The orchestrator does not retry tasks automatically.

Orchestrator crashes: If the orchestrator crashes, all message routing stops. Agents continue running but cannot receive new tasks. When the orchestrator restarts, it reconnects to existing containers and resumes routing. Messages received while the orchestrator was down are lost (message platforms do not queue messages for offline clients).

Network partition: If the orchestrator loses network connectivity to a message platform, it cannot receive or send messages on that platform. Other platforms continue working. The orchestrator does not implement exponential backoff or circuit breakers for platform APIs. If a platform rate-limits the orchestrator, requests fail immediately.

Credential expiration: If a message platform token expires (e.g., OAuth refresh token), the adapter fails to authenticate. The orchestrator logs the error but does not attempt to refresh the token. You must manually update the token in OneCLI and restart the orchestrator.

Disk full: If the host disk fills up, agent containers cannot write state to /tmp. The orchestrator does not monitor disk usage. Agents crash with write errors.

Deployment Patterns

Single-agent development: Run the orchestrator and one agent container on a local machine. Use the CLI adapter for testing. This is the default setup created by nanoclaw.sh.

Multi-agent production: Run the orchestrator on a server (EC2, DigitalOcean Droplet) with multiple agent containers. Use Telegram or Slack adapters for user interaction. Mount persistent volumes for agent state. Run the orchestrator under systemd or Docker Compose for auto-restart.

Serverless agents: Run the orchestrator as a long-lived process (e.g., on a VPS) but launch agent containers on-demand using AWS Fargate or Google Cloud Run. This reduces idle resource usage but increases task latency (cold start time: 5-10 seconds).

Comparison to OpenClaw

DimensionNanoClawOpenClaw
Lines of code~2,000~500,000
Dependencies~10 npm packages70+ npm packages
Isolation modelDocker containers (OS-level)Application-level (allowlists, pairing codes)
Memory safetySeparate address spacesShared heap
Credential storageOneCLI (system keychain)In-process (environment variables)
State persistencePer-agent JSON filesCentralized database
ObservabilityJSON logs to stdoutBuilt-in dashboard, metrics
ExtensibilityModify source codePlugin system