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

- Name
- SeongHwa Lee
- @earthloverdev
분석 일자: 2026-03-13 대상 버전: v1.2.12 저장소: https://github.com/qwibitai/nanoclaw
This article is mostly written by Claude Code
목차
- 프로젝트 개요
- 기술 스택
- 전체 아키텍처
- 핵심 모듈 구조
- 메시지 처리 파이프라인
- 오케스트레이터 상세
- 컨테이너 에이전트 시스템
- 채널 시스템
- 스케줄러 & IPC 시스템
- 보안 아키텍처
- 핵심 데이터 구조
- 레이어별 의존 관계
- OpenClaw와의 차별점
- 디렉토리 트리
- 핵심 개념 설명
- 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 Hook | Husky |
| 데이터베이스 | SQLite (better-sqlite3, 동기) |
| AI 런타임 | @anthropic-ai/claude-code (Agent SDK) |
| 스케줄러 | cron-parser |
| 로거 | pino |
| 스키마 검증 | zod |
| 컨테이너 | Docker / Apple Container |
| 서비스 관리 | macOS launchd / Linux systemd |
채널별 SDK (스킬로 설치)
| 채널 | 라이브러리 |
|---|---|
| @whiskeysockets/baileys | |
| Telegram | node-telegram-bot-api |
| Slack | @slack/bolt |
| Discord | discord.js |
| Gmail | googleapis |
의존성 특징
NanoClaw 코어는 단 5개의 런타임 의존성만 사용합니다: better-sqlite3, cron-parser, pino, yaml, zod. 채널 라이브러리는 스킬 머지 시에만 추가됩니다.
3. 전체 아키텍처
╔══════════════════════════════════════════════════════════════════════════╗
║ NanoClaw System ║
║ ║
║ ┌─────────────────────────────────────────────────────────────────┐ ║
║ │ Channels (Self-Registering Skills) │ ║
║ │ WhatsApp │ Telegram │ Slack │ Discord │ Gmail │ (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 Proxy ─ HTTP 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 |
| Database | SQLite 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.ts → storeMessage()
└── SQLite messages 테이블에 저장
│
▼
src/index.ts → startMessageLoop() [2초 폴링]
└── getNewMessages() — 미처리 메시지 조회
│
├── [비-Main 그룹] 트리거 패턴 확인 (@Andy)
│ ├── 트리거 없음 → DB에서 대기
│ └── 트리거 있음 → 계속 진행
│
▼
processGroupMessages()
└── 해당 그룹의 미처리 메시지 수집 & XML 포맷
│
▼
src/group-queue.ts → enqueueMessageCheck()
└── 동시 실행 그룹 수 확인 (MAX_CONCURRENT_CONTAINERS: 5)
│
▼
src/container-runner.ts → runContainerAgent()
└── 볼륨 마운트 구성 (그룹 폴더, 세션 디렉토리 등)
└── 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 Channel → sendMessage() → 사용자 응답 전송
그룹 큐 상태 전이
| 상태 | 설명 |
|---|---|
| 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.ts → enqueueTask()
│
▼
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.ts → startIpcWatcher()
└── 파일 감지 → 내용 파싱 → 명령 실행 → 파일 삭제
지원 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.ts │
│ Claude Agent SDK / MCP 서버 / IPC 폴링 │
└──────────────────────────────┬───────────────────────────────┘
│ HTTP (proxy)
┌──────────────────────────────▼───────────────────────────────┐
│ 외부 레이어 │
│ Anthropic API (api.anthropic.com) │
└──────────────────────────────────────────────────────────────┘
병렬 시스템:
src/ipc.ts ─── 파일 IPC 감시 (1초 폴링)
src/task-scheduler.ts ── 예약 태스크 (60초 폴링)
src/credential-proxy.ts ─ HTTP 프록시 (포트 3001)
13. OpenClaw와의 차별점
| 항목 | NanoClaw | OpenClaw |
|---|---|---|
| 코드 규모 | ~7,300 LOC | 500,000+ LOC |
| 의존성 | 5개 (코어) | 수십 개 |
| AI 런타임 | Claude Agent SDK (Anthropic 공식) | Pi Agent (자체 구현) |
| LLM 지원 | Claude 전용 | 20개+ LLM |
| 격리 방식 | OS 컨테이너 (Docker/Apple) | 애플리케이션 레벨 (샌드박스 정책) |
| 채널 방식 | Git 브랜치 스킬로 확장 | 플러그인 npm 패키지 |
| 설정 방식 | 환경변수 + 코드 수정 | config.yml (핫 리로드 지원) |
| 통신 방식 | 파일 IPC + stdin/stdout | WebSocket 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 = undefined → Claude가 새 세션 생성 → 반환된 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 요청은 프록시를 통해 라우팅되어 호스트가 실제 헤더를 주입합니다.