Stand up WhatsApp on a new machine and wire the `whatsapp` MCP into both Claude clients — the WhatsApp add-on alongside mcp-bridge-setup. It builds the external whatsapp-mcp repo's TWO processes (the Go whatsmeow bridge, run as a launchd SIDECAR like search/guard on $WHATSAPP_GO_PORT/:8010; the Python stdio MCP, exposed to Claude Code via a supergateway bridge on $WHATSAPP_MCP_BRIDGE_PORT/:8006 and to Cowork as a direct stdio command), does the one-time QR pairing against your phone's Linked ...
4 stars
0 votes
0 copies
2 views
Added September 2, 2026
ai-agentspythonrustgoshellsqlnodegitapibackend
Works with
claude code
claude desktop
cursor
terminal
cli
api
mcp
Security analysis
D55/100
criticalModifies startup scripts or system services for persistence
criticalSends environment variables or credentials to an external URL
mediumInstalls packages at runtime which could introduce malicious dependencies
Installs into .claude/skills of the current project.
Are you the author of Whatsapp Mcp Setup?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/philipyaz-whatsapp-mcp-setup)
---
name: whatsapp-mcp-setup
description: Stand up WhatsApp on a new machine and wire the `whatsapp` MCP into both Claude clients — the WhatsApp add-on alongside mcp-bridge-setup. It builds the external whatsapp-mcp repo's TWO processes (the Go whatsmeow bridge, run as a launchd SIDECAR like search/guard on $WHATSAPP_GO_PORT/:8010; the Python stdio MCP, exposed to Claude Code via a supergateway bridge on $WHATSAPP_MCP_BRIDGE_PORT/:8006 and to Cowork as a direct stdio command), does the one-time QR pairing against your phone's Linked Devices, and registers `whatsapp` in `.mcp.json` + the Cowork config. Use when setting up WhatsApp on a new machine, when Cowork or Code can't see the `whatsapp` server, when the Go bridge is down, when /whatsapp-triage can't reach WhatsApp, or when the linked device was removed / the session expired and you must re-pair the QR.
---
# WhatsApp MCP setup (Go bridge sidecar :8010 + Python stdio MCP bridge :8006)
## Why this exists / architecture
`whatsapp-mcp` is an **external repo** (the user's fork
[`verygoodplugins/whatsapp-mcp`](https://github.com/verygoodplugins/whatsapp-mcp)) checked out
**outside** this Cos repo at **`$WHATSAPP_MCP_DIR`**. It is **two processes**, and they register
into Cos the same two ways the existing servers + sidecars do — getting this split wrong is the
usual failure:
- **(a) The Go whatsmeow bridge (`:8010`)** — `whatsapp-bridge/`, a long-running HTTP daemon that
holds the WhatsApp Web session and writes the SQLite stores. It is **treated exactly like the
search / guard SIDECARS**: an HTTP backend, **NOT** a stdio MCP server, so it is **NOT in
`.mcp.json`**. launchd supervises it under label **`com.chiefofstaff.mcp-whatsappbridge`**. (Its
port is `$WHATSAPP_GO_PORT`, default **8010** — whatsmeow's own default is `8080`, but that is
commonly taken on a dev box, so cos pins it to 8010.)
- **(b) The Python stdio MCP (`whatsapp-mcp-server/main.py`)** — a FastMCP **stdio** server
(`mcp.run(transport="stdio")`, confirmed at `main.py:396`). It is exposed to **Claude Code** over
HTTP via a **supergateway + launchd BRIDGE on `:8006`** (`$WHATSAPP_MCP_BRIDGE_PORT`, label
**`com.chiefofstaff.mcp-whatsapp`**, IN `.mcp.json`), and to **Claude Cowork Desktop** as a
**direct stdio `command` entry** — identical to how mcp-bridge-setup wires board/openwhispr/etc.
```
Claude Code ──HTTP──> localhost:8006/mcp ──supergateway(launchd)──> uv run main.py (Python stdio MCP)
│ reads SQLite directly (messages.db)
└──HTTP (bearer)──> Go bridge :8010 ──whatsmeow──> WhatsApp Web
Cowork Desktop ──spawns stdio directly──> uv run --directory $WHATSAPP_MCP_DIR/whatsapp-mcp-server main.py (§6)
Go bridge :8010 = launchd SIDECAR (com.chiefofstaff.mcp-whatsappbridge), NOT in .mcp.json (§4)
```
The Python MCP reads **`messages.db` directly** for all the READ tools, so **board-only triage
tolerates the Go bridge being down** — but a **fresh pairing or any send** needs the bridge live
(see Gotchas). The data flow mirrors the upstream contract: *AI client → Python MCP → reads SQLite
**or** calls the bridge REST (`$WHATSAPP_API_URL`) → Go bridge → WhatsApp Web.*
This add-on's processes (alongside mcp-bridge-setup's four core servers + two sidecars, and the
optional `openwhispr` voice add-on):
| process | what runs | env | port | launchd label | in `.mcp.json`? |
|---|---|---|---|---|---|
| whatsappbridge (sidecar) | `whatsapp-bridge/whatsapp-bridge` (Go) | `WHATSAPP_BRIDGE_PORT` | 8010 | `com.chiefofstaff.mcp-whatsappbridge` | **no** (HTTP backend) |
| whatsapp (MCP bridge) | `uv run --directory …/whatsapp-mcp-server main.py` via supergateway | `WHATSAPP_DB_PATH` / `WHATSMEOW_DB_PATH` / `WHATSAPP_API_URL` | 8006 | `com.chiefofstaff.mcp-whatsapp` | **yes** (`http://localhost:8006/mcp`) |
> **The bearer token is read from a file, not the env.** The Python server resolves the Go bridge's
> bearer token from `$WHATSAPP_MCP_DIR/whatsapp-bridge/store/.bridge-token` (computed relative to its
> own `__file__`), so **neither** the plist **nor** the Cowork entry needs to carry it — the live
> secret never lands in `~/Library/LaunchAgents`, and a token rotation needs no re-render.
> **Board-only.** Cos's `/whatsapp-triage` uses ONLY the **READ** subset of this MCP
> (`search_contacts`, `get_contact`, `list_messages`, `list_chats`, `get_chat`,
> `get_direct_chat_by_contact`, `get_contact_chats`, `get_last_interaction`,
> `get_message_context`, `download_media`) — it reconciles WhatsApp onto the Cos board exactly like
> mail-to-board reconciles Gmail, and **never** calls `send_message` / `send_file` /
> `send_audio_message`. This setup skill stands up the plumbing; triage owns the board side.
> Machine config comes from the loader (run the preamble in §1): it exports `$REPO_ROOT`,
> `$BREW_PREFIX`, `$NODE_BIN`, `$UV_BIN`, `$SUPERGATEWAY_BIN`, `$LAUNCH_AGENTS_DIR`,
> `$COWORK_CONFIG`, and the **new WhatsApp keys** — `$WHATSAPP_MCP_DIR` (the external checkout),
> `$WHATSAPP_MCP_BRIDGE_PORT` (=`8006`), `$WHATSAPP_GO_PORT` (=`8010`), and the derived
> `$WHATSAPP_MCP_BRIDGE_URL` (=`http://localhost:8006`) / `$WHATSAPP_GO_URL`
> (=`http://localhost:8010`) — use those instead of hardcoding paths, the Homebrew prefix, your
> username, or ports.
## Prerequisites
- **`go`** (Homebrew: `brew install go`) — builds the whatsmeow bridge (`go build`). Required; the
bridge is a compiled binary, there is no prebuilt one.
- **`uv`** (Homebrew: `brew install uv`) — runs the Python MCP server and provisions its venv
(`uv sync`).
- **`supergateway`** (`npm install -g supergateway`) — the stdio→HTTP bridge for **Claude Code**
(`:8006`), exactly as in mcp-bridge-setup.
- **`ffmpeg`** (optional; `brew install ffmpeg`) — only for the MCP's `send_audio_message`
voice-note conversion. **Triage never sends**, so this is not needed for Cos; install only if you
want outbound voice notes from other workflows.
- A **phone running WhatsApp** for the one-time QR pairing (§3) — the analog of guard's gated-model
HuggingFace login.
Run the loader preamble as the first line of every shell block below — it exports `$REPO_ROOT`,
`$WHATSAPP_MCP_DIR`, `$UV_BIN`, `$NODE_BIN`, `$BREW_PREFIX`, `$LAUNCH_AGENTS_DIR`, `$COWORK_CONFIG`,
the WhatsApp ports/URLs, etc., so nothing below is hardcoded. `$U=$(id -u)` is derived inline where
`launchctl` needs it.
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
```
## Steps
### 1. Get the source (clone the fork, or point at an existing checkout)
The whatsapp-mcp repo lives **outside** this Cos checkout; `$WHATSAPP_MCP_DIR` (from `cos.env`) is
its path. Clone the user's fork there, or set `WHATSAPP_MCP_DIR` to a checkout you already have.
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
if [ -d "$WHATSAPP_MCP_DIR/.git" ]; then
echo "whatsapp-mcp already at $WHATSAPP_MCP_DIR"
git -C "$WHATSAPP_MCP_DIR" remote -v | grep origin # origin should be verygoodplugins/whatsapp-mcp (the fork)
else
git clone https://github.com/verygoodplugins/whatsapp-mcp "$WHATSAPP_MCP_DIR"
fi
```
> `$WHATSAPP_MCP_DIR` is a **`cos.env` key** (e.g. `~/Code/whatsapp-mcp`); set it there once. The
> `origin` remote is the **fork** `verygoodplugins/whatsapp-mcp`, not the upstream
> `lharries/whatsapp-mcp` — PRs/issues against this checkout target the fork.
- **CHECKPOINT** — the two components are present:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
ls "$WHATSAPP_MCP_DIR/whatsapp-bridge/main.go" "$WHATSAPP_MCP_DIR/whatsapp-mcp-server/main.py" \
&& echo "source OK"
```
### 2. Build the bridge + install the server's deps
The Go bridge is compiled to a binary; the Python server's venv is provisioned by `uv`.
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
( cd "$WHATSAPP_MCP_DIR/whatsapp-bridge" && go build -o whatsapp-bridge . ) # produces ./whatsapp-bridge
( cd "$WHATSAPP_MCP_DIR/whatsapp-mcp-server" && "$UV_BIN" sync ) # builds the venv from pyproject
```
- **CHECKPOINT** — the binary exists and the server imports:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
test -x "$WHATSAPP_MCP_DIR/whatsapp-bridge/whatsapp-bridge" && echo "bridge binary OK"
"$UV_BIN" run --directory "$WHATSAPP_MCP_DIR/whatsapp-mcp-server" python -c "import main; print('server import OK')"
```
### 3. First-run QR pairing (HUMAN-IN-THE-LOOP — the one-time external auth)
Like guard's gated-model HuggingFace login, the bridge needs a **one-time interactive
authorization**: it prints a **QR code** you scan with your phone to link this machine as a
WhatsApp **device**. Run the bridge **by hand once** (foreground, NOT under launchd yet), on the
cos port so it doesn't collide with whatever holds `:8080`:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
cd "$WHATSAPP_MCP_DIR/whatsapp-bridge"
WHATSAPP_BRIDGE_PORT="$WHATSAPP_GO_PORT" ./whatsapp-bridge # prints a QR code in the terminal; leave it running
```
On your phone: **WhatsApp → Settings → Linked Devices → Link a Device**, then scan the QR. On
success the bridge logs **`Connected to WhatsApp`** and begins history backfill, and it creates,
under `whatsapp-bridge/store/` (all **gitignored** in the whatsapp-mcp repo — real chat history +
PII):
- `whatsapp.db` — the whatsmeow session/contacts/LID map (`$WHATSMEOW_DB_PATH`),
- `messages.db` — the bridge's chats + messages, the **read source of truth** for the MCP
(`$WHATSAPP_DB_PATH`),
- `.bridge-token` — the **bearer token** every REST call to the Go bridge must present (the Python
server reads this file itself; you don't pass it anywhere).
Once you see `Connected to WhatsApp` and the stores exist, **Ctrl-C** — launchd will own the
process from §4.
> **History backfill is controlled by the phone** (the primary device), not this machine — the
> bridge can request more (it supports a `--full-history-pair` flag) but the phone has the final
> word. A freshly paired machine may show only recent history at first; triage's per-chat cursor
> simply starts from what `messages.db` has.
- **CHECKPOINT** — the three store files exist:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
ls "$WHATSAPP_MCP_DIR/whatsapp-bridge/store/whatsapp.db" \
"$WHATSAPP_MCP_DIR/whatsapp-bridge/store/messages.db" \
"$WHATSAPP_MCP_DIR/whatsapp-bridge/store/.bridge-token" && echo "store OK"
```
### 4. Install both LaunchAgents (Go bridge SIDECAR `:8010` + Python MCP bridge `:8006`)
The Go bridge is an HTTP daemon (like search/guard sidecars), so it gets a LaunchAgent but **no
`.mcp.json` entry**; the Python MCP is bridged to HTTP for Claude Code via a supergateway LaunchAgent
on `:8006`. Both plists are generated from their descriptors
(`mcp/whatsapp/whatsapp.service.json` + `whatsappbridge.service.json`) by `scripts/gen-launchd.mjs`
(see `mcp/CLAUDE.md`) — which on macOS renders the plist AND `bootout→bootstrap→kickstart`s it in one
step. Name both add-on services explicitly (they aren't core):
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
node "$REPO_ROOT/scripts/gen-launchd.mjs" --install whatsapp whatsappbridge
```
The `whatsappbridge` descriptor runs the built `whatsapp-bridge` binary with
`WHATSAPP_BRIDGE_PORT=$WHATSAPP_GO_PORT`, `WorkingDirectory` = `$WHATSAPP_MCP_DIR/whatsapp-bridge`
(so it finds `store/`), `RunAtLoad` + `KeepAlive`, a `PATH` that starts with `$BREW_PREFIX/bin`, and
logs to `$REPO_ROOT/mcp/logs/whatsappbridge.{out,err}.log`.
> **Probe leniently — it is bearer-token protected.** The REST API requires the `.bridge-token`
> bearer, so an **unauthenticated** `curl` returns **401, not 200** — a bare 200 is NOT the health
> signal. The signal is: **the port is listening** AND the log says **`Connected to WhatsApp`** (the
> session is live). Don't treat the 401 as a failure.
- **CHECKPOINT** — port up + session connected:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
lsof -nP -iTCP:"$WHATSAPP_GO_PORT" -sTCP:LISTEN >/dev/null 2>&1 && echo "bridge listening on :$WHATSAPP_GO_PORT"
grep -q "Connected to WhatsApp" "$REPO_ROOT/mcp/logs/whatsappbridge.out.log" 2>/dev/null && echo "session connected"
# An authenticated probe (optional) — should NOT be 401 with the token:
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $(cat "$WHATSAPP_MCP_DIR/whatsapp-bridge/store/.bridge-token")" \
"$WHATSAPP_GO_URL/api/chats" ; echo
```
### 5. The Python MCP supergateway BRIDGE (`:8006`)
The stdio MCP is bridged to HTTP for Claude Code exactly like mcp-bridge-setup's servers — one
supergateway LaunchAgent on `:8006`, generated from `mcp/whatsapp/whatsapp.service.json` and
**already installed by the `gen-launchd.mjs --install whatsapp whatsappbridge` run in §4**. The
generated plist's `EnvironmentVariables` carry the **three** vars the Python server reads to find its
stores + the Go bridge (all absolute / explicit):
- `WHATSAPP_DB_PATH` = `$WHATSAPP_MCP_DIR/whatsapp-bridge/store/messages.db`,
- `WHATSMEOW_DB_PATH` = `$WHATSAPP_MCP_DIR/whatsapp-bridge/store/whatsapp.db`,
- `WHATSAPP_API_URL` = **`http://localhost:$WHATSAPP_GO_PORT/api`** (pinned to the Go bridge — see the Gotcha).
The bearer token is **not** in the plist — the server reads `store/.bridge-token` itself.
> **Do NOT set `WHATSAPP_BRIDGE_PORT` in this plist.** It is read by the **Go** bridge only (the
> port it binds); the Python MCP ignores it and reaches the Go bridge solely via `WHATSAPP_API_URL`.
> Setting it here would be a silent no-op. The supergateway bridge port (8006) is named
> **`WHATSAPP_MCP_BRIDGE_PORT`** in `cos.env` and passed to supergateway's `--port`. (See Gotchas.)
- **CHECKPOINT** — an MCP `initialize` on `:8006` returns `serverInfo.name == "whatsapp"`:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
curl -s -X POST "$WHATSAPP_MCP_BRIDGE_URL/mcp" \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"c","version":"0"}}}' \
| grep -o '"name":"whatsapp"' && echo "whatsapp MCP bridge OK"
```
### 6. Register BOTH clients (each registers differently!)
As in mcp-bridge-setup, the two clients register **differently** — getting this wrong is the usual
failure. Both entries are generated from `mcp/whatsapp/whatsapp.service.json` (see `mcp/CLAUDE.md`).
**Claude Code — HTTP via the bridge.** `.mcp.json` is **generated + CI-checked**, so regenerate it
(it picks up `whatsapp` → `http://localhost:8006/mcp` from the descriptor, alongside the existing
`board`/`openwhispr`/`calendar`/`guard`/`vault` entries):
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
node "$REPO_ROOT/scripts/gen-mcp-json.mjs"
```
**Claude Cowork Desktop — direct stdio.** `claude_desktop_config.json` (`$COWORK_CONFIG`) accepts
**stdio `command` servers only** (an HTTP `url` is rejected — same validated rule as
mcp-bridge-setup §5). Cowork spawns the Python MCP itself via `uv`. Merge just the `whatsapp` entry
(named = additive, backup-first to `.bak`, secrets inlined from the resolved descriptor):
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
node "$REPO_ROOT/scripts/gen-cowork-config.mjs" whatsapp
```
> The `whatsapp` Cowork entry carries the **same three vars** as the §5 bridge — `WHATSAPP_API_URL`
> pinned to the **Go bridge** (`http://localhost:$WHATSAPP_GO_PORT/api`), never the `:8006`
> MCP-bridge port; the token is read from `store/.bridge-token` by the server.
- **Absolute `uv` path** (`$UV_BIN`, from the loader): Claude Desktop's spawn env, like launchd's,
lacks Homebrew on `PATH`.
- **Quit + reopen Claude Desktop (⌘Q)** — it reads this file only at launch; then `whatsapp`
appears in Cowork's tools.
- **CHECKPOINT** — both registrations present:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
grep -q '"whatsapp"' "$REPO_ROOT/.mcp.json" && echo ".mcp.json OK"
"$NODE_BIN" -e 'const c=require(process.env.COWORK_CONFIG);process.exit(c.mcpServers&&c.mcpServers.whatsapp?0:1)' \
&& echo "Cowork config OK"
```
### 7. Confirm the nudge loop picks up both processes
`ensure-bridges.sh` so `npm run dev/start` brings WhatsApp up with the rest — but you do **not** edit
it by hand. It is a thin consumer of `node mcp/service-manifest.mjs --probe-list`, so the two
descriptors (`whatsapp.service.json` + `whatsappbridge.service.json`) feed it automatically: the MCP
bridge probes by `httpListen` on `:8006`, the Go sidecar by `bearerHealth` on `:8010` (a
cold/unpaired bridge that isn't listening yet just prints a `WARN … DOWN` — board-only triage still
reads SQLite, so that's a nudge, not a hard failure).
- **CHECKPOINT** — the script bootstraps both and reports them up:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
"$REPO_ROOT/mcp/ensure-bridges.sh" | grep -Ei 'whatsapp'
# expect: "[mcp] whatsapp bridge up on :8006" and "[mcp] whatsappbridge bridge up on :8010"
```
### 8. End-to-end verify
Confirm the whole add-on works from the MCP surface, not just the ports:
```sh
source "$(git rev-parse --show-toplevel)/config/load-config.sh"
# (a) list_chats through the whatsapp MCP returns real chats (reads messages.db):
curl -s -X POST "$WHATSAPP_MCP_BRIDGE_URL/mcp" \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_chats","arguments":{"limit":3}}}' \
| grep -o '"jid"' | head -1 && echo "list_chats returned data"
# (b) the Go bridge session is live:
grep -q "Connected to WhatsApp" "$REPO_ROOT/mcp/logs/whatsappbridge.out.log" && echo "bridge connected"
```
- **CHECKPOINT** — `list_chats` returns chat data, the bridge log shows `Connected to WhatsApp`,
and both clients see `whatsapp` after a ⌘Q reopen. The **cursor/board flow** (per-chat watermark
at `$REPO_ROOT/config/whatsapp-triage-state.json`, guard-scanned untrusted input, board
reconciliation) is then exercised by **`/whatsapp-triage`** — that skill owns the board side; this
one only proves the plumbing.
## Manage
```sh
launchctl kickstart -k gui/$(id -u)/com.chiefofstaff.mcp-whatsapp # restart the MCP bridge
launchctl kickstart -k gui/$(id -u)/com.chiefofstaff.mcp-whatsappbridge # restart the Go sidecar
launchctl bootout gui/$(id -u)/com.chiefofstaff.mcp-whatsapp # stop the MCP bridge
tail -f mcp/logs/whatsappbridge.out.log # Go bridge log ("Connected to WhatsApp")
tail -f mcp/logs/whatsapp.err.log # supergateway / Python MCP log
```
Re-pair after a session loss: stop both agents, repeat **§3** (foreground
`WHATSAPP_BRIDGE_PORT=$WHATSAPP_GO_PORT ./whatsapp-bridge`, scan the new QR), then `kickstart -k`
both agents. The token may rotate, but the server reads it from `store/.bridge-token` at runtime, so
**no plist/Cowork re-render is needed** — just restart the agents to pick up a fresh session.
## Gotchas (read before editing)
- **`WHATSAPP_BRIDGE_PORT` is the GO bridge's var — the Python MCP never reads it.** Only the Go
whatsmeow bridge (`whatsapp-bridge/main.go`) reads `WHATSAPP_BRIDGE_PORT`, as the port it **binds**.
The Python MCP server reads only `WHATSAPP_DB_PATH`, `WHATSMEOW_DB_PATH`, `WHATSAPP_API_URL`, and
`WHATSAPP_BRIDGE_TOKEN` (`whatsapp.py`) — it locates the Go bridge **solely via `WHATSAPP_API_URL`**.
So: the **Go sidecar plist** sets `WHATSAPP_BRIDGE_PORT=$WHATSAPP_GO_PORT`; the **Python MCP plist**
must **not** set it (a silent no-op). On the cos side the two ports get **distinct names** —
`WHATSAPP_MCP_BRIDGE_PORT` (the supergateway bridge, 8006) and `WHATSAPP_GO_PORT` (the Go sidecar,
8010) — keeping the literal `WHATSAPP_BRIDGE_PORT` confined to the one plist that owns it. Pin
`WHATSAPP_API_URL=http://localhost:$WHATSAPP_GO_PORT/api` so the server reaches the Go bridge
explicitly, no matter what.
- **Why :8010, not :8080.** whatsmeow's default bridge port is `8080`, which is commonly already
taken on a dev machine. cos pins the Go bridge to `$WHATSAPP_GO_PORT` (default **8010**) in the
sidecar plist and the §3 pairing run, and points `WHATSAPP_API_URL` at it. If `8010` is also taken,
change `WHATSAPP_GO_PORT` in `cos.env` and re-run the §4 `gen-launchd.mjs --install whatsapp
whatsappbridge` (both plists regenerate from their descriptors with the new port).
- **Reads work straight from SQLite even if the Go bridge is down.** The Python MCP reads
`messages.db` directly for all READ tools, so **board-only `/whatsapp-triage` tolerates a down Go
bridge** (it never needs the bridge for reads). But a **fresh pairing or ANY send** needs the
bridge live — so keep the sidecar's probe in §7 lenient and don't make triage depend on it.
- **Re-pair when the linked device is removed or the session expires.** If you remove this machine
from the phone's **Linked Devices**, or WhatsApp expires the session, the bridge logs a
disconnect and reads go stale. Redo **§3** (foreground `./whatsapp-bridge` on `$WHATSAPP_GO_PORT`,
scan the new QR), then restart both agents.
- **A Go toolchain is required to build.** There is no prebuilt bridge binary — `go build` is
mandatory in §2. Missing `go` → no daemon → no pairing and no sends (reads still work off an
existing `messages.db`, but a fresh machine has none until §3 runs).
- **`store/` holds real chat history + PII and is gitignored in the whatsapp-mcp repo.**
`whatsapp.db`, `messages.db`, the per-chat media under `store/{chat_jid}/`, and `.bridge-token`
are all sensitive and machine-local — never commit them. The token never enters a committed file
or the installed plist (the server reads it from `store/`). Cos's encrypted backup covers the Cos
repo's own stores, **not** this external `store/`.
- **Ports `8006` and `8010` must be free** (`lsof -nP -iTCP:8006 -sTCP:LISTEN`,
`lsof -nP -iTCP:8010 -sTCP:LISTEN`). `8006` sits just past mcp-bridge-setup's `8001–8005`
bridges; `8010` sits just past the `8008/8009` sidecars. A foreign process on either breaks the
corresponding step — pick another free port in `cos.env` if so.
- **launchd cannot expand `$VARS`.** As with every Cos plist, the **rendered** plists in
`~/Library/LaunchAgents` carry **literal absolute paths** (resolved from the loader by
`gen-launchd.mjs` in §4) and a `PATH` that starts with `$BREW_PREFIX/bin` — launchd never inherits
your login shell and can't see an nvm/asdf shim, so the `uv` / `go`-built binary paths must be
literal, not a `command -v` shim.