Date: 2026-02-22 Server: Cloud VPS (4 vCPU, 8GB RAM, 80GB disk, Ubuntu 24.04) ZeroClaw version tested: v0.1.6 (released 2026-02-22) Current platform: OpenClaw Cloud (multi-tenant Docker hosting)
- Background & Motivation
- The "Claws" Landscape
- Candidate Comparison
- Why ZeroClaw
- Deep Architecture Analysis
- Phase 0 Validation (Live Server)
- Migration Architecture Design
- Risk Assessment
- Go/No-Go Decision
- Next Steps
Simon Willison reported on Andrej Karpathy's discussion of "Claws" — an emerging category of AI agents that run on personal hardware, use messaging protocols, and can both respond to instructions and schedule autonomous tasks. Karpathy described OpenClaw as a "400K lines of vibe-coded monster" and pointed to several lighter alternatives.
Our OpenClaw Cloud platform runs on a single cloud VPS (8GB RAM). Each customer bot consumes ~420-440MB RAM in an always-running Docker container capped at 1GB. With 4 active bots plus the admin bot and api-router, we're using 3.6GB of 7.6GB available — leaving room for only 1-2 more bots before hitting limits.
| Component | Actual RAM Usage |
|---|---|
| openclaw-bot-1 | 418 MB |
| openclaw-bot-2 | 419 MB |
| openclaw-bot-3 | 422 MB |
| openclaw-bot-4 | 437 MB |
| api-router | 36 MB |
| Total containers | ~1.7 GB |
| OS + portal + cron + admin bot | ~1.9 GB |
| Grand total | ~3.6 GB |
MAX_BOTS is set to 6, but in practice we can run 4-5 before the server becomes memory-constrained.
The heaviness is NOT our platform code — portal (3 npm deps, 2700 lines), api-router (zero deps, 460 lines), cron (750 lines), and manage.sh (2000 lines) are all lean. The heavy part is the OpenClaw SDK itself: a 2.48GB Docker image running Node.js 22 with the full openclaw gateway, which idles at ~420MB per bot.
Five alternatives were identified from Karpathy's discussion and community coverage:
| Project | Language | Core LOC | RAM/bot | Telegram | Channels | Security Model | |
|---|---|---|---|---|---|---|---|
| NanoClaw | TypeScript | ~3,900 | ~50MB host + shared containers | Yes (Baileys) | Yes (skill) | 2-3 | OS containers + mount allowlist |
| nanobot | Python | ~4,000 | ~100MB | Yes | Yes | 8+ | Application-level |
| ZeroClaw | Rust | — | <5MB | Cloud API + Web (feature flag) | Yes | 17+ | Workspace scoping + ChaCha20 |
| IronClaw (NEAR) | Rust | — | unknown | No | Yes (WASM) | 5 | WASM sandbox + credential injection |
| PicoClaw | Go | — | <10MB | No | Yes | 6 | Minimal |
- IronClaw: No WhatsApp support. Eliminated.
- PicoClaw: No WhatsApp support. Eliminated.
- nanobot: Python, academic project, no container isolation, no group privacy. Weaker candidate.
Both support WhatsApp and Telegram. The key differentiators:
| Factor | NanoClaw | ZeroClaw |
|---|---|---|
| Same language as our platform | Yes (TypeScript/Node.js) | No (Rust) |
| Memory search (vector + FTS5) | No (file-only, grep) | Yes (built-in, same 70/30 hybrid) |
| Resource efficiency | ~50MB/bot | <5MB/bot |
| WhatsApp library | Baileys (proven) | wa-rs (3 days old) |
| Config approach | Source code modification | TOML config (hot-reload) |
| Workspace file conventions | Different | Same as OpenClaw |
| Migration command | No | zeroclaw migrate openclaw |
| Plugin system | None (modify source) | None (recompile Rust) |
Source: github.com/qwibitai/nanoclaw
Architecture: Single Node.js host process orchestrating ephemeral Docker containers. One container spawned per conversation turn (not always-running). Claude Agent SDK runs inside containers. Max 5 concurrent containers via GroupQueue.
Process model:
WhatsApp (Baileys) → storeMessage() → SQLite → Poll (2s)
→ GroupQueue → docker run → agent-runner/index.ts
→ Claude Agent SDK query() → stdout → host → WhatsApp sendMessage()
Memory: File-based only (CLAUDE.md per group). No vector search, no embeddings, no FTS5. Agent can grep its own group folder but has no semantic search.
WhatsApp: Baileys 7.0-rc9 in the host process. Known issues: no exponential backoff on reconnect (#183), no connection state machine, text + captions only (no media #184), QR hang on Node 20 (#157).
Key strength: ~15 source files, 9 runtime dependencies, auditable in an afternoon. Same language as our platform.
Key weakness: No memory search. Bots with 50+ memory files lose the "remember when I said..." experience entirely. This is our biggest UX sacrifice.
Multi-tenant implications: NanoClaw is single-bot. Multi-tenant requires N separate NanoClaw instances, each with its own Baileys WhatsApp connection, SQLite database, and process. No built-in HTTP API — would need to add Express server for portal integration.
Source: github.com/zeroclaw-labs/zeroclaw (17k stars, 2k forks, 27+ contributors)
Architecture: Single Rust binary (~16MB). Three modes: agent (CLI), gateway (HTTP), daemon (gateway + channels + heartbeat + scheduler). One process per bot, single-threaded async (Tokio).
Trait-driven modules: Eight swappable traits:
| Trait | Implementations |
|---|---|
| Provider | 22+: Anthropic, OpenAI, OpenRouter, Ollama, Groq, Mistral, DeepSeek, etc. |
| Channel | 9+: Telegram, Discord, Slack, WhatsApp (Cloud + Web), Matrix, IRC, iMessage, Email |
| Memory | SQLite (hybrid), Markdown, PostgreSQL, Lucid, None |
| Tool | Shell, file_read/write, memory ops, browser, content_search, cron |
| Observer | Noop, Log, Prometheus, OpenTelemetry |
| Runtime | Native, Docker (sandboxed) |
| Security | Autonomy levels (readonly/supervised/full) |
| Tunnel | Cloudflare, Tailscale, ngrok |
Memory system: Built-in hybrid search — 70% vector (cosine similarity) + 30% FTS5 (BM25 keyword). Same weights as our memory-privacy plugin. Embedding via OpenAI text-embedding-3-small or custom endpoint. SQLite storage with embedding cache (LRU, 10k entries). No external dependencies.
WhatsApp: Dual-mode. Cloud API (official, requires Meta Business Account) or Web mode (wa-rs native Rust client, requires --features whatsapp-web build flag). Web mode merged 2026-02-19, QR rendering bug fixed 2026-02-22.
Config: TOML-based, stored at ~/.zeroclaw/config.toml. Hot-reloadable for provider, model, API key. Full JSON Schema available via zeroclaw config schema.
Security: Five layers:
- Network: 127.0.0.1 bind, rate limiting, 64KB body limit
- Auth: 6-digit OTP pairing, channel allowlists, HMAC webhook validation
- Authorization: Three autonomy levels, workspace scoping, command allowlist
- Runtime: Optional Docker sandbox, read-only rootfs, memory/CPU limits
- Data: ChaCha20-Poly1305 AEAD encryption at rest
Scheduling: HEARTBEAT.md (identical concept to ours) + cron tool (zeroclaw cron add "0 9 * * *" --tz "Asia/Shanghai" "task").
Maturity concerns: v0.1.6, pre-1.0. 261 .unwrap() calls across 80 files (panic risk). std::sync::Mutex in async contexts (6 modules — potential deadlocks). Only 3 integration tests. Releases every 1-2 days.
ZeroClaw was selected as the primary candidate over NanoClaw for three reasons:
-
Memory search preservation: ZeroClaw has built-in hybrid vector + FTS5 search with the exact same 70/30 weighting we use. NanoClaw has no search at all — only file-based grep.
-
Resource efficiency: 28x lighter than OpenClaw (15MB vs 420MB actual, measured). Potential to run 50-100 bots on the same server instead of 4-5.
-
OpenClaw compatibility:
identity.format = "openclaw"reads the same workspace files (SOUL.md, AGENTS.md, etc.).zeroclaw migrate openclawcommand exists and works. Same HEARTBEAT.md concept.
NanoClaw remains the fallback if ZeroClaw's WhatsApp Web support proves unworkable — same language as our platform makes it easier to modify.
Internet → Proxy (:80/:443) → Portal Express (:3000) → Docker Daemon
| ├── api-router (:9090, 36MB)
| ├── openclaw-bot1 (:19000, ~420MB)
| ├── openclaw-bot2 (:19001, ~420MB)
| └── ...
|
+→ cron.js (every 1m)
+→ admin API endpoints
Admin Bot (host, :18000, ~940MB) → exec tool → manage.sh → containers
Internet → Proxy (:80/:443) → Portal Express (:3000) → systemd
| ├── api-router (:9090, native)
| ├── zeroclaw@bot1 (:19000, ~15MB)
| ├── zeroclaw@bot2 (:19001, ~15MB)
| └── ...
|
+→ cron.js (every 1m)
+→ admin API endpoints
zeroclaw-admin (host, :18000, ~15MB) → same workspace
| Current Component | Lines | Decision | ZeroClaw Equivalent |
|---|---|---|---|
| api-router/server.js | 460 | KEEP unchanged | Move from container to native process. Change api-router:9090 → 127.0.0.1:9090 |
| portal/server.js | 2700 | ADAPT | Replace docker compose → systemctl, docker exec → zeroclaw CLI |
| portal/cron.js | 750 | ADAPT | Replace docker compose ps → systemctl list-units, adapt session capture |
| portal/public/admin.html | 2900 | ADAPT | File paths change, JSON → TOML config format |
| manage.sh | 2000 | ADAPT | Abstract Docker calls to functions, add systemd backend |
| templates/ | all | ADAPT | Add config.toml.template, keep workspace templates |
| docker-compose.yml | 220 | REPLACE | systemd unit template zeroclaw@.service |
| Dockerfile | 35 | DROP | Pre-built binary, no image needed |
| Plugins (datetime) | 104 | DEFER | ZeroClaw may have built-in equivalent |
| Plugins (bot-relay) | 924 | DEFER | No hook system in ZeroClaw, needs upstream Rust PR or sidecar |
| Plugins (memory-privacy) | 638 | DEFER | No plugin system, would need upstream contribution |
Replaces per-bot docker-compose entries:
# /etc/systemd/system/zeroclaw@.service
[Unit]
Description=ZeroClaw bot %i
After=network.target
[Service]
Type=simple
User=zeroclaw
Group=zeroclaw
WorkingDirectory=/opt/zeroclaw/customers/%i
ExecStart=/usr/local/bin/zeroclaw daemon --config-dir /opt/zeroclaw/customers/%i
EnvironmentFile=/opt/zeroclaw/customers/%i/gateway.env
Restart=on-failure
RestartSec=5
MemoryMax=64M
CPUQuota=50%
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
NoNewPrivileges=true
ReadWritePaths=/opt/zeroclaw/customers/%i
[Install]
WantedBy=multi-user.target| Current (Docker) | Proposed (systemd) |
|---|---|
docker compose up -d openclaw-<name> |
systemctl start zeroclaw@<name> |
docker compose stop openclaw-<name> |
systemctl stop zeroclaw@<name> |
docker compose restart openclaw-<name> |
systemctl restart zeroclaw@<name> |
docker logs -f openclaw-<name> |
journalctl -u zeroclaw@<name> -f |
docker inspect --format '{{.State.Health}}' |
curl http://localhost:<port>/health |
unbuffer docker exec -t ... openclaw channels login |
unbuffer zeroclaw channel add whatsapp '...' |
docker compose ps --format '{{.Name}}' |
systemctl list-units zeroclaw@* --state=active |
Keep centralized proxy (recommended). The api-router is battle-tested, tiny (36MB actual), and provides tamper-proof cost tracking. The only change:
# Bot's config.toml — point to api-router
[models.anthropic]
api_url = "http://127.0.0.1:9090" # was http://api-router:9090 (Docker bridge)
api_key = "irt_<hex>" # same internal token schemeZeroClaw has api_url and api_key top-level config fields that override provider defaults. The api-router validates the irt_ token, swaps for the real Anthropic key, proxies the request, and extracts cost from the response. No changes to api-router itself.
| Responsibility | Still Needed? | Changes Required |
|---|---|---|
| Owner channel capture | Yes | Major: ZeroClaw has no sessions.json. Need alternative data source. |
| Federation heartbeat | Yes | None — reads federation.json, independent of bot runtime. |
| New bot detection | Yes | Replace docker compose ps with systemctl list-units. |
| Bot dropoff | Yes | Same detection logic, different liveness check. |
| Trial enforcement | Yes | Replace docker compose stop with systemctl stop. |
| Aspect | OpenClaw (current) | ZeroClaw |
|---|---|---|
| Storage | SQLite main.sqlite |
SQLite brain.db |
| Vector | Gemini embeddings via api-router | OpenAI/custom embeddings |
| FTS5 | Built-in | Built-in |
| Hybrid weights | 70% vector / 30% keyword | 70% vector / 30% keyword (configurable) |
| Chunking | 400 tokens, 80 overlap | 512 max tokens |
| Privacy filtering | memory-privacy plugin | Not implemented |
| Config | memorySearch.remote.{baseUrl,apiKey} |
memory.embedding_provider, memory.embedding_model |
All tests performed on the production server (Cloud VPS) on 2026-02-22.
gh release download v0.1.6 --repo zeroclaw-labs/zeroclaw \
--pattern 'zeroclaw-x86_64-unknown-linux-gnu.tar.gz' --dir /tmp/
tar xzf /tmp/zeroclaw-x86_64-unknown-linux-gnu.tar.gz
cp zeroclaw /usr/local/bin/zeroclaw- Binary size: 16 MB
- Version:
zeroclaw 0.1.6 - Auto-initialized: Created
~/.zeroclaw/config.toml(311 lines, full defaults)
zeroclaw doctor
Results: 21 ok, 4 warnings, 1 error.
- Config file found and parsed
- Provider "openrouter" valid, no API key set (expected)
- No channels configured (expected)
- Detected: git 2.43.0, Python 3.12.3, Node 22.22.0, Docker 29.2.1
- 8 CLI tools discovered
Started with zeroclaw gateway -p 42617:
🦀 ZeroClaw Gateway listening on http://127.0.0.1:42617
🌐 Web Dashboard: http://127.0.0.1:42617/
POST /pair — pair a new client
POST /webhook — {"message": "your prompt"}
GET /api/* — REST API (bearer token required)
GET /ws/chat — WebSocket agent chat
GET /health — health check
GET /metrics — Prometheus metrics
Health endpoint (GET /health):
{
"status": "ok",
"paired": false,
"runtime": {
"pid": 4191084,
"uptime_seconds": 1,
"components": {
"gateway": {
"status": "ok",
"restart_count": 0,
"last_ok": "2026-02-22T18:34:18Z"
}
}
}
}Webhook endpoint (POST /webhook):
- Accepts
{"message": "..."}JSON - Returns
{"error": "LLM request failed"}without API key (expected) - Confirms the endpoint is functional
Web Dashboard: Embedded React SPA served at /. Built-in, no separate install.
TOML only. No JSON config support. The zeroclaw config schema command outputs full JSON Schema (useful for validation tooling but the config file must be TOML).
Key config sections relevant to our platform:
# Provider routing
default_provider = "anthropic"
default_model = "anthropic/claude-sonnet-4-20250514"
api_url = "http://127.0.0.1:9090" # API router
api_key = "irt_<hex>" # Internal token
# Gateway
[gateway]
port = 42617
host = "127.0.0.1"
require_pairing = false
# Memory (hybrid search)
[memory]
backend = "sqlite"
embedding_provider = "none" # or "openai", "custom:URL"
vector_weight = 0.7
keyword_weight = 0.3
# Identity (OpenClaw compatible)
[identity]
format = "openclaw"
# Heartbeat
[heartbeat]
enabled = true
interval_minutes = 30
# Cost tracking (self-reported)
[cost]
enabled = true
daily_limit_usd = 10.0
monthly_limit_usd = 100.0
# Channels
[channels_config.whatsapp]
session_path = "~/.zeroclaw/state/wa-session.db" # Triggers Web mode
allowed_numbers = ["*"]
[channels_config.telegram]
bot_token = "123456:ABC..."
allowed_users = ["*"]BLOCKER: The pre-built binary does NOT include WhatsApp Web.
When configuring [channels_config.whatsapp] with session_path (Web mode trigger):
WARN zeroclaw::channels: WhatsApp Web backend requires 'whatsapp-web' feature.
Enable with: cargo build --features whatsapp-web
The WhatsApp Web channel was merged on 2026-02-19 (PR #859), hardened in PR #1059, and had a QR rendering bug fixed on 2026-02-22 (PR #1371). However:
- Not in default build:
Cargo.tomldefault features are empty. The CI release builds do not include--features whatsapp-web. - GitHub issue #1301: Open, requesting features be included in Docker/release builds. Zero comments as of 2026-02-22.
- Library: Uses wa-rs v0.2 (native Rust), NOT Baileys. Custom SQLite storage backend for Signal Protocol keys.
- No Rust on server: Would need to install Rust toolchain and compile from source (~15-30 min, 2-4GB RAM during build).
- Known gaps: No media attachments (PR #1267 open), LID-to-phone normalization issues (PR #1295 open).
WhatsApp Cloud API (alternative mode): Works in pre-built binary. Set phone_number_id and access_token instead of session_path. But requires Meta Business Account — not compatible with our personal-number QR pairing flow.
Tested with real customer data:
zeroclaw migrate openclaw \
--config-dir /tmp/zeroclaw-migrate-test \
--source /path/to/openclaw/workspace \
--dry-runDry run output:
🔎 Dry run: OpenClaw migration preview
Source: /path/to/openclaw/workspace
Target: /tmp/zeroclaw-migrate-test/workspace
Candidates: 59
- from sqlite: 0
- from markdown: 59
Actual migration:
✅ OpenClaw memory migration complete
Imported: 59
Skipped unchanged: 0
Renamed conflicts: 0
Migrated database (brain.db):
| Table | Rows | Purpose |
|---|---|---|
memories |
59 | Main store (id, key, content, category, embedding, timestamps) |
memories_fts |
59 | FTS5 full-text index |
embedding_cache |
0 | Empty (no embedding provider configured) |
Sample migrated memory:
[core] openclaw_openclaw_core_3: 出生日期:2026-02-19
[core] openclaw_openclaw_core_8: **每日HN总结**:已设置凌晨1点自动发送前一天Hacker News热门总结
[core] openclaw_openclaw_core_11: **个性调整**:从"witty companion"切换至"efficient assistant"模式
Memory CLI works:
zeroclaw memory stats → 59 total (48 daily, 11 core)
zeroclaw memory list → All entries listed with categories
Migration imports memory only — does not copy SOUL.md, AGENTS.md, etc. Workspace files would need separate copying during provisioning.
Measured on live server with ps and docker stats:
| Metric | OpenClaw (per bot) | ZeroClaw (gateway) | Ratio |
|---|---|---|---|
| RSS Memory | 418-437 MB | 15 MB | 28x lighter |
| Virtual Memory | — | 426 MB (mapped, not resident) | — |
| Peak RSS | — | 15 MB | — |
| Threads | ~20+ | 7 | 3x fewer |
| Binary/Image | ~2.5 GB Docker image | 16 MB binary | 156x smaller |
Capacity projection:
| Scenario | OpenClaw | ZeroClaw |
|---|---|---|
| RAM per bot (idle) | 420 MB | 15 MB |
| Available RAM for bots | ~4 GB | ~4 GB |
| Max bots (theoretical) | ~9 | ~266 |
| Max bots (practical, with overhead) | 4-5 | 50-100 |
ZeroClaw does NOT have a sessions.json equivalent. Conversation history is kept in-memory (up to max_history_messages: 50) and optionally in runtime-trace.jsonl (observability mode). There is no persistent session transcript file that maps conversation participants to their channel identities.
This means our cron's captureOwnerChannels() function — which reads OpenClaw's sessions.json to discover the owner's Telegram ID from their first DM — has no data source in ZeroClaw.
State files found:
daemon_state.json: Component health, PID, uptime. No user/session data.workspace/state/memory_hygiene_state.json: Memory cleanup timestamps.workspace/state/runtime-trace.jsonl: Empty until observability enabled.
- api-router (460 lines, zero deps) — tamper-proof cost tracking, token isolation
- Template system — same
{{PLACEHOLDER}}substitution, same personality directories - Workspace files — SOUL.md, AGENTS.md, HEARTBEAT.md, TOOLS.md, USER.md, MEMORY.md
- Identity system — identity.json schema, trust levels, cron mutations, sync views
- Trial system — trial.json, cost limits, goodbye messages via usage.json
- Invite/purge/trash — same business logic, different process commands
- LUKS encryption — orthogonal to process management
- Reverse proxy — unchanged
| Current | Replacement |
|---|---|
docker compose up/stop/restart |
systemctl start/stop/restart zeroclaw@<name> |
docker logs |
journalctl -u zeroclaw@<name> |
docker exec ... openclaw |
Direct zeroclaw CLI calls |
docker inspect health checks |
curl localhost:<port>/health + systemctl is-active |
docker-compose.yml (append/remove entries) |
systemd unit files (enable/disable) |
openclaw.json per bot |
config.toml per bot |
| Container security (cap_drop, read_only, non-root) | systemd sandboxing (ProtectSystem, NoNewPrivileges) |
| Docker bridge network | localhost networking |
- Dockerfile — no image builds
- docker-compose.yml — replaced by systemd units
- OpenClaw SDK (2.48GB image) — replaced by 16MB binary
- 1GB/bot memory allocation — down to ~15MB actual
New templates/config.toml.template replacing templates/openclaw.json.template:
default_provider = "anthropic"
default_model = "anthropic/claude-sonnet-4-20250514"
default_temperature = 0.7
api_url = "http://127.0.0.1:9090"
api_key = "{{INTERNAL_TOKEN}}"
[gateway]
port = {{GATEWAY_PORT}}
host = "127.0.0.1"
require_pairing = true
[heartbeat]
enabled = true
interval_minutes = 30
[memory]
backend = "sqlite"
embedding_provider = "custom:http://127.0.0.1:9090/gemini/v1beta"
vector_weight = 0.7
keyword_weight = 0.3
[identity]
format = "openclaw"
[cost]
enabled = true
daily_limit_usd = {{COST_LIMIT}}
[channels_config.whatsapp]
session_path = "state/wa-session.db"
allowed_numbers = []
[channels_config.telegram]
bot_token = "{{TELEGRAM_BOT_TOKEN}}"
allowed_users = []
[autonomy]
level = "supervised"
workspace_only = true
allowed_commands = ["git", "npm", "ls", "cat", "grep", "find", "echo", "pwd", "wc", "head", "tail", "date"]Phase 0: Validate (completed — this document)
Phase 1: Parallel Infrastructure (2-3 days)
- Create systemd unit template
zeroclaw@.service - Adapt manage.sh: abstract Docker commands behind functions, add systemd backend
- Create
config.toml.template - Move api-router to native process (or
--network host) - Both Docker and systemd bots coexist
Phase 2: Portal Adaptation (2-3 days)
- Update server.js:
docker compose→systemctl,docker exec→zeroclawCLI - Update cron.js: process detection, session capture adaptation
- Update admin backend: file paths, JSON → TOML config reads/writes
- Test full onboarding flow end-to-end
Phase 3: First Live Migration (1 day)
- Pick least-active bot for testing
- Stop Docker container →
zeroclaw migrate openclaw→ start systemd service - Coordinate WhatsApp re-pairing with owner (credentials are device-bound, cannot migrate)
- Monitor 24 hours
**Phase 4: Gradual Rollout