Technical notes extracted by Claude from deploying an agentic WhatsApp bot to production (OpenClaw 2026.4.23). Focus on things not in the official docs or that cost hours of debugging.
High-Level Architecture
OpenClaw is a self-hosted agentic gateway that routes messages between:
- Channels (WhatsApp via Baileys, Slack, Discord, Telegram, etc).
- Agents (isolated objects with workspace, persona, model).
- Tools via MCP (Model Context Protocol — standard protocol).
The main process is the gateway (Node 24, listens on :18789), which maintains a Baileys session per WhatsApp account and triggers agents on demand.
Config: JSON, Not YAML
The active config is ~/.openclaw/openclaw.json (under the node user, uid 1000). The env var OPENCLAW_CONFIG=/path/to/yaml is ignored by the gateway. The schema is huge (49.5k lines), validated via JSON Schema draft-07.
Useful commands:
openclaw config schema # Full JSON Schema (stdout)
openclaw config get <path> # read value
openclaw config set <path> <value> --strict-json [--dry-run] [--replace]
openclaw config set --batch-file /tmp/batch.json --strict-json
config set automatically creates a .bak before overwriting. A gateway restart is required to apply changes (docker restart openclaw-gateway).
Gotcha — . in paths: if the path contains . (e.g., a JID like 120363406566286319@g.us), the parser interprets it as an object separator. Workaround: set the entire object one level up (channels.whatsapp.groups, with value being a dict).
Inheritance Bug: Must Set on Both Channel + Account
channels.whatsapp.<X> is not inherited by channels.whatsapp.accounts.default.<X> during resolveMergedAccountConfig. Silent symptom: you set it on the channel, the value is "default" at runtime, and the gateway applies the fallback. Config changes must be duplicated:
{
"channels.whatsapp.groupPolicy": "allowlist",
"channels.whatsapp.accounts.default.groupPolicy": "allowlist",
"channels.whatsapp.groupAllowFrom": [...],
"channels.whatsapp.accounts.default.groupAllowFrom": [...]
}
groupAllowFrom Is a Sender List, Not a Group List
The name is misleading. groupAllowFrom is validated against senderE164 (the phone number of the message sender), via isNormalizedSenderAllowed(). It is not an allowlist by group.
There is no native group JID allowlist in OpenClaw. To restrict to a specific group, the options are:
- Operational: the bot is in only one group.
- Per user: list authorized phone numbers in
groupAllowFrom. The bot responds when ONE of them speaks in ANY group (with default mention). - Combined:
requireMention: true(default) everywhere, with an exception viagroups.<JID>.requireMention: falsefor the main group.
WhatsApp in DinD
The <container_name> container is Docker-in-Docker. The Docker daemon that mounts volumes belongs to the outer host (<hostname>), not the inner container. Symptoms:
-
Bind-mounting a file creates a directory instead: the daemon looks for the file on its own filesystem, doesn't find it, and creates an empty dir. Solution: package files in a thin Dockerfile via
COPY(instead of bind mounts). -
localhostdoesn't resolve in Alpine containers: use127.0.0.1. -
Binding
127.0.0.1:18789on the host conflicts: useexpose:instead ofports:. To access the UI externally:docker execor port-forward via SSH.
Baileys Pairing
docker exec -it openclaw-gateway openclaw channels add --channel whatsapp
docker exec -it openclaw-gateway openclaw channels login --channel whatsapp
The second command opens an ASCII QR code in the terminal. It must be a TTY (docker exec -it + ssh -t if via SSH). Pair with the phone app under Linked Devices. Session is persisted in a Docker volume.
Session expires after a few days. Symptom: channels status --probe reports linked, connected, in:Xm ago but messagesHandled: 0. Common root cause: error 1006 from failed hydrating participating groups on connect. Solution: logout + login (new QR).
docker exec openclaw-gateway openclaw channels logout --channel whatsapp
docker exec -it openclaw-gateway openclaw channels login --channel whatsapp
After login, verify with directory groups list — if it returns the group list, hydration completed successfully.
append Skip Bug After Restart
Each gateway restart triggers a Baileys reconnect. Messages arriving during this window come with upsert.type === "append" (history sync). The code path in /app/dist/extensions/whatsapp/monitor-BXydC-6q.js around line ~967 checks:
const msgTsNum = msg.messageTimestamp != null ? Number(msg.messageTimestamp) : NaN;
if ((Number.isFinite(msgTsNum) ? msgTsNum * 1e3 : 0) < connectedAtMs - 60_000) continue;
If messageTimestamp is absent, the fallback 0 makes the comparison always true — skipping the message. Patch: change : 0 to : Date.now(). This is a manual edit in a dist file; it must be redone after each docker compose build. Issue tracker: openclaw/openclaw#19856.
groupAllowFrom Bug (issue #54613)
Multiple issues report "DMs work, groups don't" or "messagesHandled stuck at 0 even when connected". The actual cause is a combination of:
-
groupPolicymust be set on both channel + account (inheritance bug). -
groupAllowFrommust contain E.164 sender numbers, not JIDs. -
groups.<JID>.requireMentiondefaults totrueand silently blocks inapplyGroupGatingif the message has no real mention (and WhatsApp Web doesn't always mark mentions correctly).
Full workaround is documented in @pandeysoni's comment on issue #54613.
Sessions and Conversation Context
OpenClaw has native sessions per channel+group. SessionKey:
agent:<agentId>:whatsapp:group:<JID>
Persisted in ~/.openclaw/agents/<agentId>/sessions/<uuid>.jsonl — JSONL with each turn (user message + tool calls + agent response). Consecutive messages in the same group share context, so "add 1 more" after "how much sugar does it have?" works.
To debug a conversation:
docker exec openclaw-gateway cat ~/.openclaw/agents/main/sessions/<uuid>.jsonl
MCP Transport: Legacy SSE, Not Streamable HTTP
OpenClaw 2026.4.x uses legacy HTTP+SSE for MCP, not Streamable HTTP (the newer protocol). The client sends a GET to the configured URL and expects a text/event-stream response with an event: endpoint handshake. Servers that return 405 on GET (common with Streamable HTTP-only stateless servers) fail with SSE error: Non-200 status code (405).
Solution: implement SSEServerTransport from @modelcontextprotocol/sdk alongside Streamable HTTP, exposing GET /sse (handshake) + POST /messages?sessionId=<id> (client messages). Configure MCP in OpenClaw pointing to /sse:
openclaw mcp set <name> '{"url":"http://host:port/sse"}'
Gateway Mode Is Required
gateway.mode must be set (local or remote). Without it, gateway start is blocked and openclaw doctor complains. The default is not populated — missing this once cost a full day of debugging.
Useful Debug Commands
# General status
openclaw doctor
openclaw channels status --probe
# View recent inbound/outbound
docker exec openclaw-gateway cat /tmp/openclaw/openclaw-$(date +%F).log | grep web-inbound | tail -5
docker exec openclaw-gateway cat /tmp/openclaw/openclaw-$(date +%F).log | grep web-auto-reply | tail -5
# MCP discovery
docker logs openclaw-gateway | grep bundle-mcp
# Heartbeat (check messagesHandled and lastInboundAt)
docker exec openclaw-gateway tail -3 /tmp/openclaw/openclaw-$(date +%F).log
# List groups visible to Baileys (after hydration)
openclaw directory groups list
Strategy for Debugging Silent Symptoms
When "messages aren't arriving" and no log explains it, patch the dist file with console.error at critical points. The path is /app/dist/extensions/whatsapp/monitor-BXydC-6q.js (the hash in the filename changes with each release). Useful points to instrument:
-
handleMessagesUpsert(entry) — confirms Baileys delivers -
normalizeInboundMessagenull returns — ACL, recent outbound echo, status broadcast -
enrichInboundMessagenull return — unsupported format -
claimRecentInboundMessage— dedup -
enqueueInboundMessage— confirms entry into the pipeline
Back up first (cp file file.bak), then cp file.bak file to restore.
Recommendations for Other Deployments
- Avoid DinD if possible. The outer host's Docker daemon vs the container runs in different namespaces, and bind mounts break silently.
-
Use
mcpServersfrom the start instead of proprietary TS plugins. MCP is the official path and is reusable (Claude Desktop, Cowork, etc. consume the same server). - Patch dist files carefully. Keep versioned backups; an OpenClaw upgrade will overwrite the files, losing patches. Consider forking the image with a multi-stage Dockerfile.
-
Baileys sessions are fragile. Schedule a preventive weekly restart (cron) and monitor
messagesHandledin the heartbeat — if it stays at 0 for more than 1h whileconnected, it's time to re-login.
Top comments (0)