Installs just this skill. Get the whole plugin for auto-invocation.
β‘ How it fires
How this skill gets triggered: by you, by Claude, or both.
Fires itselfClaude auto-loads it when your prompt matches the work.
You can call itInvoke it directly when you want it.
Slash command/add-whatsapp
ποΈ Context preview
The summary Claude sees to decide when to auto-load this skill.
Add WhatsApp channel via native Baileys adapter. Direct connection β no Chat SDK bridge. Uses QR code or pairing code for authentication.
π Stats
Stars30,325
Forks12,869
LanguageTypeScript
LicenseMIT
π¦ Ships with nanoclaw
</> SKILL.md
add-whatsapp.SKILL.md
---name: add-whatsapp
description: Add WhatsApp channel via native Baileys adapter. Direct connection β no Chat SDK bridge. Uses QR code or pairing code for authentication.
---# Add WhatsApp Channel
Adds WhatsApp support via the native Baileys adapter β a direct WhatsApp Web
connection, no Chat SDK bridge. NanoClaw doesn't ship channels in trunk β this
skill copies the WhatsApp adapter in from the `channels` branch.
The mechanical steps under **Apply** carry `nc:` directive fences: an agent
reads the prose and applies them, and a parser can apply them deterministically
from the same document. Every directive is idempotent, so the whole skill is
safe to re-run; anything a parser can't apply falls back to the prose beside it.
## Number safety check (required)
Complete this check before running any install or authentication command. If
the user already said they want to use their **shared**, **personal**,
**main**, **existing**, or **everyday** WhatsApp number, treat it as a shared
number and show the warning immediately. Do not ask the number-type question
again.
Otherwise, ask which WhatsApp number NanoClaw will use:
```nc:prompt number_mode validate:^(dedicated|shared)$
Which WhatsApp number will NanoClaw use? `dedicated` (recommended) β a separate number used only for NanoClaw (spare SIM, eSIM, or old phone). `shared` β your existing everyday / personal WhatsApp number.
If the answer is `shared`, show this warning β tell the user:
```nc:operator when:number_mode=shared
β οΈ Risk to your WhatsApp account
Connecting your shared or personal number could cause WhatsApp to temporarily suspend or permanently ban that number. You could lose access to the WhatsApp account, chats, and groups you rely on.
We strongly recommend using a separate, dedicated number for NanoClaw.
On your personal number, the agent lives only in your "You" / self-chat. Messages other people send you are ignored entirely β never read, never answered, never flagged for approval. Nobody else can talk to the agent.
If you want the agent reachable as its own contact, consider:
β’ Telegram β a bot takes ~2 minutes to set up
β’ a dedicated WhatsApp number β spare SIM, eSIM, or old phone
β’ /add-whatsapp-cloud β the official Meta Business API
```
Then confirm how to proceed. Do not continue with installation or
authentication unless the user explicitly selects the second option:
How would you like to proceed? `dedicated` (recommended) β go back and use a dedicated number. `continue` β I understand the risk, continue with my shared number.
```
Remember the effective mode for the rest of this workflow: it is `shared` only
when the user explicitly acknowledged the risk and continued; anyone who chose
a dedicated number β up front or at the warning β continues as a
dedicated-number install without seeing the warning again:
Your personal number, where you'll chat from β digits only, country code first (e.g. 14155551234). Required β this must be YOUR number, not the agent's linked one.
```
Chatting from the bot's own number IS the shared-number setup β if the number
given equals the linked number, stop and route through the same interception
screen as the up-front pick: show the account-risk warning from the number
safety check again and get explicit acknowledgement before treating this
install as shared (or collect a genuinely different personal number and stay
dedicated). If the install does become shared, correct the mode everywhere it
was recorded β in particular make sure `.env` ends up with
`ASSISTANT_HAS_OWN_NUMBER=false`, rewriting a `true` that may already have been
written; a stale `true` on a personal number makes the bot claim messages
addressed to the human:
```nc:run effect:check when:mode=dedicated
[ "{{chat_phone}}" != "{{bot_phone}}" ]
```
## Dedicated vs personal number
The adapter behaves fundamentally differently depending on whether the linked
number is the assistant's own or the operator's personal one. The switch is
`ASSISTANT_HAS_OWN_NUMBER` in `.env`, read by the adapter itself at startup.
**Inference rule: absent (or anything other than `true`) means shared/personal**
β the safe default, since misreading a personal number as dedicated makes the
bot claim messages addressed to the human.
- **Shared/personal number** (`ASSISTANT_HAS_OWN_NUMBER` unset or not `true`) β DMs to this number and group @-tags of it address the *human*, not the bot. The adapter never emits a mention signal (`mentions: 'never'` in its declared channel defaults), so: no stranger DM ever auto-creates a messaging group or raises an admin approval card; group wirings default to a name pattern (`\b<AgentName>\b`) instead of platform mentions; auto-created chats default to `unknown_sender_policy: 'strict'`; outbound messages are prefixed with the assistant's name.
- **Dedicated number** (`ASSISTANT_HAS_OWN_NUMBER=true`) β everything sent to the number is for the bot. DMs and group mentions carry a real mention signal (`mentions: 'platform'`), unknown senders escalate via `request_approval` approval cards, and card-approved groups wire with `engage_mode: 'mention'`. No name prefix on outbound.
Use the mode selected in the required safety check. If information discovered
later contradicts that selection, ask again before changing modes; switching to
shared requires the same warning and explicit acknowledgement.
Write the answer to `.env` **explicitly in both cases** (don't rely on the
inference rule for new installs), replacing any existing
`ASSISTANT_HAS_OWN_NUMBER` line. Written in both modes so a re-run that
switches dedicated β shared doesn't leave a stale `true` behind:
If WhatsApp auth already exists (`store/auth/creds.json` present) but `.env` has no `ASSISTANT_HAS_OWN_NUMBER` line, the install predates the explicit switch. Use the mode established by the required safety check and write it explicitly.
Suggest a default by comparing the authed number against the wired DM chat:
pnpm exec tsx scripts/q.ts data/v2.db "SELECT mg.platform_id FROM messaging_groups mg JOIN messaging_group_agents mga ON mg.id=mga.messaging_group_id WHERE mg.channel_type='whatsapp' AND mg.is_group=0"
```
If the wired DM's phone **equals** the authed number, the operator is talking to the bot in their own self-chat β that's a personal number: suggest **Shared**. If they differ, the operator messages the bot from a different number: suggest **Dedicated**. Confirm with the operator either way, then write the flag and restart the service.
### Migration audit: spam-era group wirings
Before the shared-number fix, group chats approved via the channel-registration card were wired `engage_mode='pattern'` with pattern `.` β respond-to-everything β because the card flow couldn't tell groups from DMs on non-threaded platforms. On a personal number this shows up as the bot answering every message in family/work groups after someone once tapped Connect on a spam-triggered card.
List the suspect wirings (host service running β `ncl` is socket-only):
```bash
ncl wirings list --engage-mode pattern --engage-pattern "." --json
```
Cross-reference against WhatsApp group chats (`ncl messaging-groups list --channel-type whatsapp --is-group 1`). For each wiring with pattern `.` on a WhatsApp group that is *not* the operator's deliberate always-on chat (e.g. their self-chat), offer:
- **Flip to name-based engagement**: `ncl wirings update <wiring-id> --engage-mode pattern --engage-pattern '\b<AgentName>\b'` (or `--engage-mode mention` on a dedicated number)
- **Delete the wiring**: `ncl wirings delete <wiring-id>`
Stale approval cards from that era can also linger. Clear pending channel approvals for chats the operator doesn't want wired:
```bash
pnpm exec tsx scripts/q.ts data/v2.db "DELETE FROM pending_channel_approvals WHERE messaging_group_id IN (SELECT id FROM messaging_groups WHERE channel_type='whatsapp')"
```
## Self-chat engagement (shared number only)
On a shared number the agent lives in your "You" / self-chat. Choose whether it
responds to every message you write there, or only to messages addressed to it
Respond to every self-chat message, or only messages starting with @<agent name>? `all` β every message (the self-chat becomes the agent's inbox). `mention` β only messages starting with @<agent name> (keep the self-chat for your own notes too).
```
For `mention`, the engage pattern is `@` plus the regex-escaped agent name,
anchored to the start of the message, with a trailing `\b` word-boundary guard.
`\b` only terminates a match after a word character β skip it for names ending
`owner_handle` and `platform_id` are what the owner-wiring step needs. The
greeting goes out over your WhatsApp chat as soon as the service reconnects with
the linked credentials.
## Next Steps
For a shared number, set expectations β tell the user:
```nc:operator when:mode=shared
Self-chat mode: only your "You" / self-chat is connected. Messages other people send to your number are ignored β never seen, never asked about. The welcome message will land in your "You" chat on WhatsApp.
Wire a specific chat later with /manage-channels.
```
If you're in the middle of `/setup`, return to the setup flow now. Otherwise wire
this channel with `/init-first-agent` (or `/manage-channels`) β in shared
`mention` mode, pass the engage pattern above via `--engage-pattern`.
## Channel Info
- **type**: `whatsapp`
- **terminology**: WhatsApp calls them "groups" and "chats." A "chat" is a 1:1 DM; a "group" has multiple members.
- **platform-id-format**: DMs use `<phone>@s.whatsapp.net` (e.g. `14155551234@s.whatsapp.net`). Groups use `<id>@g.us`. Native adapter β the JID is the platform ID as-is, no `whatsapp:` prefix.
- **how-to-find-id**: To find your linked number after auth: `node -e "const c=JSON.parse(require('fs').readFileSync('store/auth/creds.json','utf-8'));console.log(c.me?.id?.split(':')[0].split('@')[0]+'@s.whatsapp.net')"`. Groups are auto-discovered β check `pnpm exec tsx scripts/q.ts data/v2.db "SELECT platform_id, name FROM messaging_groups WHERE channel_type='whatsapp' AND is_group=1"`.
- **supports-threads**: no
- **typical-use**: Interactive chat β direct messages or small groups
- **default-isolation**: Same agent group if you're the only participant across multiple chats. Separate agent group if different people are in different groups.
WhatsApp sessions corrupted from rapid restarts. Clear sessions, then restart the
service. Run from your NanoClaw project root:
```bash
source setup/lib/install-slug.sh
systemctl --user stop $(systemd_unit)
rm store/auth/session-*.json
systemctl --user start $(systemd_unit)
```
### Bot not responding
1. Auth exists: `test -f store/auth/creds.json`
2. Connected: `grep "Connected to WhatsApp" logs/nanoclaw.log | tail -1`
3. Channel wired: `pnpm exec tsx scripts/q.ts data/v2.db "SELECT mg.platform_id, mg.name FROM messaging_groups mg JOIN messaging_group_agents mga ON mg.id=mga.messaging_group_id WHERE mg.channel_type='whatsapp'"`
4. Service running: `systemctl --user status "$(. setup/lib/install-slug.sh && systemd_unit)"`
### "conflict" disconnection
Two instances connected with the same credentials. Ensure only one NanoClaw
process is running.
### Trunk updated but shared-number behavior unchanged (stale adapter copy)
The shared-number behavior (no stranger approval cards, name-pattern group defaults) lives in the **adapter copy** at `src/channels/whatsapp.ts`, installed from the `channels` branch β not in trunk. If you updated trunk via `/update-nanoclaw` but skipped the skill-update step, the old adapter copy neither reads `ASSISTANT_HAS_OWN_NUMBER` itself nor declares channel defaults, so trunk falls back to the legacy behavior: approval cards still fire on a personal number, and new wirings get the channel-blind defaults. Symptoms of the skew:
- `.env` says `ASSISTANT_HAS_OWN_NUMBER=false` (or unset) but strangers' DMs still raise approval cards
- `ncl wirings create` on a WhatsApp group defaults to `mention` instead of a name pattern
Fix: re-run `/add-whatsapp` (or `/update-skills`) to pull the current adapter from the `channels` branch, then restart the service. The reverse skew (new adapter, old trunk) can't happen β the adapter's `defaults` field is optional and old trunk ignores it.