imsg
imsg reads ~/Library/Messages/chat.db directly and sends through Messages.app automation. Reading is local and safe. Sending, reacting, marking read, typing indicators, and any chat mutation require an explicit user request — confirm recipient, service, and content in your final summary.
Ground rules
- Every read command supports
--jsonand emits NDJSON (one object per line). Pipe tojq -sto get an array. Stdout carries only JSON; progress and warnings go to stderr. - Two capability tiers:
- Standard (normal permissions):
chats,group,history,watch,search,send,react,nickname --local,account --local,whois --local. - Bridge (SIP disabled +
imsg launchdylib injection):send-rich,send-multipart,send-attachment,send-sticker,tapback,poll,edit,unsend,delete-message,read,typing,notify-anyways,chat-*,name-photo, and default-modeaccount/whois/nickname.
- Standard (normal permissions):
- Check availability with
imsg status --jsonbefore using bridge commands. Stickers additionally requiresend.stickerinrpc_methodsandselectors.stickerSend; attaching one requiresselectors.stickerAttach. If the bridge is down, use a standard command only when it preserves the requested semantics; otherwise stop and explain. Never turn a reply/effect/subject into a plain send or a GUID-targeted tapback intoreact, and never suggest disabling SIP unprompted. - Full command and flag reference:
imsg completions llm.
Preconditions
imsg status --json # feature availability + setup hints
sqlite3 ~/Library/Messages/chat.db 'pragma quick_check;' # fails => terminal lacks Full Disk Access
Reading
Resolve a person visible in the Messages.app UI from chats, not search. The UI name usually surfaces as contact_name (Contacts permission); it does not appear in imsg search results, raw message.text, or the DB handle table. No search hits is not proof the contact doesn't exist.
imsg chats --limit 200 --json | jq -s '.[] | select((.contact_name // .display_name // .name // .identifier // "" | ascii_downcase) | contains("beatrix"))'
imsg chats --unread-only --json | jq -s
Then inspect and read the chat by rowid:
imsg group --chat-id ID --json # identity + participants; check before automating
imsg history --chat-id ID --limit 50 --json | jq -s
imsg history --chat-id ID --start 2025-01-01T00:00:00Z --end 2025-02-01T00:00:00Z --json
imsg stats --chat-id ID --time-zone UTC --media --json # logical message + media totals
imsg scheduled list --json # future Send Later rows; read-only
imsg chat-background status --chat-id ID --json # inspect local background state; read-only
imsg name-photo status --chat GUID --json # read-only Share Name & Photo eligibility
- Chat
idis thechat.dbrowid: stable on one machine, the preferred--chat-idhandle.identifierandguidare portable across machines. --startis inclusive,--endexclusive; both take ISO8601. Use absolute timestamps for date-scoped questions.--attachmentsadds attachment metadata;--convert-attachmentsconverts CAF→M4A / GIF→PNG for model consumption.imsg search --query "pizza tonight" --jsonsearches message bodies only (--match containsdefault,exactavailable).imsg stats [--chat-id ID] [--time-zone IANA] [--media] --jsonaggregates logical messages by chat, inbound sender, service, and local date; no bridge required.- Chat-list JSON includes
unread_count. Inbound message payloads includeis_readand, when read,date_read; outbound payloads omit both. - SIP-free lookups:
imsg whois --address "+15551234567" --type phone --local,imsg nickname --address "+15551234567" --local --json,imsg account --local --json. Notenickname --localreturns your AddressBook label for the handle; the iMessage-shared nickname needs default-modenicknamevia the bridge. - Direct
sqlite3queries are a last resort; thehandletable lacks the resolved namesimsg chatsprovides.
Streaming
imsg watch --chat-id ID --json # filesystem events with polling fallback
imsg watch --since-rowid N --json # resume from a message id cursor
Message id doubles as the watch cursor: persist the last-seen id and pass it back via --since-rowid. Add --reactions for tapback add/remove events and --attachments for attachment metadata.
Sending (explicit request only)
imsg send --to "+15551234567" --text "message" --service auto
imsg send --chat-id ID --text "message" # prefer for groups: no address ambiguity
imsg send --to "+15551234567" --file ~/Desktop/pic.jpg
imsg name-photo share --chat GUID # shares YOUR Name & Photo; explicit request only
--service autoprefers iMessage and falls back to SMS for text-only phone sends;--no-sms-fallbackdisables that.imsg react --chat-id ID --reaction like(AppleScript) only targets the most recent incoming message and needs Accessibility permission. To react to a specific message by GUID, use bridgetapback.name-photo shareis not a vCard send. It discloses the local Messages Name & Photo to every participant in the selected chat; confirm the destination and explicit user intent before invoking it.
Bridge extras
Only after imsg status confirms the bridge is loaded (imsg launch injects it; refuses when SIP is on; macOS 26 entitlement gates can block features even with SIP off):
imsg send-rich --chat 'iMessage;-;+15551234567' --text 'hi' --reply-to MSG_GUID # replies, effects, subjects
imsg send-sticker --chat GUID --file ~/Pictures/sticker.png --attach-to MSG_GUID --target-part 0
imsg send-rich --chat 'iMessage;-;+15551234567' --url https://imsg.sh
imsg poll send --chat GUID --question 'Dinner?' --option 'Pizza' --option 'Sushi' --comment 'Vote by 5pm'
imsg poll unvote --chat GUID --poll POLL_GUID --option-index 1
imsg edit --chat GUID --message MSG_GUID --new-text 'updated' # macOS 13+
imsg chat-create --addresses '+15551234567,+15559876543' --name 'Crew'
poll send echoes --question as a best-effort plain caption after the Polls balloon; --comment overrides that caption. Do not retry automatically when only the caption fails: the poll may already be delivered. history and watch backfill a title-less inbound native poll's poll.question from its clean caption row.
Destructive bridge commands — unsend, delete-message, chat-delete, chat-leave, chat-remove-member — need per-action user confirmation.
Verification
For repo edits:
make test
make build
./bin/imsg chats --limit 3 --json | jq -s # live read proof against the local DB