DEV Community

Gustavo Gondim
Gustavo Gondim

Posted on

Field Learnings with OpenClaw and WhatsApp

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
Enter fullscreen mode Exit fullscreen mode

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": [...]
}
Enter fullscreen mode Exit fullscreen mode

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 via groups.<JID>.requireMention: false for 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).
  • localhost doesn't resolve in Alpine containers: use 127.0.0.1.
  • Binding 127.0.0.1:18789 on the host conflicts: use expose: instead of ports:. To access the UI externally: docker exec or 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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:

  • groupPolicy must be set on both channel + account (inheritance bug).
  • groupAllowFrom must contain E.164 sender numbers, not JIDs.
  • groups.<JID>.requireMention defaults to true and silently blocks in applyGroupGating if 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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"}'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
  • normalizeInboundMessage null returns — ACL, recent outbound echo, status broadcast
  • enrichInboundMessage null 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 mcpServers from 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 messagesHandled in the heartbeat — if it stays at 0 for more than 1h while connected, it's time to re-login.

Top comments (0)