# Whatsapp

> Read, search, and send WhatsApp messages through the local whatsapp-connector daemon. Use when the user asks to check WhatsApp, read or search a chat/group, summarize messages, reply, or send on WhatsApp. Reads any chat freely; sends ONLY to allow-listed chats and ONLY after the user confirms.

- **Type:** Skill
- **Install:** `agentstack add skill-omerlapidot-whatsapp-connector-whatsapp`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [OmerLapidot](https://agentstack.voostack.com/s/omerlapidot)
- **Installs:** 0
- **Category:** [Communication](https://agentstack.voostack.com/c/communication)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [OmerLapidot](https://github.com/OmerLapidot)
- **Source:** https://github.com/OmerLapidot/whatsapp-connector/tree/main/skills/whatsapp
- **Website:** https://hopala.io/en/blog/whatsapp-into-claude.html

## Install

```sh
agentstack add skill-omerlapidot-whatsapp-connector-whatsapp
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# WhatsApp connector

Drive the user's WhatsApp via the `wa` CLI, which talks to an always-on local daemon.

**Always call it by absolute path:** `{{WA_DIR}}/bin/wa`

## Reading (allowed freely)

- `wa chats` — list chats/groups
- `wa read "" [--limit 30 | --all]` — recent messages (each has an `id` for reactions). `--all` reads the WHOLE chat (every synced message), bypassing the 200 cap — use sparingly: a busy chat can be thousands of messages (large payload + heavy on your context)
- `wa search "" [--chat ""]` — find messages
- `wa members ""` — who is in a group
- `wa contacts ""` — resolve a name/number
- `wa media "" ""` — download an image/file; prints a path you can then open

Chats are addressed by name (fuzzy) or exact id. If `wa` reports the name is ambiguous, show the candidates and ask which one.

## Response shapes (verified against a live account)

Everything prints as JSON (or a plain string). Shapes you can rely on:

- **`wa chats`** → array of `{ id, name, unread }`, **ordered most-recently-active first**.
  The `id` suffix is the chat type:
  - `…@g.us` — a group
  - `…@lid` — an individual person (WhatsApp's current per-contact id). The older
    `…@c.us` form can also appear; treat BOTH as 1:1 people. To filter to real
    people, exclude `@g.us` — do NOT match on `@c.us` alone (modern accounts return
    `@lid` and you'll get zero results).
- **`wa read`** → `{ chat, id, messages: [ … ] }`, oldest→newest. Each message is
  `{ id, sender, ts, text, hasMedia }`:
  - `sender` is the literal string `"me"` for the user's own messages, otherwise the
    counterparty's chat id — so message direction is just `sender === "me"`.
  - `ts` is a UNIX timestamp in **seconds** (multiply by 1000 for a JS `Date`).
  - `--limit` is capped at **200** (MAX_LIMIT); a busier chat returns only its most
    recent 200, so a chat sitting at exactly 200 means "≥200", not exactly 200.
    Pass **`--all`** to bypass the cap and fetch every message — but "every" means
    every message **currently synced** into the Web session, NOT the full lifetime
    history. Depth is volatile: a freshly linked device backfills older messages
    over time, and a **daemon restart flushes the loaded depth** — right after a
    (re)start most chats read only their newest message or two and re-deepen as the
    session re-syncs. So a low `--all` count can mean "not synced yet," not "that's
    the whole chat." A big, warmed chat can return thousands of messages at once.
- **`wa status`** → `{ state, ready, syncPercent }` (plus `recovery: {attempt, max}`
  while self-recovering); `state` ∈ `starting | needs-login | syncing | ready |
  recovering | relinking | auth-failure | disconnected`.
- **Errors** come back as `{ ok:false, error, code }` with codes like
  `NOT_ALLOWED` (chat not on the allow-list), `NOT_APPROVED` (human denied or the
  dialog timed out), `AMBIGUOUS` (name matched >1 chat), `NOT_FOUND`.

## Sending (two-step — read this every time)

`wa send` and `wa send-media` do NOT transmit. They return a `token` + a `preview`. To actually send:
1. Run `wa send "" ""` (or `wa send-media ...`). It returns `{ pending: true, token, chat, preview }` and sends nothing.
2. Show the user the exact `chat` and `preview`, and get an explicit "yes".
3. Only then run `wa send-confirm `. The token expires in ~2 minutes.

`react` is single-step but still restricted to allow-listed chats. `mark-read` is single-step and works on **any** chat — it only changes the user's own read state and sends nothing outward, so it is NOT allow-list gated.

## Allow-list (gated by a native dialog on the user's screen)

- `wa allow list` — chats sends are permitted to. The entry `*` means EVERYONE.
- `wa allow add "" ["" ...]` — request permission for one or more chats
- `wa allow remove "" ["" ...]` — request removal (raw ids accepted)
- `wa allow all` / `wa allow remove all` — the EVERYONE wildcard on / off

Every mutation above triggers a NATIVE DIALOG that only the human can click.
The command blocks up to 60 seconds while the dialog is on screen — this is
normal, wait for it. A denial, timeout, or "not approved" error is FINAL:
do not retry, do not edit allowlist.json, report the outcome to the user.
If a send fails with "not on the send allow-list", offer `wa allow add` —
run it only after the user agrees, and remind them to expect the dialog.

## Scheduled sends

- `wa schedule "" "" --at "HH:MM"` (or `--at "YYYY-MM-DD HH:MM"`, `--in 30m`, `--every monday --at 09:00`, `--every month --on 1 --at 08:00`, `--media `)
- It returns a pending token like `send`; confirm with the user, then `wa schedule-confirm `.
- `wa schedule list` / `wa schedule cancel ` manage jobs. Fires re-check the allow-list.

## If the daemon is down

If `wa` prints "daemon is not running", start it yourself:

    {{WA_DIR}}/bot.sh start

On macOS this registers a launchd agent (`com.whatsapp-connector.daemon`) with
`KeepAlive` + `RunAtLoad`, so the daemon stays up for as long as the Mac is on —
it relaunches automatically after a crash and at every login. On Linux `bot.sh`
falls back to a `nohup` background process with a pidfile. Then poll `wa status`
until `ready: true` (see "Still syncing").

Do NOT run `node index.js` yourself as the long-term daemon — that process dies
with its terminal. `bot.sh start` is what makes it persistent.

**One exception — a fresh, never-linked machine needs a human for the QR.** If
after starting, `wa status` reports `state: "needs-login"` (no WhatsApp session
on disk yet), the login QR renders only to a foreground terminal, which you
cannot scan. Ask the user to run `node index.js` in a terminal from `{{WA_DIR}}`,
scan it via WhatsApp → Settings → Linked Devices, wait for `ready`, then Ctrl+C
and run `{{WA_DIR}}/bot.sh start` for the persistent daemon.

## Still syncing (just after a restart)

`wa status` returns `{ state, ready, syncPercent }`. Right after a (re)start the
daemon reports `state: "syncing"` while it finishes delivering messages that
queued while it was off, and commands fail with "not ready (state: syncing)".
This is normal and self-clears within a few seconds (up to ~60s if a lot
queued). Poll `wa status` until `ready: true`, then proceed — don't tell the
user the daemon is down.

## Auto-recovery (a stuck daemon heals itself)

If WhatsApp Web authenticates but never signals `ready` (an intermittent hang,
usually just after a restart), the daemon now recovers on its own — and shows
the stage in `wa status`:

- `state: "recovering"` with `recovery: {attempt, max}` — it's recycling the
  client, **reusing the on-disk session (no QR)**. Just keep polling `wa status`;
  do NOT restart the daemon yourself while it's recovering.
- `state: "relinking"` — the soft retries were exhausted, so it moved the session
  aside and a **new QR scan is required**. This becomes `needs-login` once the QR
  is ready: tell the user to scan it (see "If the daemon is down" — the QR only
  shows in a foreground `node index.js`).

So a temporary `recovering`/`syncing` status is the daemon fixing itself, not a
failure — report it calmly and wait.

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [OmerLapidot](https://github.com/OmerLapidot)
- **Source:** [OmerLapidot/whatsapp-connector](https://github.com/OmerLapidot/whatsapp-connector)
- **License:** MIT
- **Homepage:** https://hopala.io/en/blog/whatsapp-into-claude.html

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-omerlapidot-whatsapp-connector-whatsapp
- Seller: https://agentstack.voostack.com/s/omerlapidot
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
