John KuehJohn Kueh
All skills

Claude Code skill

wacli

Read and send WhatsApp from the CLI.

Install all skills in one command:

claude mcp add-plugin johnkueh/claude-skills

Why it exists

WhatsApp has no official API. The web client requires a live browser session and you can't script it. This skill uses whatsmeow under the hood to read messages, send messages, list chats and groups, and download media — all from the terminal, without a browser.

In practice

Search messages
Input
wacli search "trip dates"
Output
Matching messages across all chats with timestamps, chat name and sender.
Send a message
Input
wacli send "+61400000000" "running 10 min late"
Output
Delivered. Returns message ID and timestamp.
skills/wacli/SKILL.mdRaw
---
name: wacli
description: "Read and send WhatsApp messages from the command line via wacli (steipete/wacli, whatsmeow-based). Use when the user asks to search WhatsApp messages, send a WhatsApp message, list chats/groups/contacts, look up a thread, or download WhatsApp media. Triggers on 'WhatsApp', 'wacli', 'send a wa message', 'find that WhatsApp thread', 'WA chats', 'who said X on WhatsApp'."
---

# wacli — WhatsApp from the CLI

`wacli` is a Go CLI built on the [whatsmeow](https://github.com/tulir/whatsmeow) library. It pairs to the user's WhatsApp account as a multi-device companion (like WhatsApp Web), syncs messages to a **local SQLite DB** at `~/.wacli/wacli.db`, and exposes commands to read, search, and send.

Repo: https://github.com/steipete/wacli · install: `brew install steipete/tap/wacli`.

## Mental model — sync vs. read

Two distinct phases:

1. **Sync** (`wacli sync` / `wacli auth --follow`) connects to WhatsApp servers and writes new messages to the local DB. Runs as a long-lived process. New messages only land in the DB while sync is running.
2. **Read** (`wacli messages list/search/show`, `wacli chats list`) queries the local DB. Fast, offline, no network. Always reads what sync has already written.

So when the user asks "what did Alice send yesterday?" — the answer comes from the local DB. If the DB is stale, the user needs to run sync first. Use `wacli doctor` to check state.

## Critical gotchas

- **Single-writer lock.** The store is locked while `wacli sync` (or any connecting command) holds it. Trying to send while sync is running fails with a lock error. Tell the user to stop sync (`pkill -f 'wacli sync'`) before sending, or run `wacli send ...` in a window where sync isn't running. `wacli doctor` shows lock state.
- **History is shallow by default.** Initial sync only pulls recent messages from the user's primary device. Older history requires `wacli history backfill --chat <jid> --requests N`. Best-effort, may not return.
- **JIDs, not phone numbers.** Most read commands take `--chat <jid>` (e.g. `61400000000@s.whatsapp.net` for DMs, `<id>@g.us` for groups). `wacli send text` accepts `--to` as either phone or JID. To find a JID: `wacli chats list --json` or `wacli contacts search "name" --json`.
- **FTS5 may not be available.** `wacli doctor` shows `FTS5 false` on stock macOS SQLite — search falls back to LIKE, which is slower and less precise. Still usable for typical agent queries.
- **Do not commit `~/.wacli/`.** It contains the session keypair and message DB.

## Setup (once)

```sh
brew install steipete/tap/wacli
wacli auth                # shows QR; pair from phone → WhatsApp → Linked Devices
```

After pairing, kick off an initial sync that exits when idle:

```sh
wacli sync --once --idle-exit 30s
```

Or leave a follower running in the background to keep the local DB hot:

```sh
wacli sync --follow >/tmp/wacli-sync.log 2>&1 &
```

## Always pass `--json` when an agent is consuming output

Default output is human-formatted (columns, truncation). `--json` produces structured rows that pipe straight into `jq`. Use it everywhere the result feeds back to Claude.

```sh
wacli messages search "magic tags" --limit 20 --json | jq '.[] | {chat, sender, ts, text}'
```

## Read recipes

### Find the chat / contact JID

```sh
# Browse chats sorted by recent activity
wacli chats list --limit 30 --json

# Substring match on a contact name
wacli contacts search "Alice" --json

# Show full contact record (aliases, tags, JID, push name)
wacli contacts show --jid <jid> --json
```

### Search messages

```sh
# Global search across all chats
wacli messages search "<query>" --limit 50 --json

# Constrain to a chat
wacli messages search "<query>" --chat <jid> --json

# Constrain by sender (DMs include both sides; --from filters senders)
wacli messages search "<query>" --from <jid> --json

# Time window (RFC3339 or YYYY-MM-DD; --before is exclusive)
wacli messages search "<query>" --after 2026-04-01 --before 2026-04-28 --json

# Only media of a specific type
wacli messages search "" --chat <jid> --type image --json
```

### List a chat's recent messages

```sh
wacli messages list --chat <jid> --limit 100 --json
wacli messages list --chat <jid> --after 2026-04-25 --json
```

### Get context around a message

Once a search returns a hit, expand the surrounding thread:

```sh
wacli messages context --chat <jid> --id <message-id> --before 10 --after 10 --json
```

### Pull older messages a primary device hasn't yet shared

```sh
wacli history backfill --chat <jid> --count 50 --requests 3 --wait 60s
```

This asks the user's phone to send a history slab. Best-effort — phone must be online and recently active.

## Send recipes

> Stop any running `wacli sync` before send commands, or they'll fail on the store lock.

### Text

```sh
wacli send text --to <phone-or-jid> --message "your text"
# Phone form is auto-converted to JID; omit `+`. E.g. --to 61400000000
```

### File (image / video / audio / document)

```sh
wacli send file --to <jid> --file /abs/path.png --caption "look at this"
wacli send file --to <jid> --file /abs/notes.pdf --filename "Meeting notes.pdf"
```

`--mime` overrides detection if `wacli` mis-classifies an attachment.

## Group management

```sh
wacli groups list --json                              # known groups (from local DB)
wacli groups refresh                                  # pull fresh group list from server
wacli groups info --jid <group-jid>                   # participants, name, description
wacli groups join --code <invite-code>
wacli groups leave --jid <group-jid>
wacli groups participants add --jid <group> --participants <jid>,<jid>
wacli groups participants remove --jid <group> --participants <jid>
wacli groups invite get --jid <group-jid>             # current invite link
```

## Media download

Search returned a media message ID? Pull the binary:

```sh
wacli media download --chat <jid> --id <message-id> --output /tmp/media
```

Outputs to the configured media dir under the store by default.

## Diagnostics

```sh
wacli doctor                  # store path, lock state, auth, FTS availability
wacli doctor --connect        # try a live connection (requires lock be free)
wacli auth status             # auth-only check
wacli version
```

If `LOCKED true` and the user isn't expecting a sync running, kill it:

```sh
pkill -f 'wacli sync' && wacli doctor
```

## Common agent flows

### "Find that WhatsApp thread where we talked about X"

```sh
wacli messages search "X" --limit 30 --json \
  | jq '.[] | {chat, sender, ts, text}'
```

If a hit looks promising, fetch context:

```sh
wacli messages context --chat <jid> --id <message-id> --before 8 --after 8 --json
```

### "Send Alice the link to Y"

```sh
ALICE=$(wacli contacts search "Alice" --json | jq -r '.[0].jid')
wacli send text --to "$ALICE" --message "Y: https://..."
```

### "What's in the family group lately?"

```sh
GROUP=$(wacli groups list --json | jq -r '.[] | select(.name=="Family") | .jid')
wacli messages list --chat "$GROUP" --limit 50 --after $(date -v-7d +%Y-%m-%d) --json
```

### Confirm sync is fresh before reporting

```sh
wacli sync --once --idle-exit 15s   # foreground catch-up, ~15s
wacli messages list --chat <jid> --limit 5 --json
```

## Output flags reference

Every command supports:

- `--json` — structured output (use this when piping to jq or returning to Claude)
- `--store <dir>` — alternate store dir (default `~/.wacli` or `$WACLI_STORE_DIR`)
- `--timeout <duration>` — bound non-sync commands (default 5m)

## What NOT to do

- Don't run `wacli send ...` while a `wacli sync --follow` is up — store is locked. Stop sync first.
- Don't paste a phone number with `+` or spaces to `--to` — strip to digits only (e.g. `61400000000`).
- Don't try to send to a JID you fished out of message metadata that ends in `@lid` (linked device alias) — use the `@s.whatsapp.net` form. `wacli contacts show` returns the right one.
- Don't expect old history to be there. Run `wacli history backfill` for a specific chat if the user asks about messages older than the sync window.
- Don't commit `~/.wacli/` — it contains session keys.