| name | whatsapp-cli |
| description | Read and write your own WhatsApp chats from the terminal via the bundled `bin/wa` command. Pairs as a linked device (one-time QR scan); then `bin/wa sync` catches up the offline queue, `bin/wa chats` lists conversations, `bin/wa read <name>` shows a thread, `bin/wa send <peer> "<text>"` sends a 1:1 or group message, and `bin/wa send <peer> --doc <path>` shares a document (PDF or any file). Reads end-to-end-encrypted personal chats â not the Cloud Business API. Use when the user asks to interact with their personal WhatsApp account from a shell or script. |
WhatsApp User CLI
Terminal access to WhatsApp via protocol-level reimplementation. Pairs as a
linked device (same path WhatsApp Web uses), decrypts E2E chats locally, and
sends new messages. No browser automation, no Cloud Business API.
How to invoke
All commands run through the bundled launcher next to this file: bin/wa
(PEP 723 â uv resolves Python deps inline on first run, no install step).
Examples in this doc are written as bin/wa <cmd>; resolve bin/wa against
this skill's own directory. From any other working directory the same script
can be invoked with its absolute path.
If the user has installed the CLI globally (uv tool install --from <skill-dir> whatsapp-cli), wa on PATH is interchangeable with bin/wa.
When to use
Trigger this skill when the user wants to:
- read their own WhatsApp messages (
bin/wa read alice)
- catch up after being offline (
bin/wa sync)
- send a 1:1 or group message (
bin/wa send "Pierre" "hello")
- share a document / PDF (
bin/wa send "Pierre" --doc report.pdf)
- list, search, or count their chats programmatically (
bin/wa chats --json)
- check whether the CLI is paired or how recent the last sync was (
bin/wa status)
Do not use it when the user wants to operate on someone else's account,
use the WhatsApp Business API, or scrape a web UI.
First-time setup
bin/wa login
bin/wa status
State lives in:
~/.config/whatsapp-cli/device.json â pairing keys (keep private)
~/.config/whatsapp-cli/signal.json â Signal ratchet + sender keys
~/.cache/whatsapp-cli/store/ â chats, contacts, messages cache
Commands
All commands print human-readable output to stdout and structured logs to
stderr. Use --json where supported for machine parsing. Pass --debug on
the top level (bin/wa --debug ...) for verbose protocol logs.
bin/wa status
Show pairing state and last sync. Use to verify the user is set up before
running any other command.
bin/wa sync [--seconds N] [--idle N] [--refresh-groups]
Reconnect, drain the offline-message queue, exit when the queue goes idle for
--idle seconds (default 3). --seconds is a hard cap (default 120). Use
before bin/wa chats / bin/wa read if the user wants fresh data.
bin/wa sync
bin/wa sync --seconds 300
bin/wa chats [--limit N] [--json]
List conversations sorted by most recent activity. Default limit 20.
2026-05-12 dm Alice 123456789012345@lid
2026-05-12 group Football 123456789012345678@g.us
âŠ
--json emits [{jid, name, last_ts, display_name}, ...]. The
display_name field falls back to contacts.json when the app-state name
is empty (DMs).
bin/wa read <query> [--limit N] [--match N] [--no-extend] [--json]
Show messages from a single chat. <query> is a fuzzy substring match
against chat names, JIDs, and contact names (case-insensitive). Ambiguous
queries print a numbered list â pick with --match N or pass a full JID.
bin/wa read alice
bin/wa read alice --match 2
bin/wa read 123456789012345@lid
bin/wa read famille --limit 100
bin/wa read alice --no-extend
bin/wa read alice --json
Auto-extends from the user's phone if the cache is shorter than --limit;
pass --no-extend to disable that network round-trip.
bin/wa send <peer> "<text>"
Send a text message â or a document with --doc. <peer> is a fuzzy match
(same matcher as read) or a full JID. Supports 1:1 and group sends.
đš RĂGLE ABSOLUE : TOUJOURS lire avant d'envoyer, et imiter exactement le
fil. Avant chaque envoi (bin/wa send), lire d'abord les messages
précédents de cette conversation exacte avec bin/wa read <peer> --limit 30
(ou plus). Aucune exception : 1:1 comme groupe, mĂȘme si on croit connaĂźtre le
ton.
Le message rédigé doit épouser le fil sur TOUTES ses dimensions :
- Ton et registre : niveau de formalité, tutoiement/vouvoiement, humour,
blagues et références récurrentes.
- Langue : celle réellement parlée par les participants (et le mélange
éventuel).
- Style et syntaxe : longueur des phrases, façon de tourner, abréviations,
usage des majuscules.
- Ponctuation : reproduire les usages observés (ex. « .. », « ⊠», « ! »)
et ne jamais en inventer.
- Emojis : n'utiliser que des emojis déjà apparus dans le fil, et au
mĂȘme dosage. Si le fil n'en contient pas, n'en mettre aucun.
- Onomatopées et interjections : ne reprendre que celles déjà écrites
dans le fil, à l'orthographe exacte (ex. si le fil dit « Ahaha », ne pas
écrire « Haha » ni « héhé »). Aucune variante, aucune invention.
RĂšgle dure anti-innovation : aucun signe de ponctuation, aucun emoji, aucune
onomatopée ou interjection qui n'a pas déjà été vu, à l'identique, dans cette
conversation. On recycle uniquement le vocabulaire de signes et
d'expressions du fil, on n'en introduit jamais de nouveau. Un message
au mauvais ton (ou au mauvais dosage d'emojis) est aussi grave qu'un message
au mauvais destinataire, et un send est irréversible. Si le fil est vide ou
introuvable, le signaler et demander le ton souhaité plutÎt que de deviner.
bin/wa send "Pierre" "running late"
bin/wa send 33123456789@s.whatsapp.net "test"
bin/wa send "Football" "see you at 7pm"
bin/wa send "Pierre" --doc report.pdf
bin/wa send "Pierre" "see attached" --doc report.pdf
--doc <path> encrypts the file, uploads it to WhatsApp's media CDN, and
delivers it as a document message; the mimetype is auto-detected (a .pdf
arrives as application/pdf). TEXT is optional and becomes the caption.
A preview thumbnail is generated and embedded automatically when possible â
for images, PDFs (pdftoppm/sips), and HTML (wkhtmltoimage or headless
Chrome) â so the document shows an inline preview in the chat (WhatsApp only
auto-previews PDFs/images server-side, never HTML). Pass --no-thumbnail to
skip it.
â ïž A send is irreversible â there is no unsend/delete. When the exact
target matters, always pass a full JID, never a fuzzy name or a bare
number. The matcher (find_chat) does a plain substring match against
jid + app-state name + contact name, so:
- A bare phone number can silently resolve to a GROUP, not a person.
Legacy group JIDs embed a phone number (
<number>-<timestamp>@g.us), so
bin/wa send 33612345678 "..." may match that group instead of the
contact. It picks the most-recent match and only warns on stderr.
- To message yourself, use the explicit PN JID
<your-number>@s.whatsapp.net (e.g. bin/wa send 33612345678@s.whatsapp.net "note to self"). The bare number does not
reach your own DM: post-2024 self-chats are keyed by a @lid identity
that contains no phone digits, so a bare number can't match it and will
fall through to whatever group/contact happens to embed those digits.
- A full JID with
@ that matches no cached chat is accepted as-is and
sent directly â this is the safe, unambiguous path.
Limitations:
- Text only. Media, replies, reactions, edits not implemented.
- Server
<ack> means "queued for delivery", not "actually delivered".
- For a brand-new contact (you've never received from them), the CLI
fetches their prekey bundle automatically â no setup needed.
bin/wa import-contacts (macOS only)
Pull display names from Contacts.app and merge into the local contacts
cache. Required to resolve LID-identified DMs to human names. Run once
after bin/wa login and re-run whenever the user adds new contacts to
their phone.
bin/wa migrate [--dry-run]
One-shot cache cleanup: folds @s.whatsapp.net / @lid duplicate chats
into their canonical form (post-2024 WhatsApp routes most DMs via LIDs,
leaving legacy PN entries). Idempotent and safe to re-run; --dry-run
shows the plan.
bin/wa login [--reset]
Pair as a linked device. First time: scans a QR code. With --reset:
wipes existing keys and starts fresh (forces a new QR). For already-paired
devices, this runs the same drain as bin/wa sync but with a full
group-info refresh that resolves all contact display names from group
participants (slower â ~5s cold, ~0s warm).
Common workflows
"Catch me up, what's been happening?"
bin/wa sync && bin/wa chats --limit 30
"Show me messages from "
bin/wa sync && bin/wa read "<their name>" --limit 100
"Send a message to "
bin/wa read "<peer name or JID>" --limit 30
bin/wa send "<peer name or JID>" "<message text>"
bin/wa send "<number>@s.whatsapp.net" "<message text>"
"Find a chat by partial name"
bin/wa chats --json --limit 500 | jq '.[] | select(.display_name | test("theo"; "i"))'
"Get all messages from a chat as JSON for further processing"
bin/wa read "alice" --limit 1000 --json --no-extend
Output and exit codes
- All commands write to stdout/stderr in UTF-8.
- Exit code 0 on success, 1 on error (e.g. not paired, no chat matching
query).
--json mode emits a single JSON document; everything else is rendered
for human reading.
Things to know
- Single connection at a time. A file lock prevents two
bin/wa
processes from connecting simultaneously; the second one blocks until
the first finishes.
- No daemon. Messages only land locally when you run
bin/wa sync
(or any other connecting command). There's no push.
(unnamed) chats are normal initially. WhatsApp's app-state
doesn't ship contact labels to linked devices; run
bin/wa import-contacts (macOS) or let group-info backfill resolve
them via bin/wa login.
- Some group
skmsg decrypt warnings are expected ("invalid send key
id"). They mean the sender first distributed their group key while we
were disconnected, so we never received it. Future messages from that
sender in that group will decrypt fine once they redistribute.
- Reverse-engineered protocol. Meta has historically sent C&D letters
to similar projects (Baileys). Scoped to single-user personal tooling,
not a service. The user should not run this on a number they can't
afford to have banned.
Project layout (for development questions)
whatsapp-cli/
SKILL.md # this file
bin/wa # PEP 723 launcher â invoke this
wa/
cli.py # click commands â main entry point
wabinary/ # binary XML codec, oracle-validated
crypto/ # Noise XX, X25519, XEdDSA, HKDF
transport/ # WebSocket + WA frame format
signal/ # libsignal binding glue
proto/ # vendored .proto schemas
pair.py, handshake.py, clientpayload.py, store.py
cache.py, history.py, peerreq.py, prekeys.py
tools/oracle/ # Go binary wrapping whatsmeow for byte-diff tests
tests/ # pytest, oracle-diffed
pyproject.toml # console-script entrypoint: wa = wa.cli:cli