External WhatsApp channel plugin for nullclaw built on top of
@whiskeysockets/baileys.
This repository intentionally lives outside nullclaw core. It speaks the
generic ExternalChannel JSON-RPC/stdio protocol and can be wired through
channels.external without adding Node code to the main runtime repository.
- QR login
- pairing-code login
- inbound text messages
- outbound text messages
- typing indicators
- message edits
- message deletes
- reactions
- read receipts
Current limits:
- no rich attachments
- no streaming chunks
- no media upload pipeline yet
- Install dependencies:
npm install- Run the plugin directly for local testing:
node ./bin/nullclaw-channel-baileys.mjs- Or link it globally:
npm link
nullclaw-channel-baileys- Wire it into
nullclawviachannels.external.
npm installThe executable entrypoint is:
./bin/nullclaw-channel-baileys.mjsor after a global/link install:
nullclaw-channel-baileysExample:
{
"channels": {
"external": {
"accounts": {
"wa-main": {
"runtime_name": "whatsapp",
"transport": {
"command": "/opt/nullclaw/plugins/nullclaw-channel-baileys",
"timeout_ms": 15000
},
"config": {
"auth_mode": "qr",
"display_name": "Chrome (Linux)"
}
}
}
}
}
}Supported plugin config keys:
auth_modeqrorpair_codepair_phone_numberRequired whenauth_mode=pair_codedisplay_nameClient display name shown during pairing
The host passes a persistent state_dir in start.params.runtime.state_dir.
This plugin stores Baileys auth files under state_dir/auth.
Complete pair-code example:
{
"channels": {
"external": {
"accounts": {
"wa-main": {
"runtime_name": "whatsapp",
"transport": {
"command": "/opt/nullclaw/plugins/nullclaw-channel-baileys",
"timeout_ms": 15000
},
"config": {
"auth_mode": "pair_code",
"pair_phone_number": "551199999999",
"display_name": "Chrome (Linux)"
}
}
}
}
}
}- Configure
auth_mode: "qr". - Start the channel with
nullclaw channel start whatsapp. - The plugin prints a QR code to stderr.
- Open WhatsApp on the phone and link a new device.
- Once the websocket is open,
healthbegins returninglogged_in=true.
Practical operator flow:
nullclaw channel start whatsapp 2> /tmp/nullclaw-baileys.logThen read the QR code from stderr output:
tail -f /tmp/nullclaw-baileys.log- Configure:
auth_mode: "pair_code"pair_phone_number: "<international number>"
- Start the channel.
- The plugin requests a pairing code from Baileys and prints it to stderr.
- Enter the code in WhatsApp linked-device flow on the phone.
- When linking succeeds,
healthreportslogged_in=true.
- Add the external account config to
nullclaw. - Start the channel with
nullclaw channel start whatsapp. - If
auth_mode=qr, scan the QR printed to stderr. - If
auth_mode=pair_code, copy the printed pairing code into WhatsApp. - Wait for the linked device to complete.
- Send a test WhatsApp message to the linked account.
- Verify inbound delivery inside
nullclaw. - Trigger one outbound reply and verify it reaches WhatsApp.
- Stop
nullclaw. - Start
nullclawagain. - The plugin reloads auth files from
state_dir/auth. - If the WhatsApp session is still valid, no QR or pair code is needed.
If WhatsApp logs the device out:
- Delete the stored auth directory under
state_dir/authif you want a clean relink. - Start the channel again.
- Repeat the QR or pair-code flow.
- No QR shown: make sure you started the channel and are looking at stderr, not stdout.
- Pair code never appears:
verify
auth_mode=pair_codeandpair_phone_numberuses international digits only. - Repeated relogin requests:
check that
state_diris persistent and writable by thenullclawuser. - Channel looks started but nothing is received: send a direct text message first; media-only traffic is ignored by the current baseline.
Manifest:
protocol_version: 2capabilities.health = truecapabilities.typing = truecapabilities.edit = truecapabilities.delete = truecapabilities.reactions = truecapabilities.read_receipts = true
Not supported:
send_richstreaming
Inbound metadata contains message_id. This plugin encodes the full WhatsApp
message key as base64url JSON so later operations can target the same message.
That encoded key is what edit_message, delete_message, set_reaction, and
mark_read expect as message.message_id.
- This plugin does not expose an HTTP API by itself.
- WhatsApp auth material is stored under host-provided
state_dir. - Anyone who can read
state_dir/authcan reuse the linked device session. - Do not put this plugin on a multi-user host without filesystem hygiene.
For deployment and hardening notes, see:
The short version:
- run
nullclawunder a dedicated service user - keep this plugin on the same host as
nullclaw - prefer
pair_codeon headless hosts - persist and protect
state_dir/auth
npm test