# WhatsApp Channel for Claude Code Drive your Claude Code session from WhatsApp. Choose a **linked device** for a personal account or the **official WhatsApp Cloud API** for business DMs. The default Baileys provider connects like WhatsApp Web: pair your phone, then Claude replies from your own number. Existing installations keep this provider. The optional Cloud API provider uses Meta's business API and a public HTTPS webhook, with its own credentials, messaging policies and applicable fees. Both providers feed the same MCP channel and access gate. [![Claude Code Plugin](https://img.shields.io/badge/Claude%20Code-Plugin-blue)](https://code.claude.com/docs/en/plugins) [![MCP Server](https://img.shields.io/badge/MCP-Server-green)](https://modelcontextprotocol.io) [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) > **Community project.** This plugin is not made or endorsed by Anthropic or Meta. > Its Baileys provider uses an unofficial linked-device protocol, which may > conflict with WhatsApp's terms and put the number at risk. The optional Cloud > provider uses official API transport; confirm your use is permitted under > Meta's business and AI-provider rules before enabling it. ## Installation ```sh claude plugin marketplace add Rich627/whatsapp-claude-plugin claude plugin install whatsapp-channel@whatsapp-claude-plugin claude --dangerously-load-development-channels plugin:whatsapp-channel@whatsapp-claude-plugin ``` The `--dangerously-load-development-channels` flag matters: it registers the plugin as a **channel**, so an inbound WhatsApp message wakes your session immediately. Without it the tools still load, but nothing wakes the session when messages arrive — they sit unanswered until you (or a [watchdog](./scripts/watchdog.sh)) prompt Claude to check. `--channels` does not accept this plugin yet (it is not on the research-preview allowlist), so the development flag is currently the only way. > **Know what this flag does.** Claude Code warns against loading downloaded channels this way: a loaded channel can put text from other people straight into your session. Read this plugin's code before you use it, keep the [allowlist](./ACCESS.md) to people you trust, and keep permission prompts on. Never combine it with `--dangerously-skip-permissions`: with prompts off, anyone in an allowlisted chat could get Claude to run commands on your computer. Inside the session, choose your connection: ```text /whatsapp-channel:setup /whatsapp-channel:configure provider ``` The wizard runs in your own terminal so Cloud credentials never pass through chat. Or run `bun scripts/provider.ts choose` from the plugin directory. | Connection | Setup | Main limits | | ---------------------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | Linked device (`baileys`, default) | Pair your phone; no Meta API credentials | Unofficial protocol; existing personal/group behavior | | Official (`cloud-api`) | Meta business number, token, app secret, verify token, public HTTPS webhook | DMs only in this implementation; service window/templates and Meta fees/policies apply | For linked-device pairing, save your number with `/whatsapp-channel:configure ` (country code + number, no `+`). Enter the printed pairing code in WhatsApp → Settings → Linked Devices → Link a Device → **Link with phone number instead**, or scan the QR. For official business DMs, follow [Cloud API setup](./docs/cloud-api.md). This provider does not yet support groups, message editing, phone history/address-book import or coexistence synchronization. Add an explicitly allowed personal owner; the business number is never auto-added, and the owner must be set explicitly with `/whatsapp-channel:access set owner `. A non-empty key saved in `~/.whatsapp-channel/.env` by the terminal tools wins over plugin `/config` values for that key; remove it from `.env` to return it to plugin management. Configuration readiness is not proof that the public webhook works. Rates vary; consult [Meta pricing](https://business.whatsapp.com/products/platform-pricing) and [FAQ](https://whatsappbusiness.com/resources/faq/). ## Other MCP clients (Codex CLI, Gemini CLI, Cursor) The server is a plain stdio MCP server, so any MCP client can run it. Two things are Claude Code specific and worth knowing before you start: - **Inbound messages are not pushed.** Waking a session on an incoming message uses `notifications/claude/channel`, a Claude Code extension. MCP has no standard equivalent that reaches the model, and other clients drop unknown notifications silently. Elsewhere the plugin is poll-based: call `wait_for_messages` (parks up to 40s; the first call on a connection returns whatever is already unreplied, later calls only what arrived since) or `catch_up` / `unreplied`. Most tool results also carry a count of unreplied messages, so a client finds out there is traffic on its next call. Three do not: `unreplied` and `wait_for_messages`, which just returned those very messages, and `catch_up` with no arguments, which is that count already. - **Setup is done from a terminal, not a slash command.** `/whatsapp-channel:access` and friends are Claude Code skills. Use `bun scripts/access.ts` instead (see [Access control from a terminal](#access-control-from-a-terminal)). Register the server with an absolute path — `${CLAUDE_PLUGIN_ROOT}` is substituted by Claude Code only: **Codex CLI** (`~/.codex/config.toml`) ```toml [mcp_servers.whatsapp] command = "bun" args = ["run", "--cwd", "/absolute/path/to/whatsapp-channel", "start"] startup_timeout_sec = 30 # default 10 is tight for a first Baileys connect tool_timeout_sec = 120 # default 60; wait_for_messages parks for up to 40s ``` **Gemini CLI** (`~/.gemini/settings.json`) ```json { "mcpServers": { "whatsapp": { "command": "bun", "args": ["run", "--cwd", "/absolute/path/to/whatsapp-channel", "start"], "timeout": 600000 } } } ``` **Cursor** (`~/.cursor/mcp.json` for all projects, `.cursor/mcp.json` for one) ```json { "mcpServers": { "whatsapp": { "type": "stdio", "command": "bun", "args": ["run", "--cwd", "/absolute/path/to/whatsapp-channel", "start"] } } } ``` Only one client at a time can hold a state directory’s singleton lock. With Baileys, two connections using the same linked-device auth would kick each other off. Use separate state directories for separate numbers. A second server does not fail silently — it stays up and serves a single `whatsapp_unavailable` tool naming the process that holds the connection. ## Access control from a terminal Everything the access skill does, without Claude Code: ```sh bun scripts/access.ts status # policy, allowlist, pending codes, groups bun scripts/access.ts policy pairing # open the door bun scripts/access.ts pair # approve someone who messaged you bun scripts/access.ts allow # add directly bun scripts/access.ts remove bun scripts/access.ts group add [--mention] [--allow jid1,jid2] bun scripts/access.ts set replyToMode first # ackReaction, textChunkLimit, chunkMode, mentionPatterns ``` Approving always needs the specific code, even when only one pairing is waiting: anyone can create a pending entry just by messaging the account, so "approve the pending one" is exactly what a prompt-injected request looks like. For the same reason this is a terminal command and deliberately **not** an MCP tool, so nothing arriving over WhatsApp can reach it. ## Features The list below describes the default linked-device provider. See the [provider capability comparison](./docs/cloud-api.md#capabilities-in-this-plugins-first-cloud-provider) for the initial Cloud API scope. Shared DM access, attachment processing, transcription and context recovery also work with Cloud API. - **Bidirectional messaging.** Send and receive from the session; long replies are chunked to WhatsApp's limits or sent as a document attachment past a configurable threshold. - **Typing indicator.** The chat shows "typing…" while Claude works on its message, and "recording audio…" while a voice note uploads (linked device). - **@-mentions.** `reply` can tag people so they actually get notified — ids are accepted as phone, LID, or full JID, and mentions attach only to the chunk that names them. - **Full media support.** Photos, voice notes, video, documents, and stickers, in both directions. - **Polls.** `send_poll` asks a chat a question with up to 12 options; every vote comes back to the session as a message (linked device). - **Voice transcription.** Incoming voice notes are transcribed locally via mlx-whisper (see [setup](#voice-transcription-optional)); without the script they arrive as plain attachments. - **Access control.** Pairing codes, allowlists, and per-group policies gate every inbound message — strangers never reach your session. Managed via `/whatsapp-channel:access` in Claude Code, or `bun scripts/access.ts` anywhere. - **Per-group personalities.** Each group gets its own `config.md` with a custom personality and conversation memory. - **Permission relay.** Approve or deny Claude's tool requests from WhatsApp with an emoji reaction (👍 / 👎). - **Cron tasks.** A `## Cron Jobs` section in a group's `config.md` schedules recurring server-side tasks. - **Context recovery.** After a restart, `catch_up` with no arguments says how many messages are waiting per chat - counts only, no message text - plus open tasks from `tasks.md`. Name a chat and it replays that room's recent two-way conversation, so a fresh session resumes mid-flight work without reading every chat it has. - **Dual accounts.** Run personal and business numbers side by side with separate state and behaviors. - **Self-diagnosis.** `/whatsapp-channel:doctor` checks the server process, device link, singleton lock, and config, then walks you through the fixes — no more guessing why replies stopped. ## How it works ```text WhatsApp linked device <──Baileys────────> MCP Server <──stdio──> Claude Code WhatsApp business DMs <──Cloud API/HTTPS─> MCP Server ``` The server (a single Bun process) holds either the linked-device connection or the Cloud API webhook listener and forwards inbound messages after they pass the access gate. Claude acts through shared MCP tools such as `reply`, `react`, `download_attachment`, `status`, `unreplied` and `catch_up`. Provider-specific tools include linked-device group/edit operations, polls (`send_poll`), `set_presence`, read receipts (`mark_read`), and Cloud `send_template`. Runtime state lives in `~/.whatsapp-channel/` (or `WHATSAPP_STATE_DIR`), never in the repo. Messages sent by Claude appear from your linked personal number or configured business number. Use a dedicated number if you want a distinct assistant identity. ## Voice transcription (optional) One-time setup (Apple Silicon, mlx-whisper): ```bash brew install ffmpeg # mlx-whisper uses it to decode audio python3 -m venv ~/whisper-env source ~/whisper-env/bin/activate pip install mlx-whisper cp scripts/whisper-transcribe.sh ~/whisper-transcribe.sh chmod +x ~/whisper-transcribe.sh ~/whisper-transcribe.sh path/to/sample.ogg # optional: test ``` The reference script uses `mlx-community/whisper-large-v3-turbo` — accurate, fast, multilingual. Swap the model in the script if you prefer a smaller one. **Cloud alternative.** To skip the local setup, open `/plugin`, pick this plugin, and set **Voice transcription provider** to `groq` or `openai` along with that provider's API key. Keys are kept in your OS keychain, not in `settings.json`. Voice notes are then uploaded to `api.groq.com` or `api.openai.com` for transcription. The older `TRANSCRIPTION_PROVIDER`, `GROQ_API_KEY` and `OPENAI_API_KEY` environment variables still work. ## Troubleshooting | Issue | Solution | | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Pairing code not showing | Run `/whatsapp-channel:configure ` first, then relaunch | | 440 disconnect error | Only one connection per auth state allowed. Kill stale processes: `pkill -f "whatsapp.*server"` | | Session not waking on new messages | Most common cause: launched without `--dangerously-load-development-channels plugin:whatsapp-channel@whatsapp-claude-plugin`. Tools work but inbound pushes are dropped (`Channel notifications skipped` in the MCP debug log) — relaunch with the flag. | | Messages not arriving | Known Claude Code client bug ([#37933](https://github.com/anthropics/claude-code/issues/37933)). Server-side is correct, awaiting client fix. | | Replies still send, nothing arrives | Send a **DM** to the connected number as well as a group message — a DM that lands while groups stay silent means the group sender-key path, not the connection. `~/.whatsapp-channel/diag.log` records an `inbound upsert` line for every batch WhatsApp delivers, so it distinguishes "never arrived" from "arrived and was dropped". Set `WHATSAPP_DIAG_DEBUG=1` for Baileys' full debug stream. | | Auth expired | Run `/whatsapp-channel:configure reset-auth` and re-pair | ## Documentation Full documentation lives in [USAGE.md](./USAGE.md): [access control](./USAGE.md#access-control), the [tools exposed to the assistant](./USAGE.md#tools-exposed-to-the-assistant), [dual-account setup](./USAGE.md#dual-account-setup), [session conflicts](./USAGE.md#session-conflicts), and [resetting auth](./USAGE.md#resetting-auth). ## Contributing Issues and pull requests are welcome — read [CONTRIBUTING.md](./CONTRIBUTING.md) before opening one. Report security issues privately per [SECURITY.md](./SECURITY.md). ## Star History Star History Chart ## License [Apache 2.0](./LICENSE) — Copyright 2025 Richie Liu