← Back to blog
claude-codeanthropicmcpwhatsapptools

One WhatsApp, Five Claudes: Multi-Session Routing and Monitor as a Message Bus

· 8 min read

I run five active projects at a time. A live futures bot in ~/Work/polybillionaire. A CV rewrite in ~/Work/lodzik-cv. Byoky, my wallet product. This blog. And whatever side-project is current.

Each one has its own Claude Code terminal. I wanted one place — my phone, WhatsApp — where I could check on any of them, reply to any of them, and start new ones without being at my desk.

diogo85/claude-code-whatsapp exists, and it’s good. It’s also single-session by design. I forked it. Three things broke in sequence. Each one taught me something about Claude Code’s internals that isn’t in the docs.

Limit 1 — WhatsApp’s 4-device cap

WhatsApp Multi-Device — the thing your linked laptop uses — caps linked devices at four. Every existing WhatsApp-for-Claude-Code plugin (diogo85’s upstream, verygoodplugins/whatsapp-mcp, gokapso/claude-code-whatsapp, the half-dozen others) spawns its own Baileys socket per Claude Code session. Each session is a linked device. Open a fifth Claude Code terminal — the oldest session gets knocked offline by WhatsApp itself.

This isn’t a plugin bug. It’s a semantic mismatch. Upstream assumes one WhatsApp identity, one Claude. I wanted one WhatsApp identity, many Claudes.

The fix is architectural. Split it in two:

  • wa-daemon.cjs — persistent, owns the single Baileys socket, runs via launchd. Registers itself at /tmp/claude-wa.sock with mode 0700.
  • session-client.cjs — a thin MCP stdio subprocess, one per Claude Code session. Registers with the daemon on startup, unregisters on stdin close.

Inbound from the phone goes Baileys → daemon → router → one-or-more session-clients → MCP notification into Claude. Outbound goes Claude → reply tool → session-client → daemon → Baileys. One WhatsApp device slot regardless of how many Claudes you’re running.

Phone (WhatsApp)
    ↕  WhatsApp Web Multi-Device (Baileys v7)
wa-daemon.cjs              ← persistent, single connection, managed by launchd
    ↕  /tmp/claude-wa.sock (JSON-line IPC)
session-client.cjs         ← thin MCP stdio subprocess, one per Claude session

Claude Code (e.g. in ~/Work/polybillionaire)

This part is about 500 lines of Node. The interesting bit isn’t the socket plumbing — it’s what you do with the multiplex. I wanted four ways to address a specific Claude from my phone:

Prefix from phoneRoutes to
<N> <message>session #N (1–99, daemon-assigned on register)
#<tag> <message>session(s) whose folder-name tag matches
!allevery session — replies with one-line status
!all <message>broadcast the message to every session
(quote-reply)session that sent the quoted outbound message
(none of the above)the active session (most recent /connect-wa)

The quote-reply routing is the satisfying one. The daemon tracks outbound message IDs with a one-hour TTL; when I quote-reply a message on my phone, the WhatsApp stanza carries the quoted ID, and the daemon looks up which session sent it. No explicit addressing needed — just reply to the thing you want to reply to.

!all on its own with no body is my morning standup. Every registered session sees it as a status_request and is instructed to reply with one short line describing what it’s working on right now. I get back something like:

[1 polybillionaire] watching SOL-PERP, no signals since 02:14
[2 lodzik-cv] rewrote the Anthropic cover letter, waiting for review
[3 byoky] stress-testing the recovery-key flow before the audit

At that point I thought I was done. I wasn’t.

Limit 2 — the channel notification that doesn’t wake you

Claude Code 2.1 shipped Channels — an experimental MCP capability called notifications/claude/channel. MCP servers emit these notifications, and the host is supposed to surface them as user turns in the conversation. This is how the Telegram, Discord, and iMessage plugins deliver inbound messages.

My session-client emits them correctly. I confirmed every step:

  1. Daemon log shows the inbound routed: inbound routed via broadcast → 2 session(s): !all.
  2. Session-client debug log shows the MCP notification dispatched: → channel inbound dispatched ok.
  3. No error anywhere.

And yet: idle Claude sessions weren’t waking up. The wire worked. The host wasn’t injecting.

I could have filed a bug and waited for a Claude Code release. Instead I asked a narrower question: what does Claude Code reliably wake sessions for?

