From whatsapp-commander
user wants to send a WhatsApp message, share a link or document via WhatsApp, generate a wa.me click-to-chat link, or message a contact on WhatsApp by phone number.
How this skill is triggered — by the user, by Claude, or both
Slash command
/whatsapp-commander:send-messageThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Reach a contact on WhatsApp. Unlike Telegram (`tlg`, full MTProto personal send) and
Reach a contact on WhatsApp. Unlike Telegram (tlg, full MTProto personal send) and
Gmail (gmail-commander, OAuth API send), WhatsApp has no sanctioned personal-account
send API — Meta only offers programmatic send through the Business Cloud API, and
everything else (WhatsApp Web automation, whatsmeow/Baileys/wppconnect libraries)
is unofficial and risks account bans. This skill therefore defaults to the safe,
zero-credential path and escalates only when the user has set up the Cloud API.
Self-Evolving Skill: This skill improves through use. If instructions are wrong, parameters drifted, or a workaround was needed — fix this file immediately, don't defer. Only update for real, reproducible issues.
| Tier | Method | Actually sends? | Credentials | When to use |
|---|---|---|---|---|
| 1 (default) | wa.me click-to-chat link | No — opens WhatsApp with text pre-filled; the human taps Send | None | Almost always. ToS-safe, instant, works for any number. |
| 2 | WhatsApp Web browser automation | Yes, but brittle | A logged-in WhatsApp Web session (QR) | Not implemented here — DOM-fragile and ToS-gray. Document, don't build, unless the user insists. |
| 3 | WhatsApp Business Cloud API | Yes, officially | WHATSAPP_TOKEN + WHATSAPP_PHONE_NUMBER_ID (1Password) | The user has a Meta Business app + registered number. See references/business-cloud-api.md. |
Default to Tier 1. Only use Tier 3 when the user has explicitly set up the Business Cloud API and the credentials resolve (see Preflight).
wa.me link still opens even
if the number is not on WhatsApp (it shows an "invalid number" notice on tap). State
this assumption rather than claiming delivery.[ -n "${WHATSAPP_TOKEN:-}" ] && [ -n "${WHATSAPP_PHONE_NUMBER_ID:-}" ] && echo "cloud-api: READY" || echo "cloud-api: NOT CONFIGURED — use the link tier"
The CLI is function/enum-driven Bun TypeScript — run it directly with bun (no build step).
The link command writes the URL to stdout and its preflight/round-trip proof to
stderr (e.g. ✓ link carries the full 439-char body), so URL=$(bun … link …) still
captures a clean URL. It exits 2 if the body has emoji/astral chars (they would tofu).
/usr/bin/env bash << 'WA_EOF'
SCRIPT="${CLAUDE_PLUGIN_ROOT:-$HOME/.claude/plugins/marketplaces/cc-skills/plugins/whatsapp-commander}/scripts/wa-cli.ts"
# Tier 1 — build a click-to-chat link (the recommended default; BMP-only body, round-trip verified)
bun "$SCRIPT" link "+1 (604) 816-8818" "Hi Iris — short note and a link: https://example.com/x"
# Tier 1 — body read from a file (multi-paragraph messages are awkward inline)
bun "$SCRIPT" link 16048168818 --file ./whatsapp-message.txt
# Tier 3 — actually send via the Business Cloud API (creds resolved into env first)
export WHATSAPP_TOKEN="$(op item get <ITEM> --vault 'Claude Automation' --fields token --reveal)"
export WHATSAPP_PHONE_NUMBER_ID="$(op item get <ITEM> --vault 'Claude Automation' --fields phone_number_id --reveal)"
bun "$SCRIPT" send 16048168818 "Your appointment is confirmed."
WA_EOF
wa.me URL as a Markdown link and tell them tapping it opens the
chat with the text pre-filled — the final Send tap is theirs (WhatsApp does not allow
automating that tap). On desktop they need WhatsApp Web logged in; on phone it deep-links.
—— / over-length) and a
round-trip check, printing ✓ link carries the full N-char body to stderr. Relay that
N as proof of completeness rather than eyeballing.api.whatsapp.com/send gray box is a scrollable PREVIEW, not the message. It
truncates visually (a long or list-heavy body "ends at 1"), which triggers false "it got
cut off" reports. Tell the user to click "Continue to WhatsApp Web" / "Open app" —
the full body drops into the real compose box. Don't assert this blind; the round-trip
✓ line is the actual evidence the link is complete.messages[0].id; report the message id as proof
of submission (subject to the 24-hour window / template rules — see references).The Business Cloud API does not let you send arbitrary text to anyone at any time:
type: "text" messages are allowed. wa-cli.ts send sends exactly this.type: "template"), not free text. A free-text send
outside the window returns error 131047. For first contact, prefer Tier 1, or send an
approved template via the raw Graph API (see references).wa.me bodies emoji-free (BMP only)Do not put emoji or any other astral-plane (> U+FFFF) character in a Tier-1 wa.me link
body. They arrive on the recipient's device as � (U+FFFD replacement char / a
diamond-? tofu) — even though the source file and the percent-encoded URL are both
perfectly valid UTF-8. The wa.me → WhatsApp deep-link handoff mangles 4-byte codepoints
(surrogate pairs), while Basic-Multilingual-Plane text — including all CJK — survives
untouched. That asymmetry (Chinese renders fine, emoji become �) is the diagnostic tell:
it is NOT a source-encoding bug, so don't go re-saving the file as UTF-8 — the bytes are
already correct.
Verified 2026-07-04 (CPC ↔ Iris zh-Hans report): all Chinese rendered correctly but
👋 ✅ ❌ and keycap sequences (1️⃣) showed as � in WhatsApp. Source bytes were valid
(👋 = f0 9f 91 8b; the URL held %F0%9F%91%8B). Removing the emoji fixed it entirely.
Substitute BMP markers: headings 【…】, bullets -, numbering 1) / 2), quotes
「…」, arrows —. All ≤ U+FFFF, render everywhere.
Guard before building a link — fail if any codepoint is astral or an emoji selector:
python3 - "$MSG_FILE" <<'PY'
import sys
t = open(sys.argv[1], encoding="utf-8").read()
bad = sorted({hex(ord(c)) for c in t
if ord(c) > 0xFFFF or ord(c) in (0xFE0F, 0x20E3)
or 0x1F000 <= ord(c) or 0x2600 <= ord(c) <= 0x27BF})
print("ASTRAL/EMOJI PRESENT — will tofu to ⬜/�:", bad) if bad else print("BMP-only: safe")
PY
Tier 3 Cloud API send transmits real UTF-8 in the POST body and does not have this
problem — the mangling is specific to the Tier-1 click-to-chat deep link.
The
linkcommand now enforces the emoji check itself (hard error, exit 2) and prints a round-trip✓ link carries the full N-char bodyline — so you rarely need to run the guard above by hand, but it's kept for quick pre-checks on a draft file.
The landing-page preview is a fixed, scrollable box. Long or list-heavy bodies scroll out of view and look truncated, which reads as a bug to the recipient/operator even though the link carries everything. Two of the three round-trips in the CPC ↔ Iris case (2026-07-04) were this false alarm, not real defects. So, for Tier-1:
(1) … (2) … inside a paragraph — do NOT start a line with a bare
1) / 1.. A line that is just 1 is the classic "message got cut at 1" preview artifact.—— flowing into a numbered item — it renders as a dangling
-- 1. Use a colon lead-in (…问两件事:) instead.| Anti-Pattern | Why It Fails |
|---|---|
Claiming a wa.me link "sent" the message | It only pre-fills; the human taps Send. Say so. |
Using an unofficial library (Baileys, whatsmeow, wppconnect) to send as a personal account | Violates WhatsApp ToS; risks number ban. Not part of this skill. |
| Passing a number without a country code | wa.me needs full E.164; the CLI rejects <8 digits. |
| Free-text Cloud API send to a cold contact | Outside the 24h window only approved templates send (error 131047). |
| Hardcoding the Cloud API token in a script/commit | Resolve from 1Password into env at call time (mirrors gmail-commander/tlg). |
Emoji / astral-plane chars (👋✅❌, keycaps) in a wa.me link body | Arrive as � (U+FFFD) — the deep-link handoff drops 4-byte codepoints; BMP/CJK survive. Keep Tier-1 bodies BMP-only. |
| Symptom | Cause | Fix |
|---|---|---|
CLI: too few digits | Number missing country code | Pass full international format |
CLI: Cloud API send needs WHATSAPP_TOKEN… | Tier 3 creds absent | Use Tier 1 link, or resolve creds via 1Password |
API Cloud API HTTP 401 | Token expired/invalid | Rotate the token in Meta → update 1Password |
API error 131047 | Outside 24h window | Send an approved template, or use Tier 1 |
wa.me opens but shows "invalid number" | Recipient not on WhatsApp / wrong number | Confirm the number with the user |
Recipient sees � / diamond-? where emoji were | Astral-plane emoji lost in the wa.me→WhatsApp deep-link decode (source UTF-8 fine; CJK survives) | Strip emoji from the body; use BMP markers (【】, -, 1)). See "Message Body Encoding". |
| User reports the message "ends at 1" / looks cut off | The api.whatsapp.com/send gray box is a scrollable preview, not the message; long/list-y bodies scroll out of view | Not a bug — the round-trip ✓ carries N chars proves completeness. Tell them to click "Continue to WhatsApp Web"; keep bodies short & inline (1)(2). |
2026-07-04 (later) — hardened the Tier-1 path against the two failure modes above. Added
to wa-cli.ts link: (1) an emoji/astral preflight (hard error, exit 2) so a tofu body is
never emitted; (2) structural warnings for lone list-number lines / trailing —— /
over-length that make the preview look truncated; (3) a round-trip check that decodes
the built URL and asserts it equals the input, printing ✓ link carries the full N-char body. Lesson baked in: on Tier-1 the agent has no view of the rendered result, so verify
what's verifiable (round-trip) and never assert preview behavior blind. Cost that motivated
this: one CPC ↔ Iris message took three user round-trips (emoji → dangling dash → false
"truncated" preview) — all now caught before hand-off.
2026-07-04 — emoji tofu on the Tier-1 link path. A zh-Hans report (CPC ↔ Iris) rendered
all Chinese correctly but showed 👋 ✅ ❌ / keycaps as � in WhatsApp. Root cause: the
wa.me → WhatsApp deep-link handoff drops astral-plane (4-byte) codepoints while BMP text,
including CJK, survives — the source file and percent-encoded URL were both valid UTF-8.
Fix + guard documented in the new "Message Body Encoding" section: keep Tier-1 bodies BMP-only.
2026-06-22 — initial skill. WhatsApp has no personal-account send API. Modelled on
tlg/gmail-commander conventions but split into capability tiers because the platform
forbids the equivalent of MTProto/OAuth personal send. Default = wa.me link (the path
proven in the VanJobbers ↔ CPC engagement); Cloud API send wired but credential-gated.
CLI written as function/enum-driven Bun TypeScript per the repo language preference.
After this skill completes, check before closing:
wa-cli.ts flags or output change? — Update the Usage block to match.Only update if the issue is real and reproducible — not speculative.
Guides collaborative design exploration before implementation: explores context, asks clarifying questions, proposes approaches, and writes a design doc for user approval.
Creates structured, bite-sized implementation plans from specs or requirements before writing code. Useful for breaking down multi-step tasks into testable steps with file structure and task boundaries.
Synthesizes the current conversation into a structured spec (PRD) and publishes it to the project issue tracker with a ready-for-agent label, without interviewing the user.
npx claudepluginhub terrylica/cc-skills --plugin whatsapp-commander