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:

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.