---
type: note
alexandria_core: true
tags: [whatsapp, reference]
author: alexandria
---

# Reading WhatsApp (local, read-only)

The `whatsapp` module lets the agent read the owner's **local WhatsApp history** on
demand, so it can answer "what did X send me on WhatsApp?", "anything I haven't
replied to?", or "catch me up on the family group". It is the **inbound,
read-only** sibling of the iMessage reader — it never sends, replies, marks read,
or writes anything.

The reader (`alexandria_vault.engine.whatsapp_read`) is **plumbing**. The owner
never runs it or sees a command — they ask in plain language and the agent does the
rest, then answers with a useful read, not a raw dump.

## Where the data is — the native Mac app's store
The **native macOS WhatsApp app** (App Store) keeps its messages in
`~/Library/Group Containers/group.net.whatsapp.WhatsApp.shared/ChatStorage.sqlite` —
the same SQLite schema WhatsApp for iPhone uses, unencrypted at rest (only backups
are encrypted). That is what this reader reads. The old Electron **"WhatsApp
Desktop"** stored an IndexedDB nothing can read; a Mac still on it has no store to
read until the native app is installed and signed in (it syncs history from the
phone on first link). A store that isn't there is reported as exactly that.

## Where it runs — locally, not on the automation host
The store lives on whatever Mac is signed into WhatsApp — the owner's **laptop** —
not an always-on server/CI runner. Like the iMessage reader, this is the rare
capability that runs *in-session on the local machine, wherever the owner is asking
from*. Don't try to run it on a remote host.

## Prerequisite — Full Disk Access (one-time)
macOS TCC blocks reading the group container until the **app running the
assistant** has Full Disk Access. Grant it to Claude (or the terminal the agent
runs in): **System Settings → Privacy & Security → Full Disk Access → enable it →
re-open the app.** Until then the reader prints that fix and stops. This is a
permission grant, not a stored credential.

## Read-only — by construction
WhatsApp holds the live database open in WAL mode; the reader **never opens it**.
The store (db + `-wal` + `-shm`) is copied to a temp dir, the *copy* is queried
read-only, then deleted. There is no code path that writes to WhatsApp or sends a
message, and none should be improvised — sending is a separate, deliberately-unbuilt
capability.

## What the skill does when it fires
1. **Scope the ask.** Default to the last 24h across direct and group chats; narrow
   to a person (`--from`), one chat (`--thread`), 1:1 only (`--direct-only`), or a
   window (`--since 7d`, `today`, a date) when the owner names one.
2. **Run the reader** (`--json` for structured input the agent can reason over).
   Invisible to the owner.
3. **Sanity-check the coverage line.** Every run prints one stderr line —
   `window <start> → N msgs / T chats · oldest <time>`. The `oldest` stamp must reach
   back to the window start. A `TRUNCATED` warning means a cap dropped older
   messages — re-run with `--limit 0` before answering.
4. **Answer with a synthesis, not a transcript.** Direct chats first, then groups,
   newest first; **lead with anything that needs the owner** — a direct question, an
   unanswered inbound, a request or deadline — then one line per chat (*who · gist ·
   are they waiting on you*). A busy group gets a gist, not a replay. Link people to
   their `People/` home where one exists.
5. **Offer to drill in.** "Want the full chat with X?" pulls that chat verbatim.
   Only show a raw transcript when the owner asks for one.

## The reader (flags, for the agent)
```sh
P="${ALEXANDRIA_PYTHON:-python3}"   # the interpreter with the alexandria-vault package
"$P" -m alexandria_vault.engine.whatsapp_read --vault .                         # last 24h, direct + groups
"$P" -m alexandria_vault.engine.whatsapp_read --vault . --since 7d --json
"$P" -m alexandria_vault.engine.whatsapp_read --vault . --from alex             # a person (name or number)
"$P" -m alexandria_vault.engine.whatsapp_read --vault . --thread "Family"       # one chat by name or JID
"$P" -m alexandria_vault.engine.whatsapp_read --vault . --direct-only           # no groups
"$P" -m alexandria_vault.engine.whatsapp_read --vault . --list-threads          # recent chats + last activity
"$P" -m alexandria_vault.engine.whatsapp_read --vault . --include-all           # + broadcasts / status updates
```
`--limit` defaults to 0 (no cap) — the window is the bound; a positive cap is
warned about on stderr when it truncates (a negative one is refused). **`--thread`
reads the whole chat** unless `--since` is given explicitly — pass one for a busy
group, or the entire history lands in context.

## What the reader shows
- **Names** — the store's own first: the partner name WhatsApp took from the owner's
  address book, group-member names, then push names (what the sender calls
  themselves — shown with the number, since anyone can set one). What's left goes
  through the handle maps: the contacts module's shared
  `Agent/scripts/contacts/handles.json`, then this module's `contacts.json` on top.
  Without a name, the bare number shows.
- **Media as placeholders** — `[image: caption]`, `[video]`, `[voice note]`,
  `[document: name]`, `[sticker]`, `[contact card: name]`, `[location]`,
  `[deleted message]`; group events show their text when the store has it. Files
  are never exported. A message type the reader doesn't know is labelled by number,
  never guessed (`[type N]`, with the text after it if the row carries any). Calls show
  as `[call]` — WhatsApp's in-chat call entry (type 59, confirmed against the app's own
  call history: every incoming call, missed or answered, has one) — with no duration or
  missed/answered detail, since the row carries none the reader can decode; treat a run
  of them as "they called" when synthesizing, not as messages. A sender
  known only by a WhatsApp internal id (a "LID", not a phone number) shows as
  *(unnamed, WhatsApp id …)* rather than as a bare number.
- **Chats** — direct and group by default; broadcasts and status updates only with
  `--include-all`. `--thread` matches the chat's name or JID; `--from` matches a
  sender or chat by name or number.

## Rules
- **Runs locally; read-only; never sends.** See above.
- **Push names are self-declared.** Treat a sender known only by push name as
  unverified — the number is shown beside it for that reason.
- **Contacts are PII, off-repo.** `contacts.json` and the shared handle map are
  gitignored; only the `.example.json` ships.

## Config (`Agent/config.json` → `whatsapp`, all optional)
- `default_since` — the window when none is asked for (default `24h`).
- `include_groups` — read group chats too (default `true`).
- `contacts_file` — path to this module's handle→name map (default `Agent/scripts/whatsapp/contacts.json`).
- `db` — path to `ChatStorage.sqlite` (default the native app's store).
