greentap — WhatsApp Web CLI via Playwright
Node.js CLI that drives WhatsApp Web via Playwright aria snapshots.
Uses a background browser daemon for fast (~500ms) command execution.
Prerequisites
- Node.js (v18+); bundled Chromium auto-installed via
npx playwright install chromium
- First run:
greentap login to scan QR code
Commands
# Open the browser for QR scan and login
greentap login
# Identity of the currently logged-in WA account
greentap whoami --json
# List visible chats with last message and unread count
greentap chats --json
# List only chats with unread messages
greentap unread --json
# Read messages from a chat (exact normalized name match)
# Each message includes: sender, time, text, body, quoted_sender, quoted_text,
# kind, imageId (when image), timestamp; default reads also include links[]
# Outbound messages: sender === "You" (no separate is_self flag)
greentap read "contact or group name" --json
# Read full chat history (scrolls up, deduplicates)
greentap read "contact or group name" --scroll --json
# If multiple chats share the same name, use --index N (1-based) to pick one
greentap read "contact or group name" --index 2 --json
greentap send "contact or group name" --index 2 "message text"
# Search for a contact or group (finds archived chats too)
greentap search "query" --json
# Send a message (finds chat by name, types and sends)
# Multi-line messages: use real \n in the string — they are sent as one bubble
greentap send "contact or group name" "line one
line two"
# Read poll results from a chat (most recent poll, with vote counts per option)
greentap poll-results "contact or group name" --json
greentap poll-results "contact or group name" --index 2 --json
# Download recently-visible images from a chat to ~/.greentap/downloads/<chat-slug>/
greentap fetch-images "contact or group name" --limit 3 --json
# --scroll materializes more image rows into the DOM first (still bounded by
# --limit and WhatsApp's blob lifetime — scrolled-off blobs may be unfetchable).
# --index N disambiguates duplicate chat names.
# An invalid --limit (0, negative, non-numeric) warns on stderr and falls back to the default (20).
greentap fetch-images "contact or group name" --limit 5 --scroll --index 2 --json
# E2E roundtrip verification against the dedicated greentap-sandbox group
GREENTAP_E2E=1 greentap e2e
# Daemon management
greentap status
greentap daemon stop
# Clear session data (forces re-login on next run)
greentap logout
# Debug: dump raw aria snapshot (full|chats|messages|compose)
greentap snapshot full
greentap snapshot messages --chat "contact or group name"
Important behavior
- Daemon: first command auto-launches a background Chromium instance (port 19222), shuts down after 15min idle. Bundled Chromium with
HeadlessChrome UA stripped so WhatsApp Web accepts it.
- read shows messages visible in the viewport by default; use
--scroll for full history.
- read marks messages as read in WhatsApp (cannot be prevented).
- send verifies correct chat opened and message delivered. Multi-line strings (with
\n) become one WA bubble (uses Shift+Enter internally).
- poll-results navigates and reads the most recent WhatsApp native poll.
- fetch-images writes JPEG/PNG/WebP files to
~/.greentap/downloads/<chat-slug>/<imageId>.<ext> with mode 0o600. Returns absolute paths so the agent can Read them for multimodal understanding.
- whoami returns
{ name, phone } for the currently logged-in account. Either field can be null if WhatsApp Web doesn't expose it on this session.
- e2e runs five ordered stages (modal, preflight, text, image, link) against the sandbox group
greentap-sandbox (must exist; only the maintainer is a member). Output is structural JSON — no message content logged.
- If multiple chats share the same name, commands error with a numbered list — re-run with
--index N to pick one.
- Chat-targeting commands require an exact normalized name. If no exact match
exists, the error shows partial-name suggestions.
- Locale-agnostic: works with any WhatsApp UI language.
Read output schema
Each message in read --json carries these fields unless noted otherwise
(additive over time — older clients can ignore unknown fields):
| Field |
Type |
Notes |
sender |
string |
Always populated. "You" for outbound; "(unknown)" if unattributable. Never empty string. |
time |
string |
"HH:MM" when detected; empty string when unavailable. |
timestamp |
string | null |
"YYYY-MM-DD HH:MM" (space-separated) when WA shows a date separator; null otherwise. Stable across midnight (uses snapshot read-time, not parse-time). |
text |
string |
Full visible text, including any quoted-reply bleed (backward compat). |
body |
string |
The message's own text with the quoted block removed. Equal to text when no quote. |
quoted_sender |
string | null |
New: author of the quoted message when this is a reply-with-quote. |
quoted_text |
string | null |
New: text of the quoted message. |
links |
[{href, text}] |
http(s) URLs recovered from the DOM on default reads. Omitted with --scroll. |
kind |
string |
"text", "image", or "video" (more kinds may appear in future versions). |
imageId |
string |
Only on kind: "image". Stable across reads of the same DOM. Use with fetch-images to materialize. |
Outbound messages are identified by sender === "You" — there is no separate boolean is_self field. Pair with whoami to map "You" to a real account.
Multimodal flow for images
$ greentap fetch-images "Famiglia Rossi" --limit 3 --json
[
{ "imageId": "a7f3c211", "path": "/Users/<you>/.greentap/downloads/famiglia-rossi/a7f3c211.jpg",
"sender": "Elena Conti", "time": "14:22", "mimeType": "image/jpeg" }
]
After receiving the path: open with the Read tool — the image is handed to Claude as multimodal input. Describe, OCR, or reason about it directly. Delete the file when no longer needed (cache is not auto-pruned).
Limitations:
- Thumbnail resolution only (full-resolution viewer download is a future enhancement)
- Does not fetch images outside the currently-rendered DOM — scroll or re-read first
Guidelines for the agent
- Use
--json for parsing, plain text when showing to the user.
- NEVER send a message without explicit user confirmation.
- When drafting messages, match the language the user typically uses with that contact.
- If asked to "check messages", start with
greentap unread --json.
- If a chat isn't found, try
greentap search with a shorter query.
- If asked about poll results or a vote, use
greentap poll-results.
- For image content questions, use
fetch-images then Read the path.
- For full URLs (link previews), use
read --json and inspect the links[] array — the visible text is often a truncated preview.
- For "who am I" / outbound identification:
whoami.
- Keep tool calls minimal — one
unread or read call is usually enough.
- First command auto-starts daemon (~6s cold start), subsequent ones are ~500ms.
1---2name: greentap3description: Read and send messages from WhatsApp Web via Playwright and aria snapshots. Use when the user wants to: - Check unread messages or chat list - Read messages from a specific chat - Search for a contact or group - Send a message (single- or multi-line) to a contact or group - Draft a message for review before sending - Read poll results from a chat - Download images from a chat - Recover full URLs from link previews - Identify which WhatsApp account is currently in use Triggers: "whatsapp", "check messages", "read chat", "send message to", "unread messages", "message from", "greentap", "poll results", "sondaggio", "download image", "fetch image", "whoami", "who am i", "link preview"4license: MIT5---67# greentap — WhatsApp Web CLI via Playwright89Node.js CLI that drives WhatsApp Web via Playwright aria snapshots.10Uses a background browser daemon for fast (~500ms) command execution.1112## Prerequisites1314- Node.js (v18+); bundled Chromium auto-installed via `npx playwright install chromium`15- First run: `greentap login` to scan QR code1617## Commands1819```bash20# Open the browser for QR scan and login21greentap login2223# Identity of the currently logged-in WA account24greentap whoami --json2526# List visible chats with last message and unread count27greentap chats --json2829# List only chats with unread messages30greentap unread --json3132# Read messages from a chat (exact normalized name match)33# Each message includes: sender, time, text, body, quoted_sender, quoted_text,34# kind, imageId (when image), timestamp; default reads also include links[]35# Outbound messages: sender === "You" (no separate is_self flag)36greentap read "contact or group name" --json3738# Read full chat history (scrolls up, deduplicates)39greentap read "contact or group name" --scroll --json4041# If multiple chats share the same name, use --index N (1-based) to pick one42greentap read "contact or group name" --index 2 --json43greentap send "contact or group name" --index 2 "message text"4445# Search for a contact or group (finds archived chats too)46greentap search "query" --json4748# Send a message (finds chat by name, types and sends)49# Multi-line messages: use real \n in the string — they are sent as one bubble50greentap send "contact or group name" "line one51line two"5253# Read poll results from a chat (most recent poll, with vote counts per option)54greentap poll-results "contact or group name" --json55greentap poll-results "contact or group name" --index 2 --json5657# Download recently-visible images from a chat to ~/.greentap/downloads/<chat-slug>/58greentap fetch-images "contact or group name" --limit 3 --json59# --scroll materializes more image rows into the DOM first (still bounded by60# --limit and WhatsApp's blob lifetime — scrolled-off blobs may be unfetchable).61# --index N disambiguates duplicate chat names.62# An invalid --limit (0, negative, non-numeric) warns on stderr and falls back to the default (20).63greentap fetch-images "contact or group name" --limit 5 --scroll --index 2 --json6465# E2E roundtrip verification against the dedicated greentap-sandbox group66GREENTAP_E2E=1 greentap e2e6768# Daemon management69greentap status70greentap daemon stop7172# Clear session data (forces re-login on next run)73greentap logout7475# Debug: dump raw aria snapshot (full|chats|messages|compose)76greentap snapshot full77greentap snapshot messages --chat "contact or group name"78```7980## Important behavior8182- **Daemon**: first command auto-launches a background Chromium instance (port 19222), shuts down after 15min idle. Bundled Chromium with `HeadlessChrome` UA stripped so WhatsApp Web accepts it.83- **read** shows messages visible in the viewport by default; use `--scroll` for full history.84- **read** marks messages as read in WhatsApp (cannot be prevented).85- **send** verifies correct chat opened and message delivered. Multi-line strings (with `\n`) become one WA bubble (uses Shift+Enter internally).86- **poll-results** navigates and reads the most recent WhatsApp native poll.87- **fetch-images** writes JPEG/PNG/WebP files to `~/.greentap/downloads/<chat-slug>/<imageId>.<ext>` with mode 0o600. Returns absolute paths so the agent can `Read` them for multimodal understanding.88- **whoami** returns `{ name, phone }` for the currently logged-in account. Either field can be `null` if WhatsApp Web doesn't expose it on this session.89- **e2e** runs five ordered stages (modal, preflight, text, image, link) against the sandbox group `greentap-sandbox` (must exist; only the maintainer is a member). Output is structural JSON — no message content logged.90- If multiple chats share the same name, commands error with a numbered list — re-run with `--index N` to pick one.91- Chat-targeting commands require an exact normalized name. If no exact match92 exists, the error shows partial-name suggestions.93- Locale-agnostic: works with any WhatsApp UI language.9495## Read output schema9697Each message in `read --json` carries these fields unless noted otherwise98(additive over time — older clients can ignore unknown fields):99100| Field | Type | Notes |101|-------|------|-------|102| `sender` | string | Always populated. `"You"` for outbound; `"(unknown)"` if unattributable. Never empty string. |103| `time` | string | `"HH:MM"` when detected; empty string when unavailable. |104| `timestamp` | string \| null | `"YYYY-MM-DD HH:MM"` (space-separated) when WA shows a date separator; `null` otherwise. Stable across midnight (uses snapshot read-time, not parse-time). |105| `text` | string | Full visible text, including any quoted-reply bleed (backward compat). |106| `body` | string | The message's own text with the quoted block removed. Equal to `text` when no quote. |107| `quoted_sender` | string \| null | New: author of the quoted message when this is a reply-with-quote. |108| `quoted_text` | string \| null | New: text of the quoted message. |109| `links` | `[{href, text}]` | http(s) URLs recovered from the DOM on default reads. Omitted with `--scroll`. |110| `kind` | string | `"text"`, `"image"`, or `"video"` (more kinds may appear in future versions). |111| `imageId` | string | Only on `kind: "image"`. Stable across reads of the same DOM. Use with `fetch-images` to materialize. |112113Outbound messages are identified by `sender === "You"` — there is no separate boolean `is_self` field. Pair with `whoami` to map `"You"` to a real account.114115## Multimodal flow for images116117```118$ greentap fetch-images "Famiglia Rossi" --limit 3 --json119[120 { "imageId": "a7f3c211", "path": "/Users/<you>/.greentap/downloads/famiglia-rossi/a7f3c211.jpg",121 "sender": "Elena Conti", "time": "14:22", "mimeType": "image/jpeg" }122]123```124125After receiving the path: open with the Read tool — the image is handed to Claude as multimodal input. Describe, OCR, or reason about it directly. Delete the file when no longer needed (cache is not auto-pruned).126127Limitations:128- Thumbnail resolution only (full-resolution viewer download is a future enhancement)129- Does not fetch images outside the currently-rendered DOM — scroll or re-read first130131## Guidelines for the agent132133- Use `--json` for parsing, plain text when showing to the user.134- **NEVER send a message without explicit user confirmation.**135- When drafting messages, match the language the user typically uses with that contact.136- If asked to "check messages", start with `greentap unread --json`.137- If a chat isn't found, try `greentap search` with a shorter query.138- If asked about poll results or a vote, use `greentap poll-results`.139- For image content questions, use `fetch-images` then `Read` the path.140- For full URLs (link previews), use `read --json` and inspect the `links[]` array — the visible `text` is often a truncated preview.141- For "who am I" / outbound identification: `whoami`.142- Keep tool calls minimal — one `unread` or `read` call is usually enough.143- First command auto-starts daemon (~6s cold start), subsequent ones are ~500ms.