This page is the command reference for imsg features that run through the injected IMCore bridge. Read Advanced IMCore first for the SIP, library-validation, entitlement, and privacy boundaries.
Most commands take --chat <guid>, where a direct chat looks like iMessage;-;+15551234567 and a group looks like iMessage;+;chat0000. Get the exact GUID from imsg chats --json, and run imsg status --json to inspect the selectors and RPC methods available on the current macOS version.
imsg status probes the running helper before reporting advanced features as available. If the probe times out, text and JSON report the bridge as unresponsive and recommend imsg launch. A missing helper, enabled SIP, or a bridge that has not been injected retains its setup diagnostic; a reply error from an older helper is not treated as a timeout.
#Messaging
Send an Apple URL preview. URL mode cannot be combined with text, effects, replies, or files.
imsg send-rich --chat 'iMessage;-;+15551234567' --url https://imsg.sh
Text replies resolve the parent with chat context even when its transcript is not loaded in a Messages window. If the helper cannot derive a native thread for an explicit --reply-to / RPC reply_to, the text send returns not_started before dispatch instead of silently sending outside the thread.
Send rich text, a reply, or an attachment:
imsg send-rich --chat 'iMessage;-;+15551234567' --text "boom" \
--effect com.apple.MobileSMS.expressivesend.impact \
--reply-to <message-guid>
imsg send-rich --chat 'iMessage;-;+15551234567' \
--reply-to <message-guid> --text "here it is" --file ~/Pictures/image.jpg
imsg send-rich --chat 'iMessage;-;+15551234567' --text 'hello world' \
--format '[{"start":0,"length":5,"styles":["bold"]}]'
Formatting requires macOS 15 or newer. Multipart messages accept a JSON array of text parts:
imsg send-multipart --chat 'iMessage;+;chat0000' \
--parts '[{"text":"hi"},{"text":"there"}]'
Send regular or audio attachments:
imsg send-attachment --chat 'iMessage;-;+15551234567' \
--file ~/Pictures/image.jpg --transport auto
imsg send-attachment --chat 'iMessage;-;+15551234567' \
--reply-to <message-guid> --file ~/Pictures/image.jpg
imsg send-attachment --chat 'iMessage;-;+15551234567' \
--file ~/Desktop/audio.caf --audio
With --transport auto, a normal file can fall back to AppleScript only when the bridge is unavailable or proves the request was not_started. It never falls back after publication has an uncertain outcome, including timeout, cancellation, a vanished or claimed request, or a malformed response. --audio and --reply-to remain bridge-only.
Send a validated sticker on its own or attach it to an existing bubble part:
imsg send-sticker --chat 'iMessage;-;+15551234567' \
--file ~/Pictures/sticker.png
imsg send-sticker --chat 'iMessage;-;+15551234567' \
--file ~/Pictures/sticker.png --attach-to <message-guid> --target-part 0
Stickers are iMessage-only. They accept PNG/APNG, GIF, or JPEG images up to 500 KiB, 618×618 pixels, 100 frames, and 25 million decoded pixels. Standalone sends require selectors.stickerSend; attached stickers also require selectors.stickerAttach.
For stickers and rich-link images, the bridge rejects pipes, symlinks, and files with extra hard links before reading them. If an image changes during the read, the command reports an error; finish writing the image before retrying.
Bridge tapbacks send and remove the six standard reaction kinds. Use --part 1 to target the second part of a multipart message, or pass p:1/<message-guid> as the message target:
imsg tapback --chat 'iMessage;-;+15551234567' \
--message <message-guid> --kind love
imsg tapback --chat 'iMessage;-;+15551234567' \
--message <message-guid> --kind love --remove
Bridge send responses identify the newly constructed message even when dispatch is queued. A returned GUID is an acknowledgment, not proof of delivery; an unavailable GUID is returned as an empty string.
#Native polls
Create a poll with a visible caption:
imsg poll send --chat 'iMessage;-;+15551234567' \
--question 'Dinner?' --option 'Pizza' --option 'Sushi'
Messages does not render the payload title on the poll balloon, so poll send follows it with a best-effort caption. Use --comment to choose different visible text or --no-comment when the caller already sent the context.
The caption is the question as far as recipients are concerned, so poll send reports whether it landed under comment rather than leaving a failure on stderr:
{"messageGuid":"...","comment":{"requested":true,"sent":true,"verified":true}}
{"messageGuid":"...","comment":{"requested":true,"sent":null,"verified":false,"error":"The bridge accepted the caption, but delivery was not confirmed before the verification deadline.","disposition":"may_have_completed","retry_safe":false}}
{"messageGuid":"...","comment":{"requested":true,"sent":false,"error":"...","disposition":"not_started","retry_safe":true}}
{"messageGuid":"...","comment":{"requested":true,"sent":null,"error":"...","disposition":"still_in_flight","retry_safe":false}}
{"messageGuid":"...","comment":{"requested":true,"sent":null}}
{"messageGuid":"...","comment":{"requested":false,"sent":false}}
requested is false when the caption was suppressed. sent is tri-state: true means confirmed delivery, false means confirmed non-delivery, and null means delivery is unknown. The command still exits 0 when only the caption fails or remains unknown because the poll itself did land.
A bridge acknowledgement only proves the transport accepted the send, so poll send also looks for the caption's row in the target chat before claiming delivery:
sent: true, verified: true: Messages recorded the row as delivered. The question reached the recipient.sent: false, verified: false: Messages recorded the caption row as failed. This is confirmed non-delivery, but retry safety is not inferred from the database.sent: null, verified: false: the row was absent, pending, or only locally sent at the verification deadline. Delivery is unknown because the caption may still arrive or fail later.sent: nullwith noverifiedkey: the check could not run because there was no readable database or caption GUID. Transport acknowledgement alone is not a delivery claim.sent: false, retry_safe: true: the transport proved the caption operation did not start. Re-send only the caption text, never the poll.sent: null, retry_safe: false: the transport reported an uncertain or still-running operation. Do not retry automatically.
When the bridge returns a caption GUID, comment.message_guid identifies that separate message. Use RPC message.send_status with this GUID to check an unresolved caption later; do not use the poll GUID or assume the caption is a threaded reply.
CLI output waits for the caption send attempt (the existing bridge send timeout is 150 seconds), then polls delivery for up to eight seconds. These are separate waits, not an end-to-end deadline. A slow or offline recipient can leave delivery unknown at the deadline.
Only retry_safe: true authorizes a caption retry. Never infer retry safety from sent, verified, or human-readable error text.
Vote or remove a vote with one option selector:
imsg poll vote --chat-id 42 --poll <poll-guid> --option-index 2
imsg poll unvote --chat-id 42 --poll <poll-guid> --option-index 2
Poll creation requires selectors.pollPayloadMessage. Voting requires selectors.pollVoteMessage and the matching RPC capability reported by imsg status --json.
#Message and chat mutation
Mutate an existing message:
imsg edit --chat 'iMessage;-;+15551234567' \
--message <message-guid> --new-text "actually..."
imsg unsend --chat 'iMessage;-;+15551234567' --message <message-guid>
imsg delete-message --chat 'iMessage;-;+15551234567' --message <message-guid>
imsg notify-anyways --chat 'iMessage;-;+15551234567' --message <message-guid>
Manage chats and participants:
imsg chat-create --addresses '+15551111111,+15552222222' --name 'Crew' --text 'gm'
imsg chat-name --chat 'iMessage;+;chat0000' --name 'Renamed'
imsg chat-photo --chat 'iMessage;+;chat0000' --file ~/Pictures/group.jpg
imsg chat-add-member --chat 'iMessage;+;chat0000' --address +15553333333
imsg chat-remove-member --chat 'iMessage;+;chat0000' --address +15553333333
imsg chat-leave --chat 'iMessage;+;chat0000'
imsg chat-delete --chat 'iMessage;+;chat0000'
imsg chat-mark --chat 'iMessage;+;chat0000' --read
chat-photo clears the photo when --file is omitted. chat-mark also accepts --unread. chat-create creates iMessage chats; SMS sending remains available through the standard imsg send --service sms path.
Group-photo files use the same secure staging as other attachments. The source must be a regular file reached without symlink components, and the calling process needs write access to ~/Library/Messages/Attachments/imsg/. If macOS denies staging, grant the caller Full Disk Access as described in Permissions, then restart it. Clearing a photo does not stage a file.
chat-create can create handles for previously uncontacted phone numbers and email addresses through the active iMessage account. Every recipient is IDS-checked before creating the chat; an unreachable or unresolved address fails the whole request instead of silently creating a smaller group. No Messages.app recipient-field warm-up is needed. See chat creation for errors and optional name/message behavior.
chat-add-member uses the same handle creation and IDS check. An unreachable or unresolved recipient is rejected before Messages sends a group invitation.
#Account and identity
Inspect the active account, local history, and address capabilities:
imsg account
imsg account --local
imsg whois --address +15551234567 --type phone
imsg whois --address +15551234567 --local
imsg nickname --address +15551234567
imsg nickname --address +15551234567 --local
Inspect or explicitly share the local Messages Name & Photo:
imsg name-photo status --chat 'iMessage;-;+15551234567'
imsg name-photo share --chat 'iMessage;-;+15551234567'
status reports whether Messages would offer its native sharing action; it is not a durable record of prior sharing. share discloses the local profile to every chat participant and reports a request, not a delivery receipt. Call it only after explicit user confirmation of the destination.
#Live bridge events
Merge bridge-pushed typing and alias events into the normal watch stream:
imsg watch --bb-events --json
This combines two independently ordered best-effort sources on serialized stdout. Bridge events begin at the current event-log EOF and are non-resumable; there is no ordering guarantee relative to database messages. RPC clients can subscribe separately with bridge.events.subscribe and cancel its shared subscription ID with watch.unsubscribe.
#IPC layout
The bridge uses a UUID-keyed request queue so concurrent CLI invocations cannot overwrite one another:
~/Library/Containers/com.apple.MobileSMS/Data/
.imsg-bridge-ready PID lock set while injection is live
.imsg-rpc/in/<uuid>.json atomically published requests
.imsg-rpc/out/<uuid>.json per-request responses
.imsg-events.jsonl inbound asynchronous events
Set IMSG_BRIDGE_LEGACY_IPC=1 only when debugging against an older, unrebuilt helper that still uses the single-file IPC path.
#Delivery outcomes and retry safety
The v2 client classifies failed mutations from queue ownership, using monotonic deadlines:
- An unpublished request, or an unclaimed request that the client atomically
- A
.processing.<pid>claim or an unreadable inbox isstill_in_flight. - A published request that vanished without a response is
owns and removes before a final empty response check, is not_started and retry-safe.
Files are preserved.
may_have_completed.
The bridge accepts string response IDs and legacy integer IDs; fractional, non-finite, or out-of-range numeric IDs are rejected as malformed responses.
Malformed or unreadable responses after publication are also uncertain. The legacy single-file command timeout is always still_in_flight because it has no per-request claim proof. Never automatically retry a bridge mutation unless the typed disposition is not_started; human-readable error wording is not a retry contract.