Read, watch, and send iMessage / SMS from the macOS terminal — with stable JSON and JSON-RPC surfaces designed for agents, scripts, and long-running integrations.
imsg reads ~/Library/Messages/chat.db directly, streams new rows over
filesystem events (with a polling fallback), and drives Messages.app through
its public AppleScript automation surface. Advanced IMCore controls (read
receipts, typing indicators, edit/unsend, group management, rich sends) are
opt-in behind a SIP-disabled dylib injection. Linux builds are a read-only
preview against a chat.db copied from macOS.
Full docs: imsg.sh. Quickstart · JSON schema · JSON-RPC · Changelog
- Local-first reads. Chats, history, attachments, and search query
chat.dbdirectly — no daemon, no network round-trip. - Live streams.
imsg watchfollows filesystem events onchat.dband falls back to a lightweight poll when macOS drops the event. - Send through Messages.app. Text, files, and standard tapbacks ride the public AppleScript surface — no private send APIs required.
- Group-aware. Direct chats, group threads, participants, GUIDs, and per-chat account routing hints all show up in JSON.
- Built for agents. Stable JSON-RPC over stdio, deterministic JSON
schemas, and
imsg completions llmfor in-context CLI help. - Contacts integration. Resolves names from Address Book when permission is granted, while keeping raw handles in the output.
- Attachment-aware. Filenames, UTIs, byte counts, resolved paths, and optional CAF→M4A / GIF→PNG conversion for model consumers.
- Advanced IMCore (opt-in). Edit, unsend, delete, rich-text formatting, effects, reply threading, group create/rename/photo, member add/remove, read receipts, typing indicators, and live event streams via the bridge.
- Linux read-only preview. Inspect a copied Messages database from a Linux host. No sending, no Messages.app integration.
- macOS 14 or newer (macOS 26 / Tahoe supported, with caveats noted below).
- Messages.app signed in to iMessage and/or SMS relay.
- Full Disk Access for the terminal or parent app that launches
imsg. - Automation permission for Messages.app when using
sendorreact. - Optional Contacts permission for name resolution.
- Optional
ffmpegonPATHfor receive-side attachment conversion.
For SMS, enable Text Message Forwarding on your iPhone for this Mac.
Linux support is read-only and requires an existing Messages database copied from macOS. It does not send, react, mark read, show typing, launch Messages.app, or access iMessage/SMS accounts on Linux.
brew install steipete/tap/imsg
imsg --versionBuild from source:
make build
./bin/imsg --help# List recent chats.
imsg chats --limit 10 --json | jq -s
# Inspect one chat before automating against it.
imsg group --chat-id 42 --json
# Read history with attachment metadata.
imsg history --chat-id 42 --limit 20 --attachments --json
# Stream new messages, including tapbacks.
imsg watch --chat-id 42 --reactions --json
# Send a message — auto-pick iMessage or SMS.
imsg send --to "+14155551212" --text "on my way"
# Send a file (image, audio, document).
imsg send --to "Jane Appleseed" --file ~/Desktop/voice.m4a
# Send a standard tapback.
imsg react --chat-id 42 --reaction like
# Search local history.
imsg search --query "pizza" --match contains--json emits one JSON object per line. Pipe to jq -s to materialize an
array, or stream it to whatever consumer you're wiring up. Human progress and
warnings always go to stderr so pipes stay parseable.
Read, watch, and send (no special permissions beyond Full Disk Access and Automation):
imsg chats [--limit 20] [--json]imsg group --chat-id <id> [--json]imsg history --chat-id <id> [--limit 50] [--attachments] [--convert-attachments] [--participants <handles>] [--start <iso>] [--end <iso>] [--json]imsg watch [--chat-id <id>] [--since-rowid <id>] [--debounce <duration>] [--attachments] [--convert-attachments] [--reactions] [--participants <handles>] [--start <iso>] [--end <iso>] [--json]imsg search --query <text> [--match contains|exact] [--limit 50] [--json]imsg send (--to <handle-or-contact-name> | --chat-id <id> | --chat-identifier <id> | --chat-guid <guid>) [--text <text>] [--file <path>] [--service imessage|sms|auto] [--region US] [--json]imsg react --chat-id <id> --reaction love|like|dislike|laugh|emphasis|questionimsg rpcimsg completions bash|zsh|fish|llm
Advanced IMCore (require imsg launch with SIP off — see
Advanced IMCore):
imsg read --to <handle> [--chat-id <id>]imsg typing --to <handle> [--duration 5s] [--stop true]imsg launch [--dylib <path>] [--kill-only] [--json]imsg status [--json]imsg send-rich [--reply-to <guid>] [--file <path>],imsg send-multipart,imsg send-attachment [--reply-to <guid>],imsg tapbackimsg edit,imsg unsend,imsg delete-message,imsg notify-anywaysimsg chat-create,imsg chat-name,imsg chat-photo,imsg chat-add-member,imsg chat-remove-member,imsg chat-leave,imsg chat-delete,imsg chat-markimsg account,imsg whois,imsg nickname
react intentionally sends only the standard tapbacks Messages.app exposes
reliably through automation. Custom emoji tapbacks can be read from
history/watch output, but are sent through the bridge tapback command.
--json emits one JSON object per line, so consumers can stream it directly
or collect it with jq -s.
Chat objects include:
id,name,identifier,guid,service,last_message_atdisplay_name,contact_nameis_group,participantsaccount_id,account_login,last_addressed_handle
Message objects include:
id,chat_id,chat_identifier,chat_guid,chat_nameparticipants,is_groupguid,reply_to_guid,thread_originator_guid,destination_caller_idsender,sender_name,is_from_me,text,created_atattachments,reactions
When watch --reactions --json sees a tapback event, the message object also
includes is_reaction, reaction_type, reaction_emoji, is_reaction_add,
and reacted_to_guid.
Routing fields such as destination_caller_id, account_id,
account_login, and last_addressed_handle are read-only diagnostics from
Messages. AppleScript does not expose a way for imsg send to force a
specific outgoing Apple ID phone number or inline reply target.
imsg rpc speaks JSON-RPC 2.0 over stdin/stdout, one JSON object per line.
It is intended for agents and long-running integrations that want a single
process for chats, history, send, and watch.
Read methods: chats.list, messages.history, watch.subscribe,
watch.unsubscribe. Mutating: send. See docs/rpc.md for
request and response shapes.
--attachments reports metadata only. It does not copy or upload files.
Attachment metadata includes filename, transfer name, UTI, MIME type, byte count, sticker flag, missing flag, and resolved original path.
--convert-attachments exposes cached, model-compatible receive-side
variants:
- CAF audio → M4A
- GIF image → first-frame PNG
Conversion requires ffmpeg on PATH. Original Messages attachments are
left unchanged. Converted metadata is reported with converted_path and
converted_mime_type.
send --file sends regular files, including audio, through Messages.app.
Before handing the file to Messages, imsg stages it under
~/Library/Messages/Attachments/imsg/ so Messages can read it reliably.
imsg watch starts at the newest message by default and streams messages
written after it starts. Use --since-rowid <id> to resume from a stored
cursor.
The watcher listens for filesystem events on chat.db, chat.db-wal, and
chat.db-shm, then backs that up with a lightweight poll. The poll keeps
streams alive when macOS drops file events or rotates SQLite sidecar files.
RPC watch defaults to a 500ms debounce to reduce outbound echo races. CLI
watch can be tuned with --debounce.
If reads fail with unable to open database file, empty output, or
authorization denied:
- Open System Settings → Privacy & Security → Full Disk Access.
- Add the terminal or parent app that launches
imsg. - If launched from an editor, Node process, gateway, or shell wrapper, grant Full Disk Access to that parent app too.
- Also add the built-in Terminal.app at
/System/Applications/Utilities/Terminal.app; macOS can still consult the default terminal grant. - Toggle stale Full Disk Access entries off and on after terminal, Homebrew, Node, or app updates.
- Confirm Messages.app is signed in and
~/Library/Messages/chat.dbexists.
For sends and tapbacks, allow the terminal or parent app under Privacy & Security → Automation → Messages.
imsg opens chat.db read-only. It does not use SQLite immutable=1 by
default because immutable reads can miss WAL-backed Messages updates.
Default send, chats, history, watch, search, and read-only rpc
workflows do not require IMCore injection.
Advanced features such as read, typing, launch, bridge-backed rich
send, message mutation, and chat management are opt-in. They require SIP to
be disabled and a helper dylib to be injected into Messages.app. Homebrew
installs the helper from macOS release archives; source builds can run
make build-dylib first.
imsg launch
imsg statusImportant limits:
imsg launchrefuses to inject when SIP is enabled.imsg statusis read-only and does not auto-launch or auto-inject.- macOS 26 / Tahoe can block injection through library validation.
- macOS 26 / Tahoe can also reject direct IMCore clients through
imagentprivate-entitlement checks. - These limits affect advanced IMCore features such as typing indicators, not normal send/history/watch usage.
To revert after testing, re-enable SIP from Recovery mode with
csrutil enable.
The bridge implements a manual port of the BlueBubbles private-API surface
(inspired by their Apache-2.0 helper) into our own dylib — no third-party
binary. Most commands take a --chat argument that is the chat GUID
(e.g. iMessage;-;+15551234567 for direct, iMessage;+;chat0000 for
groups). Get a chat GUID via imsg chats --json.
Messaging:
# Rich send with effect + reply
imsg send-rich --chat 'iMessage;-;+15551234567' --text "boom" \
--effect com.apple.MobileSMS.expressivesend.impact \
--reply-to <messageGuid>
# Threaded reply with an attachment in one message
imsg send-rich --chat 'iMessage;-;+15551234567' \
--reply-to <messageGuid> --text "here it is" --file ~/Pictures/img.jpg
# Text formatting (macOS 15+ Sequoia): bold/italic/underline/strikethrough
# applied to specific ranges of the message body.
imsg send-rich --chat ... --text 'hello world' \
--format '[{"start":0,"length":5,"styles":["bold"]},
{"start":6,"length":5,"styles":["italic","underline"]}]'
# Multipart send (text-only in v1; per-part textFormatting also supported)
imsg send-multipart --chat 'iMessage;+;chat0000' \
--parts '[{"text":"hi"},
{"text":"there","textFormatting":[{"start":0,"length":5,"styles":["bold"]}]}]'
# Attachment (file or audio)
imsg send-attachment --chat ... --file ~/Pictures/img.jpg --transport auto
imsg send-attachment --chat ... --reply-to <messageGuid> --file ~/Pictures/img.jpg
imsg send-attachment --chat ... --file ~/audio.caf --audio
# Bridge tapback (custom emoji + remove supported here, unlike `imsg react`)
imsg tapback --chat ... --message <guid> --kind love
imsg tapback --chat ... --message <guid> --kind love --removeMutate (macOS 13+ — selector availability surfaced in imsg status):
imsg edit --chat ... --message <guid> --new-text "actually..."
imsg unsend --chat ... --message <guid>
imsg delete-message --chat ... --message <guid>
imsg notify-anyways --chat ... --message <guid>Chat management:
imsg chat-create --addresses '+15551111111,+15552222222' --name 'Crew' --text 'gm'
imsg chat-name --chat ... --name 'Renamed'
imsg chat-photo --chat ... --file ~/Downloads/g.jpg # set
imsg chat-photo --chat ... # clear
imsg chat-add-member --chat ... --address +15553333333
imsg chat-remove-member --chat ... --address +15553333333
imsg chat-leave --chat ...
imsg chat-delete --chat ...
imsg chat-mark --chat ... --read # or --unreadchat-create currently creates iMessage chats only. SMS sending remains
available through imsg send --service sms.
Introspection:
imsg account # active iMessage account + aliases
imsg whois --address +15551234567 --type phone
imsg whois --address foo@bar.com --type email
imsg nickname --address +15551234567Live events (typing indicators surfaced through the dylib):
imsg watch --bb-events # merge dylib events into stdout
imsg watch --bb-events --json # one JSON object per eventThe dylib v1 used a single overwriting .imsg-command.json polled at 100ms,
which races when multiple CLI invocations run concurrently. v2 uses a
per-request UUID-keyed queue:
~/Library/Containers/com.apple.MobileSMS/Data/
.imsg-bridge-ready PID lock — set when injection is live
.imsg-rpc/in/<uuid>.json requests dropped here by the CLI (atomic rename)
.imsg-rpc/out/<uuid>.json responses written by the dylib (atomic rename)
.imsg-events.jsonl inbound async events (typing, alias-removed)
Set IMSG_BRIDGE_LEGACY_IPC=1 to force the legacy single-file path for
debugging (existing v1 callers and un-rebuilt dylibs continue to work
without this).
make lint
make test
make buildmake test applies the repository's SQLite.swift patch before running Swift
tests.
The reusable Swift core lives in Sources/IMsgCore; the CLI target lives in
Sources/imsg; the injected helper lives in Sources/IMsgHelper.
MIT. Not affiliated with Apple. iMessage and SMS are trademarks of their respective owners.