Logo
Published on

NanoClaw 아키텍처 분석 보고서 / 실사용 시나리오 Q&A

Authors

분석 일자: 2026-03-13 대상 버전: v1.2.12 저장소: https://github.com/qwibitai/nanoclaw


This article is mostly written by Claude Code

목차

  1. 프로젝트 개요
  2. 기술 스택
  3. 전체 아키텍처
  4. 핵심 모듈 구조
  5. 메시지 처리 파이프라인
  6. 오케스트레이터 상세
  7. 컨테이너 에이전트 시스템
  8. 채널 시스템
  9. 스케줄러 & IPC 시스템
  10. 보안 아키텍처
  11. 핵심 데이터 구조
  12. 레이어별 의존 관계
  13. OpenClaw와의 차별점
  14. 디렉토리 트리
  15. 핵심 개념 설명
  16. Q&A: 실제 사용 시나리오

1. 프로젝트 개요

NanoClaw는 TypeScript 기반의 Personal Claude Assistant로, 사용자의 기기에서 직접 실행되는 경량 로컬 AI 비서입니다.

  • 슬로건: "Small enough to understand. Secure enough to trust."
  • 핵심 가치: 단순성, 컨테이너 기반 보안, 로컬 퍼스트, 코드로 커스터마이징
  • 설계 철학: OpenClaw(500k+ LOC)와 달리 이해하고 수정할 수 있는 작은 코드베이스 (~7,300 LOC)
  • 지원 채널: WhatsApp, Telegram, Slack, Discord, Gmail (스킬 방식으로 확장)
  • 지원 플랫폼: macOS (launchd), Linux (systemd)

NanoClaw의 가장 큰 특징은 코드가 설정이라는 철학입니다. 별도의 설정 파일 없이, 스킬(Git 브랜치)을 머지하여 기능을 추가하고, 직접 코드를 수정하여 동작을 변경합니다. 프레임워크 추상화 없이 필요한 최소한의 코드만 존재합니다.


2. 기술 스택

영역기술
언어TypeScript (ESM)
런타임Node.js 20+
패키지 관리npm
빌드tsc (TypeScript 컴파일러)
테스트Vitest
린터/포매터Prettier
Git HookHusky
데이터베이스SQLite (better-sqlite3, 동기)
AI 런타임@anthropic-ai/claude-code (Agent SDK)
스케줄러cron-parser
로거pino
스키마 검증zod
컨테이너Docker / Apple Container
서비스 관리macOS launchd / Linux systemd

채널별 SDK (스킬로 설치)

채널라이브러리
WhatsApp@whiskeysockets/baileys
Telegramnode-telegram-bot-api
Slack@slack/bolt
Discorddiscord.js
Gmailgoogleapis

의존성 특징

NanoClaw 코어는 단 5개의 런타임 의존성만 사용합니다: better-sqlite3, cron-parser, pino, yaml, zod. 채널 라이브러리는 스킬 머지 시에만 추가됩니다.


3. 전체 아키텍처