Enter Monitor

Claude Code ships a tool called Monitor. It was built for something else — tailing log files, watching CI runs, polling external APIs. You give it a shell command, every line of stdout becomes a notification, and those notifications do wake the model.

So I used Monitor as the message bus.

The session-client now does two things on every inbound:

  1. Emits notifications/claude/channel as before. If the host ever does wire this up for my Claude Code version, the preferred path will just start working with no code change.
  2. Appends one meta-only line to ~/.claude/channels/whatsapp/inbox-<tag>.log:
    2026-04-17T11:17:31.786Z inbound chat=<jid> msg=<id> route=broadcast status_request

No message content. Just chat_id, message_id, route type, and a status_request flag. The bodies stay in the daemon’s in-memory cache — never on disk.

The /connect-wa skill, when claiming a session, also starts a persistent Monitor:

tail -n 0 -F ~/.claude/channels/whatsapp/inbox-<tag>.log | grep --line-buffered " inbound "

Every appended line wakes Claude. Claude reads the route from the line, calls fetch_messages for the body if it needs one (it doesn’t, for a bare !all), and replies through the reply tool.

Monitor is under-advertised. It’s introduced as a log-tailer, but the primitive it exposes is stated plainly in its own description: each stdout line of the command you give it becomes a notification that wakes the model. That’s not incidental — it’s the whole contract. Which means Monitor isn’t a log tool that happens to work as an event bus; it’s an event bus that ships with a log-tailing example.

Once you see it that way, the generalization is obvious: anytime Claude Code’s built-in notification pipeline doesn’t fire for your case, write your events to a file and tail them with Monitor. You’re not misusing the tool — you’re using it for exactly what it does, just with your own stdout producer in front of it.

Limit 3 — spawning Claudes from the phone

Once you can talk to five Claudes, you immediately need a sixth. And you’re probably not at your desk.

Three commands, handled by the daemon before it even hits the router:

!projects              list folders in ~/Work, numbered
!spawn <N|name>        open Terminal.app + claude in that folder
!spawn <N> <prompt>    same, and type <prompt> as the first prompt
!new <name>            mkdir ~/Work/<name> and spawn

It’s osascript under the hood. The daemon asks Terminal.app to do script "cd ... && claude --dangerously-skip-permissions", then four seconds later asks System Events to type the initial prompt with a trailing return.

From my phone, from a café, I can text !spawn 2 plan what we need to ship this week and a new Claude Code instance boots up on my Mac at home with that prompt already entered. It’s the single most satisfying feature of the build. Four seconds of osascript, four years of “I wish I could just tell it to start.”

What actually matters

Two takeaways if you’re building something similar.

One. When you hit the 4-device cap on an unofficial WhatsApp client — Baileys, whiskey-sockets, whatever — daemon + fanout is the architecture. Don’t fight the protocol. Share one linked device across your processes over a Unix socket. The cap is a WhatsApp business decision, not a technical limit you can out-code.

Two. If Claude Code’s channel notifications aren’t surfacing for your setup, reach for Monitor. It’s usually described as a log-tailer, but its contract — one stdout line of the wrapped command becomes one notification that wakes the model — is a general-purpose push-delivery primitive. Any external event source that can append a line to a file fits it directly.

Other small things I won’t elaborate on but found while doing this:

  • --channels in Claude Code requires tagged entries now (server:whatsapp, not bare whatsapp). Development channels specifically need --dangerously-load-development-channels <entry> — the flag takes the tagged entry directly. Easy to get wrong if you last read the docs six weeks ago.
  • The session-client’s inbox log is the right place to be paranoid about privacy. It’s meta-only by design. Content lives in RAM in the daemon; it doesn’t need to live on disk to route.
  • Session numbers (1–99) beat tags for speed on a phone keyboard. Typing 1 status is faster than #polybillionaire status and more ergonomic than any auto-complete.

The repo, a fork of diogo85/claude-code-whatsapp with all of the above:

github.com/MichaelLod/claude-code-whatsapp

Written 2026-04-17. I’ll keep the Monitor delivery path even if Claude Code’s host-side channel injection starts working — it’s fewer moving parts, and log-tail is a more legible debugging surface than an experimental MCP notification that silently doesn’t fire.