Live Code Walkthrough

Inside NanoClaw

The entire AI agent that's taking over the tech world — broken down file by file. ~700 lines of core orchestration. Built in one weekend. 10,500+ GitHub stars. Let's read it together and map every piece to the Agent Formula.

8 source files
~700 lines in main orchestrator
9 dependencies
1 process
8 min to read entire codebase

What You're Looking At

This is every file that matters. Not a simplified version — this IS the architecture. Compare this to OpenClaw's 400,000 lines across 52+ modules.

nanoclaw/ ├── src/ # The host process — everything runs here │ ├── index.ts # 🧠 THE BRAIN — orchestrator, message loop, agent invocation (700 lines) │ ├── container-runner.ts # 🔒 Spawns isolated containers for each agent │ ├── group-queue.ts # 🔄 Per-group message queue with concurrency control │ ├── task-scheduler.ts # ⏰ Runs scheduled/recurring tasks │ ├── db.ts # 💾 SQLite operations (messages, groups, sessions) │ ├── router.ts # 📤 Message formatting and outbound routing │ ├── ipc.ts # 📡 Inter-process communication watcher │ └── config.ts # ⚙️ Trigger pattern, paths, intervals (~50 lines) ├── container/ # What runs INSIDE each agent's sandbox │ ├── agent-runner.js # 🤖 Claude Agent SDK executor │ └── ipc-mcp.ts # 🛠️ MCP server: send_message, schedule_task, register_group ├── groups/{name}/ # Per-group workspaces (each group is isolated) │ └── CLAUDE.md # 💾 MEMORY — each group's persistent context ├── data/ │ ├── messages.db # SQLite database │ ├── sessions/{name}/ # Agent SDK conversation state │ └── ipc/{name}/ # JSON files for tool communication └── .claude/skills/ # Claude Code skills (/setup, /customize, /debug)

Reading the Agent, File by File

Click each tab to see the real code patterns from NanoClaw. Every highlighted line maps directly to the Agent Formula you learned today.

index.ts LOOP
container-runner.ts SECURITY
ipc-mcp.ts TOOLS
db.ts MEMORY
CLAUDE.md BRAIN

🧠 src/index.ts — The Orchestrator

This single file is the heart of NanoClaw. At ~700 lines, it handles everything: connecting to WhatsApp, polling for new messages, deciding when to invoke the AI agent, spawning containers, and routing responses back. This is the REASONING LOOP of the entire system.

1 // src/index.ts — NanoClaw Orchestrator
2 // This is where EVERYTHING happens. One file. One process.
3
4 import { makeWASocket } from '@whiskeysockets/baileys'
5 import { runAgent } from './container-runner'
6 import { GroupQueue } from './group-queue'
7 import * as db from './db'
8 import { TRIGGER_PATTERN, MESSAGE_POLLING_INTERVAL } from './config'
9
10 // ─── THE MAIN FUNCTION ───────────────────────────────────
11
12 async function main() {
13 // 1. Connect to WhatsApp
14 const sock = makeWASocket({ auth: state })
15
16 // 2. Start the message loop (polls every 2 seconds)
17 startMessageLoop(sock, queue)
18
19 // 3. Start watching for tool responses from containers
20 startIpcWatcher()
21
22 // 4. Start the scheduled task runner
23 startTaskScheduler()
24 }
25
26 // ─── THE MESSAGE LOOP ─────────────────────────────────────
27 // This is the AGENT LOOP — the same pattern from your Colab notebook!
28
29 function startMessageLoop(sock, queue) {
30 setInterval(async () => {
31
32 // Step 1: Poll for new messages since last check
33 const messages = db.getNewMessages(lastTimestamp)
34
35 for (const msg of messages) {
36
37 // Step 2: Does this message trigger the agent?
38 if (!TRIGGER_PATTERN.test(msg.text)) continue
39
40 // Step 3: Show "typing..." indicator on WhatsApp
41 sock.sendPresenceUpdate('composing', msg.chatJid)
42
43 // Step 4: Load conversation history for context
44 const history = db.getMessagesSince(chatJid, lastAgentTs)
45
46 // Step 5: SPAWN THE AGENT in an isolated container
47 const response = await runAgent(groupFolder, prompt, chatJid)
48
49 // Step 6: Strip internal reasoning, send response
50 const text = raw.replace(/<internal>[\s\S]*?<\/internal>/g, '')
51 sock.sendMessage(chatJid, { text })
52 }
53 }, MESSAGE_POLLING_INTERVAL) // 2000ms
54 }
This is it. The entire agent loop fits on one screen. Poll for messages → check trigger → load history → spawn Claude in a container → send response. This is the same pattern as your Colab notebook's run_agent() function, but connected to WhatsApp instead of a Python prompt.

