imsg — iMessage CLI for agents
When to use
- Reading, searching, or summarizing iMessage conversations on this Mac.
- Sending a text or file attachment to a handle (phone/email) or chat GUID.
- Waiting for a reply (blocking
watch --timeoutor non-blockingpollloop).
When NOT to use
- Non-macOS systems (the CLI reads
~/Library/Messages/chat.dband drives Messages.app). - Group MMS edge cases (RCS/MMS group behavior is not guaranteed).
- Anything requiring message deletion or editing — the db is opened read-only.
Agent rules
- Always pass
--json. It is the stable machine contract: no ANSI, ISO dates, fixed field names. Never parse human output. - Branch on exit codes:
| Code | Meaning |
|---|---|
| 0 | success / new messages |
| 1 | error (bad target, missing file, unreadable db, invalid config) |
| 2 | usage error OR blocked by safety policy (JSON has error + blocked: true when policy-blocked) |
| 3 | poll: no new messages |
| 124 | watch: timed out with no new message |
- Disambiguate exit 2 via stdout: policy blocks emit
{"error": "...", "blocked": true}; usage errors print a usage line to stderr.
Wait-for-reply loop (poll + cursor)
Bootstrap once (returns the current cursor, exit 3, no messages):
imsg poll --json
# => { "chat": null, "messages": [], "cursor": { "rowid": 48291, "ts": "..." } }
Then loop: sleep N seconds, poll from the saved cursor, scoped to the handle you are waiting on:
imsg poll +14085551234 --since-rowid 48291 --json
- Exit 0: new messages — process
messages[], save the newcursor.rowid. - Exit 3: nothing yet — keep waiting with the same cursor.
- The cursor never re-emits a message and never skips one; when
--limittruncates, the cursor points at the last emitted message so the next call resumes exactly there.
Alternative single blocking wait:
imsg watch +14085551234 --timeout 300 --json
- Exit 0: stdout is one
{ chat, messages, cursor }object with the new messages. - Exit 124: timed out;
messagesis empty,cursoris current.
Monitoring / waiting for messages
For agent harnesses that watch a background process's output rather than
polling a script, use imsg stream — a long-running NDJSON event stream
(one JSON object per stdout line, no ANSI, no human formatting):
imsg stream --from +14085551234 --max-events 1 --timeout 600
Run it as a background command and watch stdout for the "type":"message"
line: in Claude Code, the Monitor tool watching the background shell's
stdout; in other harnesses, tail the background process's output until a
matching line appears or the process exits. Exits 0 once --max-events is
hit, 124 on --timeout with no match, 2 if policy-blocked. See
references/commands.md for the full event schema.
When the monitor is started shortly after a separate one-shot read, add a bounded replay window so the handoff cannot miss an intervening message:
imsg stream --chat-id 12 --from +17739974600 --lookback 2m
--lookback applies the same chat, sender, and text filters to historical and
live rows. Historical message events add "replay": true; live event shape is
unchanged. Do not use it to bypass allowlist blocks or Full Disk Access.
Send safety
- Confirm the recipient handle with the user before any real send.
- Optional advanced config at
~/.config/imsg/config.jsoncan restrict imsg to an allowlist of handles/chats ({ "allowlist": [...], "confirmSend": true }); by default (no config file) everything is unrestricted. When a block does occur (exit 2,blocked: true), surface it to the user — do NOT retry or work around it. - When
confirmSendis enabled, sends require--yes. - Use
--dry-runto validate a send with zero side effects (works with text and files). - Messages over 500 chars require
--forcein interactive human mode.
Attachments
Send a file (with or without text):
imsg send +14085551234 --file /absolute/path/pic.jpg --json
imsg send +14085551234 "see attached" --file ~/Desktop/doc.pdf --json
Incoming attachments appear in read/recent/poll/watch JSON as:
"attachments": [{ "filename": "~/Library/Messages/Attachments/...", "mimeType": "image/heic", "transferName": "IMG_0001.heic", "resolvedPath": "/Users/me/Library/Messages/Attachments/..." }]
resolvedPath is the absolute on-disk path (may be null if not downloaded).
Troubleshooting
Run imsg doctor --json first. Five checks: chat.db access, osascript,
Messages.app, runtime, config.
chat.db accessfailing = the terminal lacks Full Disk Access (System Settings > Privacy & Security > Full Disk Access, then restart the terminal).osascriptfailing or send errors = Automation permission for Messages.app not granted (System Settings > Privacy & Security > Automation).- Env overrides:
IMESSAGE_DB_PATH(alternate chat.db),IMSG_CONFIG_PATH(alternate safety config).
Full command/flag/exit-code and JSON-shape reference: references/commands.md. Copy-paste patterns for common use cases: references/recipes.md.