How I Connected Claude Code to WhatsApp
I run Claude Code on a VPS. I access it from my phone. Not through a web terminal. Not through an app. Through WhatsApp messages — just like texting a friend.
I send a message. Claude thinks. The reply lands in my WhatsApp. I can share Claude Design links from my phone. I can ask questions while commuting. I can deploy code from anywhere with signal.
Here's exactly how the relay works.
The Architecture
Three components. All on one VPS. All in pm2.
My Phone (WhatsApp)
↓
WhatsApp Servers
↓
Baileys WebSocket ← QR scan once → persistent auth
↓
wa-relay (Express, port 3232) ← HTTP bridge
↓
abah-brain (memory + context, port 3131)
↓
Claude Code (reads messages, calls tools, replies)
Component 1: The WhatsApp Socket (Baileys)
WhatsApp doesn't have a public API for sending messages. The official Business API requires Facebook verification, a phone number you don't use for personal chat, and template approval. It's designed for customer support bots — not for talking to your own AI.
Enter Baileys. It's an open-source WhatsApp WebSocket library that connects to WhatsApp's multi-device protocol — the same one WhatsApp Web uses. It doesn't require a business account. It works with your existing WhatsApp number.
// Connect to WhatsApp via Baileys
const { makeWASocket, useMultiFileAuthState } = require('baileys');
const { state, saveCreds } = await useMultiFileAuthState('auth_dir');
const sock = makeWASocket({ auth: state, printQRInTerminal: true });
// QR code appears in terminal — scan once
// Credentials saved to disk — auto-reconnect forever
sock.ev.on('messages.upsert', async (msg) => {
// New WhatsApp message received
const text = msg.messages[0]?.message?.conversation;
// Forward to Claude, get reply, send back
});
Scan a QR code once. After that, Baileys reconnects automatically using saved credentials. No re-scanning. No session expiry. It just works — like WhatsApp Web but running headlessly on a server.
Component 2: The Relay Bridge
Claude Code runs in a terminal. It can't directly receive WebSocket events. So I built a thin Express server that sits between Baileys and Claude:
WhatsApp Message → Baileys Socket
↓
wa-relay receives message event
↓
POST /message → forwards to Claude's handler
↓
Claude processes → generates reply
↓
POST /send → wa-relay sends back via Baileys
↓
WhatsApp Message delivered to my phone
The relay is just two HTTP endpoints. POST /message receives incoming WhatsApp texts. POST /send pushes replies back out. This clean separation means Claude doesn't need to know anything about WhatsApp — it just reads text and produces text.
Component 3: Claude with Tools
The real power isn't just chat — it's function calling. Claude has access to my VPS shell, web search, a persistent memory database, and a calendar. From WhatsApp, I can:
- "Check if the PPTX converter is running" → Claude runs
pm2 status pptxand replies with the process list - "Remind me to check the news pipeline at 8am" → Claude sets a reminder in abah-brain's KV store
- "What's the latest AI news?" → Claude fetches from nerdstudio.online/news and summarizes
- "Deploy the latest nara-agent commit" → Claude pulls, restarts pm2, confirms status
Each tool is a simple function that Claude calls via Gemini's function calling API. The WhatsApp relay doesn't know about tools — it just passes text. Claude decides which tool to use and formats the response.
Multi-Client Architecture
The relay supports multiple users. Each client gets their own agent instance with isolated memory, separate brain database, and independent tool access. The architecture for the client-facing version (called "Wolly") looks like this:
/opt/nerdy/wibowo/
├── nara-agent/ # The AI brain (Gemini + function calling)
│ ├── index.js # Express server on port 3400
│ ├── core/llm.js # Gemini function calling loop
│ ├── core/brain.js # Persistent memory client
│ └── tools/ # Shell, web, reminders, calendar
├── abah-brain/ # Memory database (port 3131)
└── wa-relay/ # WhatsApp socket (port 3232)
One VPS. Multiple agents. Each with their own WhatsApp number, their own memory, their own tools.
The Hard Part: Pairing
Getting Baileys to pair with WhatsApp was the hardest part. The library provides two methods: QR code scanning and 8-character pairing codes. Both work. Both are fragile.
QR codes expire in ~60 seconds. On a headless VPS, you need to display the QR somehow — I built a web page at /connect that shows the QR and auto-refreshes.
Pairing codes are even trickier. The user opens WhatsApp → Settings → Linked Devices → Link with phone number, enters an 8-character code. But the code regenerates on every QR refresh, so by the time you navigate the menus, it's already expired.
The fix: generate pairing codes on-demand via a button click, not automatically. And use the QR as the primary method — it's the same flow everyone knows from WhatsApp Web.
Running It
# Start the relay
pm2 start wa-relay --name wa-relay
# Start the agent
pm2 start nara-agent --name nara-wolly
# Start memory brain
pm2 start abah-brain
# Scan QR once, then forget about it
# WhatsApp is now your terminal
Why This Matters
Claude Code is powerful on a laptop with a full keyboard and monitor. But most of my ideas happen away from my desk — in transit, during meetings, late at night.
WhatsApp is always in my pocket. Now Claude is too.
No new apps. No web terminal. No SSH client. Just the chat app I already use, with an AI on the other end that can think, search, remember, and execute code on my server.
This is what a personal AI assistant should feel like.