WhatsApp is the default communication channel for NanoClaw. It’s built into the core codebase and requires no additional skills or installation steps beyond initial authentication.
Overview
NanoClaw connects to WhatsApp using the Baileys library, which provides a clean interface to WhatsApp’s Web API.
Features
Group and individual chats - Message the assistant in any WhatsApp chat
Multi-device support - Uses WhatsApp’s official multi-device protocol
Automatic reconnection - Handles disconnections gracefully
Typing indicators - Shows when the assistant is working
Message queuing - Queues messages when disconnected and flushes on reconnect
LID translation - Handles WhatsApp’s Locally IDentified (LID) JID format
Group metadata sync - Automatically syncs group names every 24 hours
How it works
The WhatsApp channel implementation is in src/channels/whatsapp.ts (src/channels/whatsapp.ts:1):
export class WhatsAppChannel implements Channel {
name = 'whatsapp' ;
async connect () : Promise < void > {
// Connects to WhatsApp using Baileys
}
async sendMessage ( jid : string , text : string ) : Promise < void > {
// Sends messages with assistant name prefix
}
async setTyping ( jid : string , isTyping : boolean ) : Promise < void > {
// Shows typing indicator
}
}
Architecture
WhatsApp (Baileys) → SQLite → Polling loop → Container (Claude Agent SDK) → Response
Authentication
WhatsApp Web QR code authentication. Credentials stored in store/auth/.
Message receipt
Baileys emits messages.upsert events, filtered by registered groups.
Message storage
Messages are stored in SQLite (store/messages.db).
Queue processing
The polling loop checks for messages that require agent responses.
Container invocation
Agent runs in an isolated Linux container with the group’s filesystem mounted.
Response routing
Response is sent back via sendMessage() with assistant name prefix.
Setup
WhatsApp setup is handled by the /setup skill when you first install NanoClaw:
The setup process will:
Generate a QR code for WhatsApp Web authentication
Wait for you to scan it with your phone
Store authentication credentials in store/auth/
Connect to WhatsApp and verify the connection
If authentication fails or expires, run /setup again to re-authenticate.
Configuration
Assistant has own number
Set this in src/config.ts (src/config.ts:1):
export const ASSISTANT_HAS_OWN_NUMBER = false ; // Default: shared number
Shared number (false):
You and the assistant share the same WhatsApp number
Assistant messages are prefixed with the assistant name (e.g., “Andy: Hello!”)
Required for self-chat (messaging yourself)
Own number (true):
The assistant has its own dedicated WhatsApp number
No message prefix needed
Cleaner appearance in group chats
Assistant name
The assistant name is used as the message prefix and trigger pattern:
export const ASSISTANT_NAME = 'Andy' ;
Messages from the assistant appear as:
Andy: Your scheduled task completed successfully.
Connection settings
Connection logic is in src/channels/whatsapp.ts (src/channels/whatsapp.ts:52):
const { version } = await fetchLatestWaWebVersion ({});
this . sock = makeWASocket ({
version ,
auth: { creds: state . creds , keys: makeCacheableSignalKeyStore ( state . keys , logger ) },
printQRInTerminal: false ,
logger ,
browser: Browsers . macOS ( 'Chrome' ),
});
Message handling
Bot message detection
The channel determines if a message came from the bot (src/channels/whatsapp.ts:216):
const isBotMessage = ASSISTANT_HAS_OWN_NUMBER
? fromMe
: content . startsWith ( ` ${ ASSISTANT_NAME } :` );
This prevents the bot from responding to its own messages.
Message prefix
Outgoing messages are prefixed unless using a dedicated number (src/channels/whatsapp.ts:240):
const prefixed = ASSISTANT_HAS_OWN_NUMBER
? text
: ` ${ ASSISTANT_NAME } : ${ text } ` ;
Group names are synced from WhatsApp every 24 hours (src/channels/whatsapp.ts:293):
async syncGroupMetadata ( force = false ): Promise < void > {
const groups = await this . sock . groupFetchAllParticipating ();
for ( const [ jid , metadata ] of Object.entries( groups )) {
if ( metadata . subject ) {
updateChatName ( jid , metadata . subject );
}
}
}
This ensures group names in the database match their current WhatsApp names.
WhatsApp uses JIDs (Jabber IDs) to identify chats:
Individual: 1234567890@s.whatsapp.net
Group: 120363012345678901@g.us
LID (newer format): 123456:78@lid
The channel translates LID JIDs to phone JIDs automatically (src/channels/whatsapp.ts:324).
Registering chats
To make the assistant respond in a chat, register it:
Main chat (self-chat)
Your self-chat is the admin control channel:
registerGroup ( "<your-number>@s.whatsapp.net" , {
name: "Main" ,
folder: "main" ,
trigger: `@ ${ ASSISTANT_NAME } ` ,
added_at: new Date (). toISOString (),
requiresTrigger: false ,
});
Group chats
For WhatsApp groups:
registerGroup ( "<group-jid>@g.us" , {
name: "Family Chat" ,
folder: "family-chat" ,
trigger: `@ ${ ASSISTANT_NAME } ` ,
added_at: new Date (). toISOString (),
requiresTrigger: true , // Only responds when mentioned
});
Use requiresTrigger: true for group chats so the assistant only responds when explicitly mentioned.
Troubleshooting
Authentication expired
If you see “WhatsApp authentication required” notifications:
Follow the QR code authentication flow again.
Connection issues
The channel automatically reconnects on disconnection (src/channels/whatsapp.ts:95):
if ( connection === 'close' ) {
const shouldReconnect = reason !== DisconnectReason . loggedOut ;
if ( shouldReconnect ) {
logger . info ( 'Reconnecting...' );
this . connectInternal (). catch ( ... );
}
}
Messages are queued during disconnection and flushed on reconnect.
Not receiving messages
Check if the chat is registered:
sqlite3 store/messages.db "SELECT * FROM registered_groups"
If the chat isn’t listed, register it using the process above.
Bot responding to itself
Ensure ASSISTANT_HAS_OWN_NUMBER is set correctly in src/config.ts:
If sharing a number, set to false (bot messages will have the name prefix)
If using a dedicated number, set to true (bot messages are identified by fromMe)
Switching to another channel
You can add other channels alongside WhatsApp or replace it entirely:
Add alongside WhatsApp
/add-telegram # Keeps WhatsApp, adds Telegram
Replace WhatsApp
When adding a new channel, choose “Replace WhatsApp” and set the appropriate flag:
TELEGRAM_ONLY = true # in .env
This disables WhatsApp channel creation while keeping the implementation available.
Implementation details
Message queue
Messages sent while disconnected are queued and flushed on reconnect (src/channels/whatsapp.ts:357):
private async flushOutgoingQueue (): Promise < void > {
while (this.outgoingQueue.length > 0) {
const item = this . outgoingQueue . shift () ! ;
await this . sock . sendMessage ( item . jid , { text: item . text });
}
}
Presence updates
The channel announces availability on connection (src/channels/whatsapp.ts:129):
this . sock . sendPresenceUpdate ( 'available' ). catch ( ... );
And sends typing indicators per chat (src/channels/whatsapp.ts:278):
async setTyping ( jid : string , isTyping : boolean ): Promise < void > {
const status = isTyping ? 'composing' : 'paused' ;
await this.sock.sendPresenceUpdate( status , jid);
}
Source code reference
The WhatsApp implementation is fully contained in:
src/channels/whatsapp.ts - Main WhatsAppChannel class (379 lines)
src/config.ts - Configuration constants
src/index.ts - Channel initialization and routing
Next steps
Add Telegram Add Telegram support alongside or instead of WhatsApp
Add Discord Add Discord support to your installation