╔══════════════════════════════════════════════════════════════════════════╗
NanoClaw System║                                                                          ║
║  ┌─────────────────────────────────────────────────────────────────┐    ║
║  │              Channels (Self-Registering Skills)                  │    ║
║  │  WhatsAppTelegramSlackDiscordGmail  (custom)        │    ║
║  │           └─────────────────┬───────────────────                 │    ║
║  │                  registerChannel() at startup                    │    ║
║  └───────────────────────────┬─────────────────────────────────────┘    ║
║                               │ onMessage(jid, NewMessage)║                               ▼                                          ║
║  ┌────────────────────────────────────────────────────────────────────┐ ║
║  │                     SQLite Database                                │ ║
║  │  chats │ messages │ scheduled_tasks │ sessions │ registered_groups │ ║
║  └───────────────────────────┬────────────────────────────────────────┘ ║
║                               │ poll every 2s                            ║
║                               ▼                                          ║
║  ┌────────────────────────────────────────────────────────────────────┐ ║
║  │              Orchestrator (src/index.ts)                           │ ║
║  │                                                                    │ ║
║  │  Message Loop ──┬── Trigger Check (@Andy)                         │ ║
║  │                 ├── Group Queue (per-group concurrency)            │ ║
║  │                 └── runAgent()                                     │ ║
║  │                                                                    │ ║
║  │  IPC Watcher ───── File-based IPC (poll every 1s)                 │ ║
║  │  Scheduler ─────── Cron/Interval/Once tasks (poll every 60s)      │ ║
║  │  Credential ProxyHTTP proxy on port 3001                       │ ║
║  └───────────────────────────┬────────────────────────────────────────┘ ║
║                               │ spawn container                          ║
║                               ▼                                          ║
║  ┌────────────────────────────────────────────────────────────────────┐ ║
║  │              Container Agent (Docker / Apple Container)            │ ║
║  │                                                                    │ ║
║  │  entrypoint.sh → agent-runner/index.ts                            │ ║
║  │       ├── Claude Agent SDK (streaming)                            │ ║
║  │       ├── Tools: Bash, File I/O, Web, Browser                     │ ║
║  │       └── MCP: ipc-mcp-stdio.ts (scheduler tools)                 │ ║
║  └───────────────────────────┬────────────────────────────────────────┘ ║
║                               │ stdout (JSON markers)║                               ▼                                          ║
║  ┌──────────────────────────────────────────────────────────────────┐   ║
║  │              Response Routing (src/router.ts)                    │   ║
║  │  Strip <internal> tags → findChannel()sendMessage()          │   ║
║  └──────────────────────────────────────────────────────────────────┘   ║
╚══════════════════════════════════════════════════════════════════════════╝

4. 핵심 모듈 구조

모듈역할핵심 파일
Orchestrator메시지 루프, 에이전트 호출src/index.ts
Channel Registry채널 자기 등록src/channels/registry.ts
Router메시지 포맷 & 아웃바운드 라우팅src/router.ts
Config환경변수 기반 설정src/config.ts
Container Runner컨테이너 스폰 & 마운트 관리src/container-runner.ts
Group Queue그룹별 큐 & 동시성 제어src/group-queue.ts
IPC Watcher파일 기반 IPC 처리src/ipc.ts
Task Scheduler예약 태스크 실행src/task-scheduler.ts
DatabaseSQLite CRUD (동기)src/db.ts
Credential Proxy자격증명 격리 HTTP 프록시src/credential-proxy.ts
Mount Security마운트 경로 검증src/mount-security.ts
Agent Runner컨테이너 내부 에이전트 실행container/agent-runner/src/index.ts
IPC MCP컨테이너 → 호스트 MCP 도구container/agent-runner/src/ipc-mcp-stdio.ts

5. 메시지 처리 파이프라인

[사용자 WhatsApp 메시지 수신]
WhatsApp Channel (Baileys)
  └── onMessage(jid, NewMessage) 콜백 호출
src/db.tsstoreMessage()
  └── SQLite messages 테이블에 저장
src/index.tsstartMessageLoop() [2초 폴링]
  └── getNewMessages() — 미처리 메시지 조회
        ├── [-Main 그룹] 트리거 패턴 확인 (@Andy)
        │     ├── 트리거 없음 → DB에서 대기
        │     └── 트리거 있음 → 계속 진행
processGroupMessages()
  └── 해당 그룹의 미처리 메시지 수집 & XML 포맷
src/group-queue.tsenqueueMessageCheck()
  └── 동시 실행 그룹 수 확인 (MAX_CONCURRENT_CONTAINERS: 5)
src/container-runner.tsrunContainerAgent()
  └── 볼륨 마운트 구성 (그룹 폴더, 세션 디렉토리 등)
      └── docker run / container run 실행
  container/agent-runner/src/index.ts
    └── Claude Agent SDK (스트리밍 실행)
          ├── Tool 실행 (Bash, 파일,, 브라우저)
          └── MCP 도구 (schedule_task, send_message 등)
                │ stdout JSON (sentinel markers)
src/index.ts → 결과 수신
  └── <internal> 태그 제거 → router.ts → 채널 응답