🔒 src/container-runner.ts — The Security Layer

This is why NanoClaw exists. Instead of giving the AI access to your whole computer (like OpenClaw), each agent runs in its own isolated Linux container. It can only see what you explicitly mount. This file handles spawning those containers and streaming the AI's output back.

1 // src/container-runner.ts — Spawn isolated agent containers
2
3 export async function runAgent(
4 groupFolder: string, // Which group's workspace to mount
5 prompt: string, // The user's message + conversation history
6 chatJid: string, // WhatsApp chat ID (for routing responses)
7 ): Promise<string> {
8
9 // Validate that all mounts are in the allowlist
10 validateAdditionalMounts(containerConfig.additionalMounts)
11
12 // Build the container with ONLY this group's files visible
13 const mounts = [
14 `groups/${groupFolder}:/workspace`, // Group's workspace → /workspace
15 `data/sessions/${groupFolder}:/.claude`,// Session state → /.claude
16 `data/ipc/${groupFolder}:/ipc`, // IPC channel → /ipc
17 ]
18
19 // Spawn the container and stream Claude's output
20 const proc = spawn('container', ['run', ...mounts, CONTAINER_IMAGE])
21
22 // Stream the agent's response back in real time
23 let output = ''
24 proc.stdout.on('data', (chunk) => { output += chunk })
25 return output
26 }
This is the key security difference. OpenClaw runs everything in one Node process — the AI can access everything on your computer. NanoClaw gives each group its own Linux container. It's like giving each AI its own locked room instead of the keys to your house. The validateAdditionalMounts() function checks against a mount allowlist — you explicitly approve what each agent can see.

🛠️ container/ipc-mcp.ts — The Agent's Tools

Remember the TOOLS part of the Agent Formula? This file defines what the agent can do. Each tool is an MCP server function that the agent calls when it needs to take action in the world. Same concept as the calculator tool from your Colab notebook — but these tools send messages, schedule tasks, and manage groups.

1 // container/ipc-mcp.ts — Tools available to the agent
2 // These are like your Colab tools, but for real-world actions
3
4 // Tool 1: Send a message to any WhatsApp chat
5 server.tool('send_message', {
6 description: 'Send a message to a WhatsApp chat',
7 input: { chatJid: string, text: string }
8 })
9
10 // Tool 2: Schedule a recurring task
11 server.tool('schedule_task', {
12 description: 'Schedule a task to run at a specific time or interval',
13 input: { name: string, schedule: string, prompt: string }
14 })
15
16 // Tool 3: Register a new WhatsApp group (main channel only)
17 server.tool('register_group', {
18 description: 'Register a WhatsApp group for the agent to monitor',
19 input: { jid: string, name: string, folder: string }
20 })
21
22 // Authorization check: non-main groups can only message their own chat
23 if (!isMainGroup && targetJid !== ownJid) throw 'Unauthorized'
Same pattern as your Colab. In your notebook, you defined a calculator tool with a name, description, and input schema. NanoClaw does the exact same thing — but with send_message instead of calculator. When Claude decides "I need to send a message," it calls this tool. The authorization check on line 23 prevents non-main groups from messaging other chats — a real-world security decision.

💾 src/db.ts — The Memory Layer

