Skip to content

iMessage / SMS

The live iMessage/SMS connector. Unlike every other connector, it runs on the host Mac, not in the container scheduler — read this whole page before turning it on.

What it does

An incremental poll over ~/Library/Messages/chat.db, the local SQLite database the macOS Messages app maintains for iMessage and (via Continuity/SMS relay) text messages. The connector opens the database read-only and walks forward from a {rowid, guid} cursor, normalizing new rows into the same cap_items shape as every other connector. No credentials, no network calls.

Prereqs

Full Disk Access required

chat.db lives behind macOS's Full Disk Access (FDA) protection. Grant FDA to whichever process runs the poll — the terminal app, or the python3 binary itself: System Settings → Privacy & Security → Full Disk Access → add it. Without FDA, opening chat.db fails with authorization denied.

  • macOS only, run on the same Mac whose Messages app owns chat.db.
  • No IMAP-style account or app password — this connector reads a local file.

Why host-only

The connector declares host_only = True. capture_agent.poll partitions connectors by that flag: the containerized scheduler (poll.py without --host) skips any host_only row silently — no last_error noise — and only the owner's --host invocation polls it. There's deliberately no module.yaml schedule for it; it's driven by an owner-installed launchd job instead (see below).

The cap_connectors row

{
  "kind": "imessage",
  "enabled": true,
  "secret_ref": "",
  "config": {
    "db_path": "~/Library/Messages/chat.db"
  },
  "owner_addresses": ["<your iMessage handles>"]
}

secret_ref is empty — chat.db needs no credentials. owner_addresses should list your own iMessage handles (phone numbers / email aliases); if left empty, the connector falls back to the comma-separated JARVIS_OWNER_EMAIL env var, same as every other connector.

Turn it on (host)

Create the cap_connectors row above via the PocketBase admin UI, then run the poll once by hand from the repo root to confirm it works:

PYTHONPATH="agent-runtime/src:modules/capture/agent" POCKETBASE_URL=http://127.0.0.1:8091 DEV_USER_EMAIL=user@jarvis.local DEV_USER_PASSWORD=userpassword123 python3 -m capture_agent.poll --host

Expect capture poll: {'connectors_polled': 1, 'ingested': N, 'errors': 0}.

LaunchAgent (recurring)

For it to run on its own every 10 minutes, install the LaunchAgent template at modules/capture/agent/host/com.jarvis.capture.imessage-poll.plist:

  1. Edit the plist, replacing every REPLACE_ME_* placeholder (repo root, dev credentials, owner email).
  2. Install and load it:
cp modules/capture/agent/host/com.jarvis.capture.imessage-poll.plist ~/Library/LaunchAgents/com.jarvis.capture.imessage-poll.plist
launchctl load ~/Library/LaunchAgents/com.jarvis.capture.imessage-poll.plist

The job runs python3 -m capture_agent.poll --host every 10 minutes against PocketBase at http://127.0.0.1:8091. Logs go to /tmp/jarvis-capture-imessage-poll.log (stdout) and /tmp/jarvis-capture-imessage-poll.err (stderr). To stop it:

launchctl unload ~/Library/LaunchAgents/com.jarvis.capture.imessage-poll.plist

Full details, including how the LaunchAgent is wired up, live in the host runbook: modules/capture/agent/host/HOST_POLL.md.

Backfill

To import history that predates turning the live poll on:

python -m capture_agent.backfill imessage --db ~/Library/Messages/chat.db --since 2026-01-01

Verify

In the PocketBase admin: cap_items grows by roughly the number of new messages, the row's last_poll_at updates each run, and last_error is empty. The row's cursor becomes {"rowid": <last ROWID>, "guid": <that row's guid>}.

Errors

  • Full Disk Access denied — opening chat.db raises authorization denied; grant FDA as described above and re-run.
  • chat.db rebuilt — a Messages restore or migration can reset SQLite ROWIDs. Before trusting its cursor, the connector checks that the row still at the cursor's rowid still has the recorded guid; if it doesn't, the cursor resets to 0 and the whole database is re-scanned. This is safe: cap_items dedupes by external_id (the message guid), so already-ingested messages are skipped and only genuinely new ones get written.