WhatsApp ChannelsendMessage() → 사용자 응답 전송

그룹 큐 상태 전이

상태설명
idle활성 컨테이너 없음, 대기 중
running컨테이너 실행 중 (메시지/태스크 처리)
idle-waiting컨테이너 종료 대기, 파이프 메시지 수신 가능
pending메시지/태스크 큐에 대기 중

6. 오케스트레이터 상세

초기화 시퀀스 (src/index.ts)

1. initDatabase()SQLite 초기화 & 마이그레이션
2. startCredentialProxy() — 자격증명 프록시 시작 (포트 3001)
3. 채널 팩토리 로드 — registerChannel()로 자기 등록된 채널 초기화
4. getRegisteredGroups()DB에서 등록된 그룹 목록 조회
5. startIpcWatcher() — 파일 IPC 감시 시작
6. startSchedulerLoop() — 예약 태스크 루프 시작
7. 미처리 메시지 복구 확인 (크래시 복구)
8. startMessageLoop() — 메인 메시지 폴링 루프 시작

트리거 로직

// Main 그룹: 트리거 불필요, 모든 메시지 처리
// 비-Main 그룹: @Andy 패턴 확인 후 처리
const TRIGGER_PATTERN = new RegExp(`@${ASSISTANT_NAME}`, 'i')

if (!group.isMain && !messages.some(m => TRIGGER_PATTERN.test(m.content))) {
  // 메시지를 DB에 보관, 트리거 대기
  return
}

주요 상태

// 마지막 처리 타임스탬프 (그룹별)
lastAgentTimestamp: Record<groupFolder, timestamp>

// 그룹 세션 ID (컨테이너 연속성)
sessions: Map<groupFolder, sessionId>

// 등록된 그룹 정보
registeredGroups: Map<jid, RegisteredGroup>

7. 컨테이너 에이전트 시스템

컨테이너 실행 구조

호스트 프로세스
  └── docker run / container run
        ├── stdin  ← ContainerInput JSON
{ messages, group, sessionId, isScheduledTask, ... }
        └── stdout → ContainerOutput JSON (sentinel markers)
                     { result, sessionId, ... }

볼륨 마운트 정책

마운트소스컨테이너 경로권한
그룹 폴더groups/{name}//workspace/group읽기/쓰기
글로벌 메모리groups/global//workspace/global읽기 전용
프로젝트 루트nanoclaw//workspace/project읽기 전용 (Main만)
Claude 세션data/sessions/{group}/.claude//home/node/.claude/읽기/쓰기
IPC 폴더data/ipc/{group}//workspace/ipc/읽기/쓰기
스킬 폴더container/skills/(세션 sync)
추가 마운트허용 목록 기반사용자 지정사용자 지정

Dockerfile 구조

FROM node:22-slim

# Chromium 및 브라우저 자동화 라이브러리 설치
RUN apt-get install -y chromium ...

# 글로벌 패키지
RUN npm install -g agent-browser @anthropic-ai/claude-code

# 에이전트 러너 컴파일
COPY container/agent-runner/ /app/agent-runner/
RUN tsc ...

# 비-루트 사용자로 실행
USER node
ENTRYPOINT ["/app/entrypoint.sh"]

에이전트 입출력 프로토콜

// stdin (호스트 → 컨테이너)
interface ContainerInput {
  messages: string          // XML 포맷 메시지
  group: RegisteredGroup    // 그룹 메타데이터
  sessionId?: string        // 세션 연속성
  isScheduledTask?: boolean // 예약 태스크 여부
  taskPrompt?: string       // 태스크 프롬프트
}

// stdout (컨테이너 → 호스트, sentinel markers로 감싸짐)
interface ContainerOutput {
  result: string            // 에이전트 응답
  sessionId: string         // 새 세션 ID
  error?: string
}

// 마커
const START = '---NANOCLAW_OUTPUT_START---'
const END   = '---NANOCLAW_OUTPUT_END---'

팔로우업 메시지 (IPC Input)

활성 컨테이너 실행 중 새 메시지 도착 시:

호스트 → /workspace/ipc/input/{timestamp}.json 파일 생성
컨테이너 (500ms 폴링) → 파일 감지 → MessageStream에 추가
에이전트 → 팔로업 메시지 처리 → 응답
(컨테이너 유지, 아이들 타임아웃 30)

8. 채널 시스템

채널 자기 등록

NanoClaw의 채널은 별도의 Git 브랜치(스킬)로 존재하며, 머지 시 src/channels/ 디렉토리에 파일을 추가하고 스타트업에 자기 등록합니다.

// src/channels/registry.ts
registerChannel('whatsapp', async (config) => {
  if (!process.env.WHATSAPP_ENABLED) return null
  return new WhatsAppChannel(config)
})

// 오케스트레이터에서 조회
const factory = getChannelFactory('whatsapp')
const channel = await factory?.(config)

Channel 인터페이스

interface Channel {
  name: string
  connect(): Promise<void>
  disconnect(): Promise<void>
  sendMessage(jid: string, text: string): Promise<void>
  isConnected(): boolean
  ownsJid(jid: string): boolean
  setTyping?(jid: string, typing: boolean): Promise<void>
  syncGroups?(): Promise<void>
}

채널이 없을 때 (graceful skip)

// 자격증명 없으면 null 반환 → 오케스트레이터가 스킵
const factory = getChannelFactory(name)
if (!factory) return  // 채널 미설치
const channel = await factory(config)
if (!channel) return  // 자격증명 없음

멀티채널 라우팅

// 응답을 원본 채널로 라우팅
function findChannel(jid: string): Channel | undefined {
  return channels.find(ch => ch.ownsJid(jid))
}

9. 스케줄러 & IPC 시스템

스케줄러 (src/task-scheduler.ts)

startSchedulerLoop() [60초 폴링]
  └── getDueTasks() — next_run <= now AND status='active'
        ├── [cron] cron-parser로 다음 실행 시간 계산
        ├── [interval] 마지막 실행 + 간격 (드리프트 방지)
        └── [once] 단일 실행 후 completed로 변경
        group-queue.tsenqueueTask()
        runContainerAgent() (isolated 또는 group 컨텍스트)
        결과 → send_message IPC 도구 → 채널 응답
        로그 → task_run_logs 테이블

IPC 시스템 (src/ipc.ts)

파일 기반 단방향 IPC. 컨테이너가 JSON 파일을 작성하면 호스트가 읽고 삭제합니다.

컨테이너  (MCP 도구)
  ipc-mcp-stdio.ts
    └── /workspace/ipc/{group}/messages/{id}.json 파일 생성
          { type: 'schedule_task', payload: {...} }

호스트  (1초 폴링)
  src/ipc.tsstartIpcWatcher()
    └── 파일 감지 → 내용 파싱 → 명령 실행 → 파일 삭제

지원 IPC 명령

명령설명권한
schedule_task새 태스크 예약자신의 그룹만
pause_task태스크 일시 정지자신의 그룹만
resume_task태스크 재개자신의 그룹만
cancel_task태스크 취소자신의 그룹만
update_task태스크 수정자신의 그룹만
list_tasks태스크 목록 조회자신의 그룹만
send_message채널로 메시지 전송Main: 모든 JID, 기타: 자신의 JID만
register_group새 그룹 등록Main 그룹만
refresh_groups그룹 메타데이터 갱신Main 그룹만

10. 보안 아키텍처

다층 격리 모델

┌─────────────────────────────────────────────────────┐
│                   호스트 시스템                        │
│                                                     │
│  자격증명 프록시 ─────────── 실제 API  (컨테이너 불가)│  마운트 허용 목록 ─────────── .ssh, .aws 등 차단        │
│                                                     │
│  ┌─────────────────────────────────────────────┐   │
│  │              컨테이너 (격리 경계)              │   │
│  │                                             │   │
│  │  비-루트 사용자 (node, uid 1000)             │   │
│  │  /workspace/group 만 읽기/쓰기 가능          │   │
│  │  플레이스홀더 자격증명만 주입                 │   │
│  │                                             │   │
│  │  ┌─────────────────────────────────────┐   │   │
│  │  │         에이전트 실행 공간            │   │   │
│  │  │  그룹 메모리, 세션 격리              │   │   │
│  │  └─────────────────────────────────────┘   │   │
│  └─────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘

자격증명 프록시 (src/credential-proxy.ts)

컨테이너
  └── ANTHROPIC_API_KEY=placeholder
      ANTHROPIC_BASE_URL=http://host:3001
HTTP 요청
자격증명 프록시 (포트 3001)
  └── 실제 API 키 주입
HTTP 요청 (헤더 교체)
Anthropic API (api.anthropic.com)

두 가지 인증 모드:

  • api-key: x-api-key 헤더에 실제 키 주입
  • oauth: Bearer 토큰 교체 (OAuth 플로우)

마운트 보안 (src/mount-security.ts)

// ~/.config/nanoclaw/mount-allowlist.json (호스트만 접근, 컨테이너 마운트 불가)
{
  "allowedRoots": [
    { "path": "~/Documents", "label": "Documents" }
  ],
  "blockedPatterns": [
    ".ssh", ".gnupg", ".aws", ".env", "credentials",
    "id_rsa", "id_ed25519", "*.pem", "*.key"
  ],
  "nonMainReadOnly": true
}

IPC 권한 모델

Main 그룹 컨테이너
  ├── 모든 JID로 메시지 전송 가능
  ├── 모든 그룹의 태스크 관리 가능
  └── 새 그룹 등록 가능

-Main 그룹 컨테이너
  ├── 자신의 JID로만 메시지 전송 가능
  ├── 자신의 그룹 태스크만 관리 가능
  └── 그룹 등록 불가

11. 핵심 데이터 구조

SQLite 스키마

-- 그룹/채팅 메타데이터
CREATE TABLE chats (
  jid TEXT PRIMARY KEY,
  name TEXT,
  last_message_time INTEGER,
  channel TEXT,        -- 'whatsapp', 'telegram', 등
  is_group INTEGER
);

-- 메시지 이력
CREATE TABLE messages (
  id TEXT PRIMARY KEY,
  chat_jid TEXT,
  sender TEXT,
  timestamp INTEGER,
  content TEXT,
  is_bot_message INTEGER DEFAULT 0
);

-- 예약 태스크
CREATE TABLE scheduled_tasks (
  id INTEGER PRIMARY KEY,
  group_folder TEXT,
  chat_jid TEXT,
  prompt TEXT,
  schedule_type TEXT,   -- 'cron' | 'interval' | 'once'
  schedule_value TEXT,  -- cron 식 또는 ms 또는 ISO 날짜
  context_mode TEXT,    -- 'group' | 'isolated'
  next_run INTEGER,
  status TEXT           -- 'active' | 'paused' | 'completed'
);

-- 태스크 실행 로그
CREATE TABLE task_run_logs (
  id INTEGER PRIMARY KEY,
  task_id INTEGER,
  started_at INTEGER,
  duration_ms INTEGER,
  status TEXT,
  result TEXT,
  error TEXT
);

-- 라우터 상태 (JSON 키-값)
CREATE TABLE router_state (
  key TEXT PRIMARY KEY,
  value TEXT
  -- last_timestamp: 전역 마지막 처리 타임스탬프
  -- last_agent_timestamp: { groupFolder: timestamp } JSON
);

-- 그룹 세션 ID
CREATE TABLE sessions (
  group_folder TEXT PRIMARY KEY,
  session_id TEXT
);

-- 등록된 그룹
CREATE TABLE registered_groups (
  jid TEXT PRIMARY KEY,
  name TEXT,
  folder TEXT,
  trigger TEXT,
  is_main INTEGER,
  added_at INTEGER
);

핵심 타입

// 채널에서 호스트로 전달되는 메시지
interface NewMessage {
  id: string
  chat_jid: string      // "1234567890@s.whatsapp.net"
  sender: string
  sender_name: string
  content: string
  timestamp: number
  is_from_me?: boolean
  is_bot_message?: boolean
}

