Personal Claude assistant. See README.md for philosophy and setup. See docs/REQUIREMENTS.md for architecture decisions.
Single Node.js process with skill-based channel system. Channels (WhatsApp, Telegram, Slack, Discord, Gmail) are skills that self-register at startup. Messages route to Claude Agent SDK running as native Node.js processes (no containers). Each group has isolated filesystem and memory.
| File | Purpose |
|---|---|
src/index.ts |
Orchestrator: state, message loop, agent invocation |
src/channels/registry.ts |
Channel registry (self-registration at startup) |
src/ipc.ts |
IPC watcher and task processing |
src/router.ts |
Message formatting and outbound routing |
src/config.ts |
Trigger pattern, paths, intervals |
src/container-runner.ts |
Spawns native agent processes with env-based path config |
src/task-scheduler.ts |
Runs scheduled tasks |
src/db.ts |
SQLite operations |
groups/{name}/CLAUDE.md |
Per-group memory (isolated) |
skills/ |
Global skills synced into agent session dirs at runtime |
container/agent-runner/ |
Agent runner: receives input via stdin, runs agent backend, outputs via IPC |
container/agent-runner/src/backend.ts |
Backend abstraction: AgentBackend interface, createBackend() factory |
container/agent-runner/src/backends/ |
Backend implementations (claude-code, codex) |
scripts/codex_proxy/ |
Codex proxy: translates OpenAI Chat Completions to ChatGPT Responses API |
src/models.ts |
Model registry for /model command (Anthropic, local, codex models) |
API keys, secret keys, OAuth tokens, and auth credentials are managed by the OneCLI gateway — which provides credentials via getContainerConfig() injected as env vars into agent processes. Run onecli --help.
Two skill locations:
.claude/skills/— Claude Code skills loaded by the SDK (dev-facing, not shown in/skills)skills/— Runtime skills synced into agent session dirs (shown via/skillsin Telegram)
| Location | Skill | Purpose |
|---|---|---|
.claude/skills/ |
audit-docs |
Verify docs against codebase |
.claude/skills/ |
prompt-optimizer |
Analyze and optimize prompts |
.claude/skills/ |
vibe-code-auditor |
Audit AI-generated code |
skills/ |
add-karpathy-llm-wiki |
Persistent wiki knowledge base |
skills/ |
agent-browser |
Browser automation for agents |
skills/ |
capabilities |
Agent capability docs |
skills/ |
claw |
Run agents from CLI |
skills/ |
context |
Context management |
skills/ |
create-agent |
Create domain agents from Telegram |
skills/ |
skill-creator |
Create and manage skills |
skills/ |
status |
System status reporting |
skills/ |
sync-fleet |
Phone + nix fleet sync |
Run commands directly—don't tell the user to run them.
npm run dev # Run with hot reload
node node_modules/typescript/bin/tsc # Build main project (tsc not on PATH in Termux)
# Agent runner (container/agent-runner/) must be built separately:
cd container/agent-runner && npm install && node ../../node_modules/typescript/bin/tscService management:
# Termux (runit) — this installation
sv status nanoclaw
sv restart nanoclaw
sv up nanoclaw # start
sv down nanoclaw # stop
# Logs: tail -f $PREFIX/var/log/sv/nanoclaw/currentQuick rebuild shortcut: ./rebuild (runs tsc + sv restart nanoclaw)
Two hosts run NanoClaw: phone (Termux, runit) and nix (NixOS, systemd). Both track main.
Before committing:
- Run
node node_modules/typescript/bin/tsc --noEmitto type-check - Group related changes into logical commits (e.g., separate refactor from feat from docs)
- Use conventional commit prefixes:
feat:,fix:,refactor:,docs:
Syncing after push:
~/bin/sync-fleet --nanoclaw-only # pulls, builds, restarts on both hostssync-fleet handles both phone and nix. Use --signet-only for signetai, no flag for both repos. See skills/sync-fleet/SKILL.md for details.
Common type pitfall: TelegramChannelOpts in src/channels/telegram.ts is a separate interface from ChannelOpts in src/channels/registry.ts. When adding fields to ChannelOpts, also add them to TelegramChannelOpts if the Telegram channel uses them.
Agents run as native Node.js processes — no containers, no Docker. The agent-runner
(container/agent-runner/dist/index.js) is spawned directly with workspace paths
passed via env vars (NANOCLAW_IPC_DIR, NANOCLAW_GROUP_DIR, NANOCLAW_GLOBAL_DIR,
NANOCLAW_EXTRA_DIR). Agents have full system access.
src/container-runtime.ts is a no-op stub (no runtime checks or orphan cleanup needed).
Built-in bot commands handled directly by the Telegram channel (no agent invocation):
/model— switch AI model per-group (registry insrc/models.ts)/new— clear session, start fresh/effort— set reasoning effort (low/medium/high)/plan— toggle plan-before-execute mode/skills— list available commands and skills/tools— list available agent MCP tools/compact— handled by session-commands at orchestrator level (SDK compaction)/chatid,/ping— utility commands
Commands and skills auto-register in Telegram's autocomplete menu via setMyCommands on bot start. Skills are discovered from skills/ and groups/*/skills/ SKILL.md frontmatter.
Code: TelegramChannel.connect() in src/channels/telegram.ts, discoverSkillCommands().
All MCP tools are defined in container/agent-runner/src/ipc-mcp-stdio.ts and available as mcp__nanoclaw__*:
send_chat_message— send a message to the user/group chat (with optionalsenderfor swarm bots)send_agent_message— send a message to another agent (local or remote via SSH)list_agents— list all known agents across instancesscreenshot— capture phone screen (root screencap + resize). Also~/bin/screenshotCLI.read_pdf— extract text from local PDFs or URLs (pdftotext). Also~/bin/pdf-readerCLI.schedule_task,list_tasks,pause_task,resume_task,cancel_task,update_task— task schedulingregister_group— register new chat groups with optionalcontainerConfig(main only)
Agents across hosts (phone + nix) communicate via send_agent_message MCP tool or scripts/remote-message CLI.
All paths deliver messages with full session context and trigger an immediate response.
- Service agents (telegram_main): orchestrator receives an
injectIPC message, pipes it into the active session or wakes a new one if the agent is idle - Dev agents (Claude Code sessions):
claude -c -pvia SSH resumes the most recent session; response is returned to the sender - Remote hosts use SSH; local agents use direct filesystem writes
- Config:
data/agents.json(per-instance, not in git — has"self"field for sender identity likephone:dev,nix:dev) - CLI wrappers on Termux in
~/bin/(setNANOCLAW_DIR); on nix symlinked directly - Code:
send_agent_message,list_agentsincontainer/agent-runner/src/ipc-mcp-stdio.ts;injecthandler insrc/ipc.ts; wake-up logic insrc/index.ts
Users can switch models per-group via /model in Telegram. Model registry is in src/models.ts.
- Anthropic models (opus, sonnet, haiku): use Claude Code SDK via OAuth (
claude-codebackend) - Local models (via Claude Code Router): use
ANTHROPIC_BASE_URL/ANTHROPIC_MODELenv vars - Model IDs for local models use
llama-swap,<alias>format to bypass CCR default routing - Codex models (gpt-5.5, gpt-5.4, gpt-5.4-mini): use
codexbackend via codex-proxy service
When a codex model is selected, container-runner.ts sets CODEX_MODEL and NANOCLAW_BACKEND=codex env vars. The agent-runner's backend factory (createBackend() in container/agent-runner/src/backend.ts) selects the appropriate backend implementation.
TELEGRAM_BOT_POOL in .env contains comma-separated tokens for send-only pool bots.
Pool bots are used when agents call send_chat_message with a sender parameter on tg: JIDs.
Each sender gets a stable pool bot assignment (round-robin), renamed via setMyName.
Code: initBotPool() and sendPoolMessage() in src/channels/telegram.ts, IPC routing in src/ipc.ts.
SignetAI runs as a daemon on localhost:3850, providing persistent memory across sessions.
- Config:
~/.agents/agent.yaml - Database:
~/.agents/memory/memories.db - CLI:
signet status,signet daemon restart - Dashboard:
http://localhost:3850 - Embeddings: OpenAI
text-embedding-3-large(API key in signet secrets) - Extraction/Synthesis:
codexprovider,gpt-5.4-minimodel
Two agents share the daemon:
default— Claude Code dev sessions (hooks in~/.claude/settings.json)telegram_main(and other groups) — NanoClaw service agents (MCP server in agent-runner,SIGNET_AGENT_IDset dynamically to group folder name)
Agent-runner mounts signet-mcp alongside nanoclaw MCP in container/agent-runner/src/index.ts. Tools are mcp__signet__* (memory_store, memory_recall, memory_search, knowledge graph, etc.).
Service agents get SessionStart injection — before each fresh query, the agent-runner calls POST http://127.0.0.1:3850/api/hooks/session-start and injects recalled memories into the system prompt. This bridges the gap between dev sessions (full hooks) and service agents (previously MCP-only).
Proactively store anything significant to memory — user preferences, decisions, project context, corrections. When recalling, search before assuming. Memory is cheap; forgetting is expensive.
Groups can be configured with per-agent restrictions and capabilities via containerConfig (stored as JSON in the container_config SQLite column). Fields:
allowedTools— override default tool list (e.g., restrict an agent to read-only tools)mcpServers— additional MCP servers merged with the always-presentnanoclaw+signetadditionalMounts— extra directories symlinked intogroups/{folder}/extra/timeout— custom agent timeout in millisecondsbackend— agent backend (claude-codeorcodex)
Set via register_group MCP tool or direct SQLite. The agent-runner reads container_config.json from NANOCLAW_IPC_DIR at startup.
tscis not on PATH — usenode node_modules/typescript/bin/tsc- The Grep tool fails (vendored rg binary ENOENT) — use
rgvia Bash instead /tmpis not writable — use$HOMEor$PREFIX/tmpfor temp files- Root-owned files (e.g. from
su -c screencap) needsu -c "rm ..."to clean up - Telegram Markdown v1 is fragile — use plain text for bot command responses to avoid parse errors