The MEMORY component. This is how NanoClaw remembers conversations, stores scheduled tasks, and tracks which messages have been processed. Without this, the agent would have no context — every message would be a blank slate.

1 // src/db.ts — SQLite database operations
2 // This is the agent's long-term memory
3
4 // Tables created on first run:
5 // chats — WhatsApp chat metadata
6 // messages — Every message sent and received
7 // scheduled_tasks — Recurring jobs with cron/interval/once schedules
8 // task_run_logs — Execution history for each task run
9
10 // Key functions the orchestrator uses:
11
12 export function getNewMessages(since: number): Message[]
13 // → Called every 2 seconds by the message loop
14
15 export function getMessagesSince(jid: string, ts: number): Message[]
16 // → Gets conversation history to give Claude context
17
18 export function getDueTasks(): ScheduledTask[]
19 // → Called every 60 seconds by the task scheduler
20
21 export function storeMessage(msg: Message): void
22 // → Every incoming AND outgoing message gets saved
Memory is what separates agents from chatbots. Without getMessagesSince(), Claude would have no idea what you said 5 minutes ago. Without getDueTasks(), scheduled jobs wouldn't exist. All of it lives in one SQLite file: data/messages.db.

🧠 groups/{name}/CLAUDE.md — Per-Group Memory

This is one of NanoClaw's most elegant ideas. Each group has its own CLAUDE.md file — a Markdown document that serves as the agent's BRAIN instructions for that specific context. The agent reads this before every response, and can modify it to "remember" things.

1 # groups/family-chat/CLAUDE.md
2 # This file IS the agent's personality + memory for this group
3
4 # Family Chat Assistant
5
6 You are Andy, a helpful family assistant.
7 Keep responses warm and brief.
8 Mom's birthday is March 15th.
9 Dad prefers no emojis in responses.
10
11 ## Things I've Learned
12 - Grocery day is Sunday
13 - Sarah has a peanut allergy
14 - The WiFi password is on the fridge
Markdown as the interface between humans and AI. This is the most radical idea in NanoClaw. Memory isn't stored in a complex database schema — it's a Markdown file that both humans and Claude can read and edit. Each group gets its own, so your family agent has different memories than your work agent. This is what the industry calls "AI-native" software.

NanoClaw ↔ Agent Formula

Every piece of NanoClaw maps to the formula you learned today. Here's the exact mapping.

🧠

LLM (The Brain)

container/agent-runner.js → Claude Agent SDK

NanoClaw uses Claude Code running inside each container. The Agent SDK handles all the thinking. NanoClaw never processes language itself — it just routes messages to Claude and routes responses back.

🛠️

Tools

container/ipc-mcp.ts → send_message, schedule_task, register_group

Same pattern as your Colab calculator tool. Each tool has a name, description, and input schema. Claude decides when to use them. The IPC system communicates tool results back to the host.

🔄

Reasoning Loop

src/index.ts → startMessageLoop() — polls every 2 seconds

The message loop polls → detects trigger → loads context → spawns agent → sends response. Same pattern as your run_agent() while loop. NanoClaw adds a concurrency queue so multiple groups don't step on each other.

💾

Memory

src/db.ts + groups/{name}/CLAUDE.md

Two memory systems: SQLite for structured data (messages, tasks, state), and Markdown files for unstructured knowledge ("Mom's birthday is March 15"). The agent can read AND write both.

💡 The Business Insight

NanoClaw's entire core is ~700 lines. Your Colab agent loop was ~50 lines. The difference is infrastructure (containers, WhatsApp, databases) — not intelligence. The agent pattern is the same.

The most valuable decisions in NanoClaw aren't technical. They're: "What tools should the agent have?" (business question), "What should each group's CLAUDE.md say?" (product question), and "What gets mounted in each container?" (trust question).

One developer built this in a weekend. With Claude Code. The same tools you used today. The only difference is he identified a real problem (security), had a clear 10× advantage (auditable code), and shipped fast. You can do this.