// DB에 저장된 그룹 메타데이터
interface RegisteredGroup {
  name: string
  folder: string        // "groups/family-chat"
  trigger: string       // "@Andy"
  added_at: number
  isMain?: boolean
  requiresTrigger?: boolean
  containerConfig?: ContainerConfig
}

// 예약 태스크
interface ScheduledTask {
  id: number
  group_folder: string
  chat_jid: string
  prompt: string
  schedule_type: 'cron' | 'interval' | 'once'
  schedule_value: string
  context_mode: 'group' | 'isolated'
  next_run: number
  status: 'active' | 'paused' | 'completed'
}

메시지 XML 포맷

컨테이너로 전달되는 메시지 포맷. JSON보다 토큰 효율이 높습니다.

<messages timezone="Asia/Seoul">
  <message id="abc123" sender="+82101234567" sender_name="Alice" timestamp="2026-03-13T10:00:00+09:00">
    <content>@Andy 오늘 날씨 어때?</content>
  </message>
  <message id="def456" sender="+82109876543" sender_name="Bob" timestamp="2026-03-13T10:01:00+09:00">
    <content>나도 궁금해</content>
  </message>
</messages>

12. 레이어별 의존 관계

┌──────────────────────────────────────────────────────────────┐
│                     채널 레이어                                │
WhatsApp / Telegram / Slack / Discord / Gmail  (자기 등록, Channel 인터페이스 구현)└──────────────────────────────┬───────────────────────────────┘
                               │ onMessage callback
┌──────────────────────────────▼───────────────────────────────┐
│                     데이터 레이어                              │
│  src/db.ts (SQLite, better-sqlite3, 동기)│  storeMessage / getNewMessages / getRegisteredGroups / ...└──────────────────────────────┬───────────────────────────────┘
DB 읽기/쓰기
┌──────────────────────────────▼───────────────────────────────┐
│                   오케스트레이션 레이어                         │
│  src/index.ts → src/group-queue.ts│  메시지 루프 / 트리거 체크 / 큐 관리 / 동시성 제어              │
└──────────────────────────────┬───────────────────────────────┘
                               │ spawn + stdin/stdout
┌──────────────────────────────▼───────────────────────────────┐
│                   컨테이너 레이어                              │
│  src/container-runner.ts│  볼륨 마운트 / 자격증명 프록시 주입 / 스킬 동기화               │
└──────────────────────────────┬───────────────────────────────┘
Claude Agent SDK
┌──────────────────────────────▼───────────────────────────────┐
│                   에이전트 레이어                              │
│  container/agent-runner/src/index.tsClaude Agent SDK / MCP 서버 / IPC 폴링                      │
└──────────────────────────────┬───────────────────────────────┘
HTTP (proxy)
┌──────────────────────────────▼───────────────────────────────┐
│                   외부 레이어                                  │
Anthropic API (api.anthropic.com)└──────────────────────────────────────────────────────────────┘

병렬 시스템:
  src/ipc.ts         ─── 파일 IPC 감시 (1초 폴링)
  src/task-scheduler.ts ── 예약 태스크 (60초 폴링)
  src/credential-proxy.tsHTTP 프록시 (포트 3001)

13. OpenClaw와의 차별점

항목NanoClawOpenClaw
코드 규모~7,300 LOC500,000+ LOC
의존성5개 (코어)수십 개
AI 런타임Claude Agent SDK (Anthropic 공식)Pi Agent (자체 구현)
LLM 지원Claude 전용20개+ LLM
격리 방식OS 컨테이너 (Docker/Apple)애플리케이션 레벨 (샌드박스 정책)
채널 방식Git 브랜치 스킬로 확장플러그인 npm 패키지
설정 방식환경변수 + 코드 수정config.yml (핫 리로드 지원)
통신 방식파일 IPC + stdin/stdoutWebSocket RPC
메모리그룹별 CLAUDE.md벡터 검색 메모리
스케줄러내장 (cron-parser)내장 (croner)
UI없음 (CLI 전용)WebUI + macOS/iOS/Android 앱
학습 곡선낮음 (코드 직접 수정)높음 (설정 시스템 학습 필요)
수정 용이성매우 높음낮음 (추상화 레이어 많음)

