Configuration
Configuration Philosophy
NanoClaw deliberately avoids configuration sprawl. The source code IS the configuration — the codebase is small enough that modifying code is safer than maintaining a complex config system.
There are exactly three categories of configuration:
- Environment variables — runtime settings (API keys, trigger word, timezone)
- SQLite state — dynamic state managed by the system (groups, sessions, tasks)
- External security files — out-of-band security configuration (mount allowlist, sender allowlist)
Environment Variables
File: src/config.ts
All environment configuration is loaded once at startup and exported as constants:
| Variable | Default | Purpose | Line |
|---|---|---|---|
ASSISTANT_NAME |
"Andy" |
Trigger word and agent identity | config.ts:7 |
ASSISTANT_HAS_OWN_NUMBER |
false |
Whether agent has its own phone number | config.ts:10 |
POLL_INTERVAL |
2000 (ms) |
Message loop polling frequency | config.ts:12 |
SCHEDULER_POLL_INTERVAL |
60000 (ms) |
Task scheduler polling frequency | config.ts:13 |
CONTAINER_IMAGE |
"nanoclaw-agent:latest" |
Docker image to spawn | config.ts:15 |
CONTAINER_TIMEOUT |
300000 (5 min) |
Hard timeout for containers | config.ts:17 |
CONTAINER_MAX_OUTPUT_SIZE |
10485760 (10 MB) |
Max stdout/stderr capture | config.ts:19 |
ONECLI_URL |
"http://host.docker.internal:21666" |
OneCLI credential gateway URL | config.ts:21 |
MAX_MESSAGES_PER_PROMPT |
200 |
Max messages fetched per prompt | config.ts:23 |
IPC_POLL_INTERVAL |
1000 (ms) |
IPC watcher polling frequency | config.ts:25 |
IDLE_TIMEOUT |
1800000 (30 min) |
Container idle timeout | config.ts:27 |
MAX_CONCURRENT_CONTAINERS |
3 |
Global concurrency limit | config.ts:29 |
TZ / TIMEZONE |
System timezone | Timezone for cron expressions and timestamps | config.ts:49 |
Timezone Resolution (src/config.ts:86-96)
Timezone follows a fallback chain:
TZ env var → NANOCLAW_TIMEZONE env var → system timezone (Intl.DateTimeFormat) → "UTC"
Validated against Intl.DateTimeFormat — invalid timezones fall back to UTC with a warning.
Trigger Pattern (src/config.ts:67-80)
function buildTriggerPattern(trigger: string): RegExp {
return new RegExp(`^${escapeRegex(trigger)}\\b`, 'i');
}
function getTriggerPattern(customTrigger?: string): RegExp {
if (customTrigger) return buildTriggerPattern(customTrigger);
return DEFAULT_TRIGGER; // Built from ASSISTANT_NAME
}Groups can override the trigger word. The default is @{ASSISTANT_NAME} with word-boundary matching.
Path Configuration
| Constant | Default | Purpose | Line |
|---|---|---|---|
MOUNT_ALLOWLIST_PATH |
~/.config/nanoclaw/mount-allowlist.json |
Mount security allowlist | config.ts:33 |
SENDER_ALLOWLIST_PATH |
~/.config/nanoclaw/sender-allowlist.json |
Sender filtering config | config.ts:35 |
STORE_DIR |
./store |
SQLite database directory | config.ts:37 |
GROUPS_DIR |
./groups |
Per-group persistent storage | config.ts:39 |
DATA_DIR |
./data |
IPC directories, session caches | config.ts:41 |
Security-sensitive files (mount-allowlist.json, sender-allowlist.json) live in ~/.config/nanoclaw/ — outside the project root and outside any container mount.
SQLite State (Dynamic Configuration)
Registered Groups (src/db.ts)
Groups are the primary configurable entity, stored in the registered_groups table:
CREATE TABLE IF NOT EXISTS registered_groups (
jid TEXT PRIMARY KEY,
name TEXT NOT NULL,
folder TEXT NOT NULL UNIQUE,
trigger TEXT NOT NULL,
added_at TEXT NOT NULL,
requires_trigger INTEGER DEFAULT 1,
is_main INTEGER DEFAULT 0,
container_config TEXT -- JSON: { additionalMounts?, timeout? }
)The container_config JSON field allows per-group customization:
interface ContainerConfig {
additionalMounts?: AdditionalMount[]; // Extra volume mounts
timeout?: number; // Custom container timeout
}Sessions (src/db.ts)
CREATE TABLE IF NOT EXISTS sessions (
group_folder TEXT PRIMARY KEY,
session_id TEXT NOT NULL
)Maps each group to its Claude Agent SDK session ID. Sessions persist across container restarts, enabling multi-turn conversations that span days.
Scheduled Tasks (src/db.ts)
CREATE TABLE IF NOT EXISTS scheduled_tasks (
id TEXT PRIMARY KEY,
group_folder TEXT NOT NULL,
chat_jid TEXT NOT NULL,
prompt TEXT NOT NULL,
script TEXT,
schedule_type TEXT NOT NULL, -- 'cron' | 'interval' | 'once'
schedule_value TEXT NOT NULL,
context_mode TEXT DEFAULT 'group', -- 'group' | 'isolated'
next_run TEXT,
last_run TEXT,
last_result TEXT,
status TEXT DEFAULT 'active', -- 'active' | 'paused' | 'completed'
created_at TEXT NOT NULL
)Security Configuration
Mount Allowlist (~/.config/nanoclaw/mount-allowlist.json)
Controls which host directories can be mounted into containers:
{
"allowedRoots": [
{
"path": "~/projects",
"allowReadWrite": true,
"description": "Development projects"
}
],
"blockedPatterns": ["password", "secret", "token"],
"nonMainReadOnly": true
}Key rules:
- No allowlist = all additional mounts blocked (fail-closed)
- Built-in blocked patterns always apply (
.ssh,.gnupg,.env, etc.) — merged with user patterns nonMainReadOnly: trueforces read-only for non-main groups regardless of per-mount settings- Symlinks are resolved before comparison (no symlink bypass)
Sender Allowlist (~/.config/nanoclaw/sender-allowlist.json)
Controls which senders can trigger the agent:
{
"mode": "trigger",
"groups": {
"120363336345536173@g.us": {
"allowed": ["sender1@s.whatsapp.net"],
"mode": "drop"
}
},
"logDenied": true
}Two enforcement modes:
trigger— non-allowed senders can post messages, but their messages can't trigger the agentdrop— non-allowed senders' messages are discarded before storage
Container-side Configuration
SDK Settings (src/container-runner.ts:128-149)
Each group gets an auto-generated .claude/settings.json:
{
"env": {
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1",
"CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD": "1",
"CLAUDE_CODE_DISABLE_AUTO_MEMORY": "0"
}
}These enable:
- Agent teams — subagent orchestration
- Additional CLAUDE.md loading — from extra mounted directories
- Auto memory — persistent user preferences between sessions
Group CLAUDE.md (groups/{folder}/CLAUDE.md)
Per-group agent instructions. Templates are copied from groups/main/CLAUDE.md or groups/global/CLAUDE.md when a group is registered (src/index.ts:162-179). The assistant name is substituted if different from "Andy".
Container Skills (container/skills/)
Skills are synced from container/skills/ into each group's .claude/skills/ directory at container spawn time (src/container-runner.ts:151-161). This means all groups get the same container skills, but each group's copy is independently writable.
Configuration Diagram
~/.config/nanoclaw/
├── mount-allowlist.json ← What dirs agents can access (tamper-proof)
└── sender-allowlist.json ← Who can trigger the agent (tamper-proof)
.env
├── ANTHROPIC_API_KEY ← API credential (never reaches containers)
├── ASSISTANT_NAME ← Trigger word
├── TZ ← Timezone
└── ... ← Other env vars (see table above)
store/
└── messages.db ← SQLite: messages, groups, tasks, sessions, state
groups/
├── main/CLAUDE.md ← Main group agent instructions (template)
├── global/CLAUDE.md ← Shared instructions (template)
└── {folder}/ ← Per-group storage
├── CLAUDE.md ← Copied from template on registration
├── logs/ ← Container execution logs
└── conversations/ ← Archived conversation transcripts
data/
├── ipc/{folder}/ ← Per-group IPC directories
│ ├── messages/ ← Agent → host messages
│ ├── tasks/ ← Agent → host task commands
│ └── input/ ← Host → agent follow-up messages
└── sessions/{folder}/ ← Per-group SDK sessions and skills
├── .claude/settings.json ← Auto-generated SDK settings
├── .claude/skills/ ← Synced container skills
└── agent-runner-src/ ← Per-group agent-runner code (customizable)