A pure-Python WhatsApp client that pairs as a linked device and gives you your own chats from the terminal:
wa sync # catch up
wa chats # 1 row per conversation
wa read "alice" # show a thread
wa send "alice" "hi" # send a text (1:1 or group)Not the Cloud Business API (which cannot read personal chats), not a Playwright browser scraper. The client speaks WhatsApp's native protocol: Noise XX over WebSocket, Signal Double Ratchet for messages, Sender Keys for groups, LtHash-verified app-state sync for chat metadata.
Requires Python 3.11+ and uv.
Two ways:
A — Bundled launcher. No install step. Clone the repo and invoke
bin/wa directly; the #!/usr/bin/env -S uv run --script shebang and
PEP 723 inline metadata make uv pull deps on the first run.
git clone <repo> whatsapp-cli
cd whatsapp-cli
./bin/wa login # pair (one-time QR scan)
./bin/wa statusB — Global tool install. Puts a wa command on PATH; the rest of
the docs use that shorter form interchangeably with bin/wa.
uv tool install --from ./whatsapp-cli whatsapp-cli
wa loginState lives under ~/.config/whatsapp-cli/ (keys) and
~/.cache/whatsapp-cli/ (chats, contacts, message history). To
uninstall cleanly, uv tool uninstall whatsapp-cli and rm -rf both
directories.
(If you previously ran a version that stored state under
~/.config/whatsapp-user-cli/, the CLI auto-renames those directories
on first import — your pairing is preserved.)
wa sync # ~3s if nothing new, longer for a backlog
wa chats # 20 most-recent conversations
wa chats --limit 200 --json # everything, machine-readable
wa read "alice" # show alice's thread; auto-fetches more if cache is short
wa read "alice" --match 2 # disambiguate when multiple "alices" match
wa read 33123456789@s.whatsapp.net # exact JID, never ambiguous
wa send "alice" "running 10 min late"
wa send "Football" "see you at 7pm" # groups work too
wa import-contacts # macOS only: pull names from Contacts.appwa --help lists every subcommand; wa <cmd> --help for per-command options
including --json, --limit, --seconds, --idle, --refresh-groups,
--no-extend, --match, --reset, --dry-run.
The repo ships with a SKILL.md and a self-contained launcher at bin/wa,
both at the project root. The skill is published in the
Vercel Labs skills format — same
YAML frontmatter + markdown body that the npx skills CLI installs, and
the same layout other agentic systems consume.
npx skills add ClementWalter/whatsapp-cliThis drops the skill under ~/.agents/skills/whatsapp-cli/ and
symlinks it into every supported agent runtime that's installed on your
machine (Claude Code, Cursor, Windsurf, Codex, Gemini CLI, …). Agents
then drive the CLI by invoking the bundled bin/wa script directly.
# Option 1 — symlink so the skill picks up live edits.
mkdir -p ~/.claude/skills
ln -s "$(pwd)" ~/.claude/skills/whatsapp-cli
# Option 2 — copy a frozen snapshot.
cp -R . ~/.claude/skills/whatsapp-cliEither way, agents that scan ~/.claude/skills/ (or
~/.agents/skills/) will load SKILL.md and resolve commands against
the sibling bin/wa launcher. No separate uv tool install needed —
bin/wa is self-contained via PEP 723.
Trigger phrases include "send a WhatsApp message", "read my WhatsApp chats", "catch me up on WhatsApp".
# One-time: build the Go oracle (used as the byte-for-byte reference for tests).
cd tools/oracle && go build -o oracle .
# Run the test suite. uv resolves dev deps from pyproject.toml's `test` group.
uv run --group test pytest tests/ -v
# Iterate without reinstalling — PEP 723 launcher pulls deps inline.
./bin/wa chats
# After editing source, reinstall the console script.
# (`--reinstall` is required: uv sees the version unchanged and won't rebuild
# under plain `--force`.)
uv tool install --reinstall --from . whatsapp-cli| Stage | Scope | State |
|---|---|---|
| 1 | Binary XML codec (tokens, nodes, JIDs, packing) | ✅ |
| 2 | Noise XX handshake + WebSocket transport | ✅ |
| 3 | QR pairing + device registration | ✅ |
| 4 | Login, keepalive, IQ round-trip | ✅ |
| 5 | Signal session, message decrypt (DM + group) | ✅ |
| 6 | CLI surface (status/login/sync/chats/read/send/migrate) | ✅ |
| 7 | App-state sync — bootstrap history works | partial |
| 8 | Media download / upload (AES-CBC + HMAC) | not yet |
| 9 | Replies, reactions, edits, deletes | not yet |
Reliability features implemented: process lock (prevents racing wa
invocations from corrupting signal.json), delivery receipts (sender's ✓✓
indicator updates, prevents server from re-queueing the same messages),
persistent Signal sessions and Sender Keys across runs.
Not dep-free. Hard runtime requirements (declared in pyproject.toml):
aiohttp— WebSocket transportcryptography— X25519, AES-GCM, HKDF for Noisesignal-protocol— Rust-backed libsignal binding (Double Ratchet, Sender Keys)protobuf— wire format for handshake / app-state messagesclick— CLI frameworkqrcode[pil]— render pairing QR in the terminalpyobjc-framework-Contacts— macOS-only, used bywa import-contacts
Only wa/wabinary/ is pure stdlib.
Reverse-engineered clients run against WhatsApp's ToS. Meta has historically sent cease-and-desist letters to popular libs (e.g. Baileys). This project is scoped to single-user personal tooling, not a service. Use on a number you can afford to have banned; expect periodic breakage when WhatsApp bumps protocol tokens; no Meta branding anywhere.