14. 디렉토리 트리

nanoclaw/
├── src/                          # 호스트 프로세스 소스
│   ├── index.ts                  # 오케스트레이터 (메인 엔트리)
│   ├── channels/
│   │   └── registry.ts           # 채널 자기 등록
│   ├── router.ts                 # 메시지 포맷 & 라우팅
│   ├── config.ts                 # 환경변수 설정
│   ├── container-runner.ts       # 컨테이너 스폰 & 마운트
│   ├── group-queue.ts            # 그룹별 큐 & 동시성
│   ├── ipc.ts                    # 파일 IPC 감시
│   ├── task-scheduler.ts         # 예약 태스크
│   ├── db.ts                     # SQLite CRUD
│   ├── credential-proxy.ts       # 자격증명 격리 프록시
│   ├── mount-security.ts         # 마운트 경로 검증
│   └── types.ts                  # 공통 타입 정의
├── container/                    # 컨테이너 관련
│   ├── Dockerfile                # 에이전트 컨테이너 이미지
│   ├── entrypoint.sh             # 컨테이너 엔트리포인트
│   ├── build.sh                  # 이미지 빌드 스크립트
│   ├── agent-runner/             # 컨테이너 내부 에이전트
│   │   └── src/
│   │       ├── index.ts          # Claude SDK 실행
│   │       └── ipc-mcp-stdio.ts  # MCP 도구 (스케줄러 등)
│   └── skills/                   # 에이전트에게 주입되는 스킬
│       └── agent-browser.md      # 브라우저 자동화 가이드
├── groups/                       # 그룹별 메모리 (격리)
│   ├── main/
│   │   └── CLAUDE.md             # Main 그룹 메모리
│   └── global/
│       └── CLAUDE.md             # 전역 공유 메모리 (읽기 전용)
├── data/                         # 런타임 데이터
│   ├── messages.db               # SQLite 데이터베이스
│   ├── sessions/                 # 그룹별 .claude/ 세션
│   └── ipc/                      # 파일 IPC 디렉토리
│       └── {group}/
│           ├── messages/         # 컨테이너 → 호스트 명령
│           └── input/            # 호스트 → 컨테이너 팔로업 메시지
├── store/                        # 채널 인증 상태
├── logs/                         # 로그 파일
├── launchd/                      # macOS 서비스 설정
│   └── com.nanoclaw.plist
├── docs/
│   ├── SPEC.md                   # 전체 사양 (31.7k)
│   ├── SECURITY.md               # 보안 모델
│   └── REQUIREMENTS.md           # 설계 결정
└── CLAUDE.md                     # Claude Code 컨텍스트

15. 핵심 개념 설명

그룹 (Group)

NanoClaw에서 "그룹"은 채팅 채널(WhatsApp 그룹, Telegram 채팅 등)과 1:1로 대응하는 논리 단위입니다. 각 그룹은:

  • 별도의 groups/{name}/ 폴더 (CLAUDE.md 포함)
  • 별도의 .claude/ 세션 디렉토리
  • 별도의 SQLite 세션 ID
  • 별도의 IPC 디렉토리

를 가지며, 컨테이너도 그룹별로 격리되어 실행됩니다.

Main 그룹

Main 그룹은 특권 그룹으로, 보통 개인 DM이나 관리용 그룹에 할당됩니다.

  • 트리거 없이 모든 메시지에 반응
  • 프로젝트 루트를 읽기 전용으로 마운트 (코드 검사 가능)
  • 다른 그룹의 태스크 관리 가능
  • 모든 JID로 메시지 전송 가능

스킬 (Skill)

스킬은 Git 브랜치로 존재하는 확장 패키지입니다. 채널 추가, 기능 변경 등 모든 확장이 스킬로 이루어집니다.

# 스킬 설치 예시 (WhatsApp 채널 추가)
git remote add whatsapp https://github.com/qwibitai/nanoclaw-whatsapp.git
git fetch whatsapp main
git merge whatsapp/main
npm run build

