From claude-skills
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'.
How this skill is triggered — by the user, by Claude, or both
Slash command
/claude-skills:comms-whatsappThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
`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.
wacli is a Go CLI built on the 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.
Two distinct phases:
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.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.
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.wacli history backfill --chat <jid> --requests N. Best-effort, may not return.--chat <jid> (e.g. [email protected] 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.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.~/.wacli/. It contains the session keypair and message DB.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:
wacli sync --once --idle-exit 30s
Or leave a follower running in the background to keep the local DB hot:
wacli sync --follow >/tmp/wacli-sync.log 2>&1 &
--json when an agent is consuming outputDefault output is human-formatted (columns, truncation, senders shown as bare @lid numbers). --json produces structured rows that pipe straight into jq. Use it everywhere the result feeds back to Claude — and never attribute a quote from the human output (see "Attribution" below).
Every --json response is an envelope: {"success":bool, "data":…, "error":…}. The payload is under .data:
messages list/search → .data.messages[], each with PascalCase keys: ChatJID, ChatName, SenderJID, Timestamp, FromMe, Text, DisplayText, MediaType, Snippet.chats list → .data[] with JID, Kind, Name, LastMessageTS.contacts search → .data[] with JID, Phone, Name, Alias, Tags.wacli messages search "magic tags" --limit 20 --json \
| jq '.data.messages[] | {ChatName, FromMe, Timestamp, Text}'
Getting "who said X" wrong is the most common and most damaging wacli mistake. Two traps cause it; both are avoided by the same two rules.
FromMe is the only reliable speaker signal — never infer the sender from the JID or the human output. In a DM both sides come back. Only the account owner is reliably identifiable, via the boolean FromMe. The other party's SenderJID is a raw @lid (e.g. 191624378347690@lid), not a name, and the same person appears under device variants (…@lid, …:19@lid). The human output prints your messages as me and theirs as the bare @lid. Rule: attribute every quote from FromMe — true = the account owner (you), false = the other party. Resolve a @lid to a name with wacli contacts search/show only for labelling, never for deciding direction.
Text is the message's own words; DisplayText/Snippet bundle quoted-reply context — so a search can pin a phrase on the wrong author. When B replies to A, B's DisplayText/Snippet contains A's quoted line, so a substring search matches both A's original and B's reply — making it look like B said A's words. Rule: when deciding who said a phrase, match the Text field only — never DisplayText or Snippet.
Correct attribution recipe — a clean transcript labelled by FromMe, using each message's own Text:
wacli messages list --chat <jid> --limit 200 --json \
| jq -r '.data.messages[] | select(.Text != "")
| (if .FromMe then "ME " else "THEM" end) + " | " + .Timestamp + " | " + .Text'
Build any "who said what" answer from this — not from the human output, not from DisplayText. If a phrase isn't in anyone's own Text (only in quoted replies), it means nobody in the window said it as their own message — don't attribute it, and history backfill if it predates the sync window.
# 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
# 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
wacli messages list --chat <jid> --limit 100 --json
wacli messages list --chat <jid> --after 2026-04-25 --json
Once a search returns a hit, expand the surrounding thread:
wacli messages context --chat <jid> --id <message-id> --before 10 --after 10 --json
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.
Stop any running
wacli syncbefore send commands, or they'll fail on the store lock.
wacli send text --to <phone-or-jid> --message "your text"
# Phone form is auto-converted to JID; omit `+`. E.g. --to 61400000000
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.
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
Search returned a media message ID? Pull the binary:
wacli media download --chat <jid> --id <message-id> --output /tmp/media
Outputs to the configured media dir under the store by default.
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:
pkill -f 'wacli sync' && wacli doctor
wacli messages search "X" --limit 30 --json \
| jq '.data.messages[] | {ChatName, FromMe, Timestamp, Text}'
If a hit looks promising, fetch context:
wacli messages context --chat <jid> --id <message-id> --before 8 --after 8 --json
ALICE=$(wacli contacts search "Alice" --json | jq -r '.data[0].JID')
wacli send text --to "$ALICE" --message "Y: https://..."
GROUP=$(wacli groups list --json | jq -r '.data[] | select(.Name=="Family") | .JID')
wacli messages list --chat "$GROUP" --limit 50 --after $(date -v-7d +%Y-%m-%d) --json
wacli sync --once --idle-exit 15s # foreground catch-up, ~15s
wacli messages list --chat <jid> --limit 5 --json
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)wacli send ... while a wacli sync --follow is up — store is locked. Stop sync first.+ or spaces to --to — strip to digits only (e.g. 61400000000).@lid (linked device alias) — use the @s.whatsapp.net form. wacli contacts show returns the right one.DisplayText/Snippet — only FromMe (direction) + the message's own Text (content) are reliable. See "Attribution" above.wacli history backfill for a specific chat if the user asks about messages older than the sync window.~/.wacli/ — it contains session keys.npx claudepluginhub johnkueh/claude-skills --plugin system-memory-cleanupProvides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.