세션 연속성

컨테이너는 매번 새로 스폰되지만, sessionId를 통해 Claude Agent SDK의 대화 맥락을 이어갑니다.

첫 번째 실행: sessionId = undefinedClaude가 새 세션 생성 → 반환된 sessionId 저장
두 번째 실행: sessionId = "abc123"Claude가 기존 세션 이어서 실행

16. Q&A: 실제 사용 시나리오

Q: WhatsApp에서 "오늘 할 일 정리해줘"라고 보내면 어떻게 처리되나요?

A: Main 그룹이라면 트리거 없이 즉시 처리됩니다. 비-Main 그룹이라면 @Andy 가 포함되어야 합니다. 메시지는 SQLite에 저장되고, 2초 내 폴링에서 감지됩니다. 컨테이너가 스폰되고, Claude Agent SDK가 groups/{name}/CLAUDE.md의 맥락을 읽은 뒤 응답을 생성하여 WhatsApp으로 전송합니다.

Q: 매일 아침 8시에 날씨를 알려달라고 할 수 있나요?

A: 네. Claude에게 "매일 오전 8시에 날씨 알려줘"라고 하면, 에이전트가 MCP 스케줄러 도구를 통해 schedule_task IPC 명령을 발행합니다. 호스트의 스케줄러가 이를 감지하고 cron: "0 8 * * *" 태스크를 DB에 등록합니다. 이후 매일 8시에 컨테이너가 자동 스폰되어 날씨를 조회하고 채팅으로 전송합니다.

Q: 여러 그룹이 동시에 메시지를 보내면 어떻게 되나요?

A: src/group-queue.ts의 동시성 제어가 처리합니다. MAX_CONCURRENT_CONTAINERS(기본값 5)까지 동시 실행이 가능하며, 초과하면 큐에서 대기합니다. 각 그룹은 독립적인 큐를 가지므로 한 그룹의 처리가 다른 그룹을 차단하지 않습니다.

Q: 컨테이너가 내 홈 디렉토리에 접근할 수 있나요?

A: 마운트 허용 목록(~/.config/nanoclaw/mount-allowlist.json)과 차단 패턴에 의해 제어됩니다. .ssh, .aws, .env 등 민감한 경로는 기본 차단됩니다. 컨테이너는 groups/{name}/data/sessions/{name}/.claude/만 읽기/쓰기 접근 가능합니다.

Q: 에이전트가 실수로 중요한 파일을 삭제할 수 있나요?

A: 컨테이너는 마운트된 볼륨 외에는 접근할 수 없습니다. Main 그룹의 경우 프로젝트 루트가 읽기 전용으로 마운트되므로 코드 수정은 불가능합니다. 그룹 폴더(groups/{name}/)만 쓰기 가능하며, 이는 그룹의 메모리 파일입니다.

Q: 새 채널(예: LINE)을 추가하려면 어떻게 하나요?

A: /customize 스킬을 실행하거나, 직접 src/channels/Channel 인터페이스를 구현한 파일을 추가하고, registerChannel('line', factory) 를 호출하면 됩니다. 스킬로 패키징된 경우 Git 브랜치를 머지하는 것만으로 설치됩니다.

Q: 메시지가 처리되지 않고 누락될 수 있나요?

A: 메시지는 SQLite에 저장되므로, 서비스 재시작 후에도 미처리 메시지가 복구됩니다. src/index.ts 초기화 시 getNewMessages()로 크래시 전 미처리 메시지를 확인하고 재처리합니다. 메시지 누락 방지를 위해 lastAgentTimestamp가 그룹별로 DB에 영속됩니다.

Q: API 키가 컨테이너에 노출될 가능성이 있나요?

A: 없습니다. 컨테이너에는 플레이스홀더 자격증명(ANTHROPIC_API_KEY=placeholder)만 주입됩니다. 실제 API 키는 호스트의 자격증명 프록시(포트 3001)에만 존재하며, 컨테이너의 모든 API 요청은 프록시를 통해 라우팅되어 호스트가 실제 헤더를 주입합니다.