For OpenClaw iMessage deployments, use
imsg on a signed-in macOS Messages host. If your Gateway runs on Linux or Windows, point channels.imessage.cliPath at an SSH wrapper that runs imsg on the Mac.Inbound recovery is automatic. After a bridge or gateway restart, iMessage replays the messages missed while it was down and suppresses the stale “backlog bomb” Apple can flush after a Push recovery, deduping so nothing is dispatched twice. There is no config to enable — see Inbound recovery after a bridge or gateway restart.imsg rpc and speaks JSON-RPC over stdio — no separate daemon or port. Advanced actions require imsg launch and a successful private API probe.
Private API actions
Replies, tapbacks, effects, polls, attachments, and group management.
Pairing
iMessage DMs default to pairing mode.
Remote Mac
Use an SSH wrapper when the Gateway is not running on the Messages Mac.
Configuration reference
Full iMessage field reference.
Quick setup
- Local Mac (fast path)
- Remote Mac over SSH
Requirements and permissions (macOS)
- Messages must be signed in on the Mac running
imsg. - Full Disk Access is required for the process context running OpenClaw/
imsg(Messages DB access). - Automation permission is required to send messages through Messages.app.
- For advanced actions (react / edit / unsend / threaded reply / effects / polls / group ops), System Integrity Protection must be disabled — see Enabling the imsg private API. Basic text and media send/receive work without it.
SSH wrapper sends fail with AppleEvents -1743
SSH wrapper sends fail with AppleEvents -1743
A remote-SSH setup can read chats, pass Check the signed-in Mac user’s TCC database or System Settings > Privacy & Security > Automation. If the Automation entry is recorded for In that state, repeating
channels status --probe, and process inbound messages while outbound sends still fail with an AppleEvents authorization error:/usr/libexec/sshd-keygen-wrapper instead of the imsg or local shell process, macOS may not expose a usable Messages toggle for that SSH server-side client:tccutil reset AppleEvents or rerunning imsg send through the same SSH wrapper may keep failing because the process context that needs Messages Automation is the SSH wrapper, not an app the UI can grant.Use one of the supported imsg process contexts instead:- Run the Gateway, or at least the
imsgbridge, in the logged-in Messages user’s local session. - Start the Gateway with a LaunchAgent for that user after granting Full Disk Access and Automation from the same session.
- If you keep the two-user SSH topology, verify that a real outbound
imsg sendsucceeds through the exact wrapper before enabling the channel. If it cannot be granted Automation, reconfigure to a single-userimsgsetup instead of relying on the SSH wrapper for sends.
Enabling the imsg private API
imsg ships in two operational modes:
- Basic mode (default, no SIP changes needed): outbound text and media via
send, inbound watch/history, chat list. This is what you get out of the box from a freshbrew install steipete/tap/imsgplus the standard macOS permissions above. - Private API mode:
imsginjects a helper dylib intoMessages.appto call internalIMCorefunctions. This unlocksreact,edit,unsend,reply(threaded),sendWithEffect,pollandpoll-vote(native Messages polls),renameGroup,setGroupIcon,addParticipant,removeParticipant,leaveGroup, plus typing indicators and read receipts.
imsg README is explicit about the requirement:
Advanced features such asThe helper-injection technique usesread,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 intoMessages.app.imsg launchrefuses to inject when SIP is enabled.
imsg’s own dylib to reach Messages private APIs. There is no third-party server or BlueBubbles runtime in the OpenClaw iMessage path.
Setup
-
Install (or upgrade)
imsgon the Mac that runs Messages.app:Theimsg status --jsonoutput reportsbridge_version,rpc_methods, and per-methodselectorsso you can see what the current build supports before you start. -
Disable System Integrity Protection, and (on modern macOS) Library Validation. Injecting a non-Apple helper dylib into the Apple-signed
Messages.appneeds SIP off and library validation relaxed. The Recovery-mode SIP step is macOS-version-specific:- macOS 10.13-10.15 (Sierra-Catalina): disable Library Validation via Terminal, reboot to Recovery Mode, run
csrutil disable, restart. - macOS 11+ (Big Sur and later), Intel: Recovery Mode (or Internet Recovery),
csrutil disable, restart. - macOS 11+, Apple Silicon: power-button startup sequence to enter Recovery; on recent macOS versions hold the Left Shift key when you click Continue, then
csrutil disable. Virtual-machine setups follow a separate flow, so take a VM snapshot first.
csrutil disablealone is usually not enough. Apple still enforces library validation againstMessages.appas a platform binary, so an adhoc-signed helper is rejected (Library Validation failed: ... platform binary, but mapped file is not) even with SIP off. After disabling SIP, also disable library validation and reboot:macOS 26 (Tahoe), verified on 26.5.1: SIP off plus theDisableLibraryValidationcommand above is sufficient to inject the helper across 26.0 through 26.5.x. No boot-args are required. The plist is the decisive factor and the most common missing step when injection fails on Tahoe:- With the plist:
imsg launchinjects andimsg statusreportsadvanced_features: true. - Without the plist (even with SIP off):
imsg launchfails withFailed to launch: Timeout waiting for Messages.app to initialize. AMFI rejects the adhoc helper at load, so the bridge never becomes ready and the launch times out. That timeout is the symptom most people hit on Tahoe; the fix is the plist above, not anything more drastic.
imsg launchinjection or specificselectorsstart returning false after a macOS upgrade, this gate is the usual cause. Check your SIP and library-validation state before assuming the SIP step itself failed. If those settings are correct and the bridge still cannot inject, collectimsg status --jsonplus theimsg launchoutput and report it to theimsgproject instead of weakening additional system-wide security controls. - macOS 10.13-10.15 (Sierra-Catalina): disable Library Validation via Terminal, reboot to Recovery Mode, run
-
Inject the helper. With SIP disabled and Messages.app signed in:
imsg launchrefuses to inject when SIP is still enabled, so this also doubles as a confirmation that step 2 took. -
Verify the bridge from OpenClaw:
The iMessage entry should report
works, andimsg status --json | jq '{rpc_methods, selectors}'should show the capabilities exposed by your macOS build. Poll creation requiresselectors.pollPayloadMessage; voting requires bothselectors.pollVoteMessageand thepoll.voteRPC method. The OpenClaw plugin advertises only actions supported by the cached probe, while an empty cache stays optimistic and probes on first dispatch.
openclaw channels status --probe reports the channel as works but specific actions throw “iMessage <action> requires the imsg private API bridge” at dispatch time, run imsg launch again — the helper can fall out (Messages.app restart, OS update, etc.) and the cached available: true status will keep advertising actions until the next probe refreshes.
When SIP stays enabled
If disabling SIP is not acceptable for your threat model:imsgfalls back to basic mode — text + media + receive only.- The OpenClaw plugin still advertises text/media send and inbound monitoring; it hides
react,edit,unsend,reply,sendWithEffect, and group ops from the action surface (per the per-method capability gate). - You can run a separate non-Apple-Silicon Mac (or a dedicated bot Mac) with SIP off for the iMessage workload, while keeping SIP enabled on your primary devices. See Dedicated bot macOS user (separate iMessage identity) below.
Access control and routing
- DM policy
- Group policy + mentions
- Sessions and deterministic replies
channels.imessage.dmPolicy controls direct messages:pairing(default)allowlist(requires at least oneallowFromentry)open(requiresallowFromto include"*")disabled
channels.imessage.allowFrom.Allowlist entries must identify senders: handles or static sender access groups (accessGroup:<name>). Use channels.imessage.groupAllowFrom for chat targets such as chat_id:*, chat_guid:*, or chat_identifier:*; use channels.imessage.groups for numeric chat_id registry keys.ACP conversation bindings
iMessage chats can be bound to ACP sessions. Fast operator flow:- Run
/acp spawn codex --bind hereinside the DM or allowed group chat. - Future messages in that same iMessage conversation route to the spawned ACP session.
/newand/resetreset the same bound ACP session in place./acp closecloses the ACP session and removes the binding.
bindings[] entries with type: "acp" and match.channel: "imessage".
match.peer.id can use:
- normalized DM handle such as
+15555550123oruser@example.com chat_id:<id>(recommended for stable group bindings)chat_guid:<guid>chat_identifier:<identifier>
Deployment patterns
Dedicated bot macOS user (separate iMessage identity)
Dedicated bot macOS user (separate iMessage identity)
Use a dedicated Apple ID and macOS user so bot traffic is isolated from your personal Messages profile.Typical flow:
- Create/sign in a dedicated macOS user.
- Sign into Messages with the bot Apple ID in that user.
- Install
imsgin that user. - Create an SSH wrapper so OpenClaw can run
imsgin that user context. - Point
channels.imessage.accounts.<id>.cliPathand.dbPathto that user profile.
Remote Mac over Tailscale (example)
Remote Mac over Tailscale (example)
Common topology:Use SSH keys so both SSH and SCP are non-interactive.
Ensure the host key is trusted first (for example
- gateway runs on Linux/VM
- iMessage +
imsgruns on a Mac in your tailnet cliPathwrapper uses SSH to runimsgremoteHostenables SCP attachment fetches
ssh bot@mac-mini.tailnet-1234.ts.net) so known_hosts is populated.Multi-account pattern
Multi-account pattern
iMessage supports per-account config under
channels.imessage.accounts.Each account can override fields such as cliPath, dbPath, allowFrom, groupPolicy, mediaMaxMb, history settings, and attachment root allowlists.Direct-message history
Direct-message history
Set
channels.imessage.dmHistoryLimit to seed new direct-message sessions with recent decoded imsg history for that conversation. Use channels.imessage.dms["<sender>"].historyLimit for per-sender overrides, including 0 to disable history for a sender.iMessage DM history is fetched on demand from imsg. Leaving dmHistoryLimit unset disables global DM history seeding, but a positive per-sender channels.imessage.dms["<sender>"].historyLimit still enables seeding for that sender.Media, chunking, and delivery targets
Attachments and media
Attachments and media
- inbound attachment ingestion is off by default — set
channels.imessage.includeAttachments: trueto forward photos, voice memos, video, and other attachments to the agent. With it disabled, attachment-only iMessages are dropped before reaching the agent and may produce noInbound messagelog line at all. - remote attachment paths can be fetched via SCP when
remoteHostis set - attachment paths must match allowed roots:
channels.imessage.attachmentRoots(local)channels.imessage.remoteAttachmentRoots(remote SCP mode)- configured roots extend the default root pattern
/Users/*/Library/Messages/Attachments(merged, not replaced)
- SCP uses strict host-key checking (
StrictHostKeyChecking=yes) - outbound media size uses
channels.imessage.mediaMaxMb(default 16 MB)
Outbound text and chunking
Outbound text and chunking
- text chunk limit:
channels.imessage.textChunkLimit(default 4000) - chunk mode:
channels.imessage.chunkModelength(default)newline(paragraph-first splitting)
- outbound markdown bold/italic/underline/strikethrough is converted to native styled text (macOS 15+ recipients render the styling; older recipients see plain text without the markers); markdown tables are converted per the channel markdown table mode
channels.imessage.sendTransport(autodefault,bridge,applescript) selects howimsgdelivers sends
Addressing formats
Addressing formats
Preferred explicit targets:
chat_id:123(recommended for stable routing)chat_guid:...chat_identifier:...
imessage:+1555...sms:+1555...user@example.com
Private API actions
Whenimsg launch is running and openclaw channels status --probe reports privateApi.available: true, the message tool can use iMessage-native actions in addition to normal text sends.
All actions are enabled by default; use channels.imessage.actions to turn individual actions off:
Available actions
Available actions
- react: Add/remove iMessage tapbacks (
messageId,emoji,remove). Supported tapbacks map to love, like, dislike, laugh, emphasize, and question. Removing without an emoji clears whichever tapback was set. - reply: Send a threaded reply to an existing message (
messageId,textormessage, pluschatGuid,chatId,chatIdentifier, orto). Reply-with-attachment additionally needs animsgbuild whosesend-richsupports--file. - sendWithEffect: Send text with an iMessage effect (
textormessage,effectoreffectId). Short names: slam, loud, gentle, invisibleink, confetti, lasers, fireworks, balloon, heart, echo, happybirthday, shootingstar, sparkles, spotlight. - edit: Edit a sent message on supported macOS/private API versions (
messageId,textornewText). Only messages the gateway itself sent can be edited. - unsend: Retract a sent message on supported macOS/private API versions (
messageId). Only messages the gateway itself sent can be unsent. - upload-file: Send media/files (
bufferas base64 or a hydratedmedia/path/filePath,filename, optionalasVoice). Legacy alias:sendAttachment. - renameGroup, setGroupIcon, addParticipant, removeParticipant, leaveGroup: Manage group chats when the current target is a group conversation. These mutate the host’s Messages identity, so they require an owner sender or an
operator.adminGateway client. - poll: Create a native Apple Messages poll (
pollQuestion,pollOptionrepeated 2 to 12 times, pluschatGuid,chatId,chatIdentifier, orto). Recipients on iOS/iPadOS/macOS 26+ see and vote on it natively; older OS versions get a “Sent a poll” text fallback. Requiresselectors.pollPayloadMessage. - poll-vote: Vote on an existing poll (
pollIdormessageId, plus exactly one ofpollOptionIndex,pollOptionId, orpollOptionText). Requiresselectors.pollVoteMessageand thepoll.voteRPC method.
poll-vote.Message IDs
Message IDs
Inbound iMessage context includes both short
MessageSid values and full message GUIDs (MessageSidFull) when available. Short IDs are scoped to the recent SQLite-backed reply cache and are checked against the current chat before use. If a short ID has expired or belongs to another chat, retry with the full MessageSidFull.Capability detection
Capability detection
OpenClaw hides private API actions only when the cached probe status says the bridge is unavailable. If the status is unknown, actions remain visible and dispatch probes lazily so the first action can succeed after
imsg launch without a separate manual status refresh.Read receipts and typing
Read receipts and typing
When the private API bridge is up, accepted inbound chats are marked read and direct chats show a typing bubble as soon as the turn is accepted, while the agent prepares context and generates. Disable read-marking with:Older
imsg builds that pre-date the per-method capability list gate off typing/read silently; OpenClaw logs a one-time warning per restart so the missing receipt is attributable.Inbound tapbacks
Inbound tapbacks
OpenClaw subscribes to iMessage tapbacks and routes accepted reactions as system events instead of normal message text, so a user tapback does not trigger an ordinary reply loop.Notification mode is controlled by
channels.imessage.reactionNotifications:"own"(default): notify only when users react to bot-authored messages."all": notify for all inbound tapbacks from authorized senders."off": ignore inbound tapbacks.
channels.imessage.accounts.<id>.reactionNotifications.Approval reactions (👍 / 👎)
Approval reactions (👍 / 👎)
When
approvals.exec.enabled or approvals.plugin.enabled is true and the request routes to iMessage, the gateway delivers an approval prompt natively and accepts a tapback to resolve it:👍(Like tapback) →allow-once👎(Dislike tapback) →denyallow-alwaysremains a manual fallback: send/approve <id> allow-alwaysas a regular reply.
channels.imessage.allowFrom (or channels.imessage.accounts.<id>.allowFrom); add the user’s phone number in E.164 form or their Apple ID email (chat targets such as chat_id:* are not valid approver entries). The wildcard entry "*" is honored but allows any sender to approve; an empty approver list disables the reaction shortcut entirely. The reaction shortcut intentionally bypasses reactionNotifications, dmPolicy, and groupAllowFrom because the explicit-approver allowlist is the only gate that matters for approval resolution./approve text command authorization follows the same list: when channels.imessage.allowFrom is non-empty, /approve <id> <decision> is authorized against that approver list (not the broader DM allowlist), and senders permitted on the DM allowlist but not in allowFrom receive an explicit denial. When allowFrom is empty, the same-chat fallback stays in effect and /approve authorizes anyone the DM allowlist permits. Add every operator who should approve — via /approve or via reactions — to allowFrom.Operator notes:- The reaction binding is stored both in memory and in the gateway’s persistent keyed store (TTL matched to the approval expiry), and the gateway also polls pending prompts for tapbacks, so a tapback that lands shortly after a gateway restart still resolves the approval.
- The operator’s own
is_from_me=truetapback (for example from a paired Apple device) resolves the approval when that handle is an explicit approver. - Approval prompts route into a group conversation only when explicit approvers are configured; otherwise any group member could approve.
- Legacy text-style tapbacks (
Liked "…"plain text from very old Apple clients) cannot resolve approvals because they carry no message GUID; reaction resolution requires the structured tapback metadata that current macOS / iOS clients emit.
Config writes
iMessage allows channel-initiated config writes by default (for/config set|unset when commands.config: true).
Disable:
Coalescing split-send DMs (command + URL in one composition)
When a user types a command and a URL together — e.g.Dump https://example.com/article — Apple’s Messages app splits the send into two separate chat.db rows:
- A text message (
"Dump"). - A URL-preview balloon (
"https://...") with OG-preview images as attachments.
imsg introduces.
channels.imessage.coalesceSameSenderDms opts a DM into buffering consecutive same-sender rows. When imsg exposes the structural URL-preview marker balloon_bundle_id: "com.apple.messages.URLBalloonProvider" on one of the source rows, OpenClaw merges only that real split-send and keeps any other buffered rows as separate turns. On older imsg builds that emit no balloon metadata at all, OpenClaw cannot tell a split-send from separate sends, so it falls back to merging the bucket. That preserves the pre-metadata behavior rather than regressing Dump <url> split-sends into two turns. Group chats continue to dispatch per-message so multi-user turn structure is preserved.
- When to enable
- Enabling
- Trade-offs
Enable when:
- You ship skills that expect
command + payloadin one message (dump, paste, save, queue, etc.). - Your users paste URLs alongside commands.
- You can accept the added DM turn latency (see below).
- You need minimum command latency for single-word DM triggers.
- All your flows are one-shot commands without payload follow-ups.
Scenarios and what the agent sees
The “Flag on” column shows behavior on animsg build that emits balloon_bundle_id. On older imsg builds that emit no balloon metadata at all, the rows below marked “Two turns” / “N turns” instead fall back to a legacy merge (one turn): OpenClaw cannot structurally tell a split-send from separate sends, so it preserves the pre-metadata merge. Precise separation activates once the build emits balloon metadata.
| User composes | chat.db produces | Flag off (default) | Flag on + window (imsg emits balloon metadata) |
|---|---|---|---|
Dump https://example.com (one send) | 2 rows ~1 s apart | Two agent turns: “Dump” alone, then URL | One turn: merged text Dump https://example.com |
Save this 📎image.jpg caption (attachment + text) | 2 rows without URL balloon metadata | Two turns | Two turns after metadata is observed; one merged turn on old/pre-latch metadata-less sessions |
/status (standalone command) | 1 row | Instant dispatch | Wait up to window, then dispatch |
| URL pasted alone | 1 row | Instant dispatch | Wait up to window, then dispatch |
| Text + URL sent as two deliberate separate messages, minutes apart | 2 rows outside window | Two turns | Two turns (window expires between them) |
| Rapid flood (>10 small DMs inside window) | N rows without URL balloon metadata | N turns | N turns after metadata is observed; one bounded merged turn on old/pre-latch metadata-less sessions |
| Two people typing in a group chat | N rows from M senders | M+ turns (one per sender bucket) | M+ turns — group chats are not coalesced |
Inbound recovery after a bridge or gateway restart
iMessage recovers messages missed while the gateway was down, and at the same time suppresses the stale “backlog bomb” Apple can flush after a Push recovery. The default behavior is always on, built on the inbound dedupe.- Replay dedupe. Every dispatched inbound message is recorded by its Apple GUID in persistent plugin state (
imessage.inbound-dedupe), claimed at ingestion and committed after handling (released on a transient failure so it can retry). Anything already handled is dropped instead of dispatched twice. This is what lets recovery replay aggressively without per-message bookkeeping. - Downtime recovery. On startup the monitor remembers the last dispatched
chat.dbrowid (a persisted per-account cursor) and passes it toimsg watch.subscribeassince_rowid, so imsg replays the rows that landed while the gateway was down, then tails live. Replay is bounded to the most recent 500 rows and to messages up to ~2 hours old, and the dedupe drops anything already handled. - Stale-backlog age fence. Rows above the startup boundary are genuinely live; one whose send date is more than ~15 minutes older than its arrival is the Push-flush backlog and is suppressed. Replayed rows (at or below the boundary) use the wider recovery window instead, so a recently-missed message is delivered while ancient history is not.
cliPath setups, because since_rowid replay runs over the same imsg RPC connection. The difference is the window: when the gateway can read chat.db (local), it anchors the startup rowid boundary, caps the replay span, and delivers missed messages up to a couple of hours old. Over a remote SSH cliPath it cannot read the database, so the replay is uncapped and every row uses the live age fence — it still recovers recently-missed messages and still suppresses old backlog, just with the narrower live window. Run the gateway on the Messages Mac for the wider recovery window.
Operator-visible signal
Suppressed backlog is logged at the default level, never silently dropped (therecovery flag shows which window applied):
Migration
channels.imessage.catchup.* is deprecated — downtime recovery is automatic and needs no config for new setups. Existing configs with catchup.enabled: true remain honored as a compatibility profile for the recovery replay window. Disabled catchup blocks (enabled: false or no enabled: true) are retired; openclaw doctor --fix removes those.
Troubleshooting
imsg not found or RPC unsupported
imsg not found or RPC unsupported
Validate the binary and RPC support:If the probe reports RPC unsupported, update
imsg. If private API actions are unavailable, run imsg launch in the logged-in macOS user session and probe again. If the Gateway is not running on macOS, use the Remote Mac over SSH setup above instead of the default local imsg path.Messages send but inbound iMessages do not arrive
Messages send but inbound iMessages do not arrive
First prove whether the message reached the local Mac. If If phone-sent messages create no new rows, repair the macOS Messages and Apple Push layer before changing OpenClaw config. A one-shot service refresh is often enough:Send a fresh iMessage from the phone and confirm a new
chat.db does not change, OpenClaw cannot receive the message even when imsg status --json reports a healthy bridge.chat.db row or imsg watch event before debugging OpenClaw sessions. Do not run this as a periodic bridge-relaunch loop; repeated imsg launch plus gateway restarts during active work can interrupt deliveries and strand in-flight channel runs.Gateway is not running on macOS
Gateway is not running on macOS
The default Then run:
cliPath: "imsg" must run on the Mac signed into Messages. On Linux or Windows, set channels.imessage.cliPath to a wrapper script that SSHes to that Mac and runs imsg "$@".DMs are ignored
DMs are ignored
Check:
channels.imessage.dmPolicychannels.imessage.allowFrom- pairing approvals (
openclaw pairing list imessage)
Group messages are ignored
Group messages are ignored
Check:
channels.imessage.groupPolicychannels.imessage.groupAllowFromchannels.imessage.groupsallowlist behavior- mention pattern configuration (
agents.list[].groupChat.mentionPatterns)
Remote attachments fail
Remote attachments fail
Check:
channels.imessage.remoteHostchannels.imessage.remoteAttachmentRoots- SSH/SCP key auth from the gateway host
- host key exists in
~/.ssh/known_hostson the gateway host - remote path readability on the Mac running Messages
macOS permission prompts were missed
macOS permission prompts were missed
Re-run in an interactive GUI terminal in the same user/session context and approve prompts:Confirm Full Disk Access + Automation are granted for the process context that runs OpenClaw/
imsg.Configuration reference pointers
Related
- Channels Overview — all supported channels
- BlueBubbles removal and the imsg iMessage path — announcement and migration summary
- Coming from BlueBubbles — config translation table and step-by-step cutover
- Pairing — DM authentication and pairing flow
- Groups — group chat behavior and mention gating
- Channel Routing — session routing for messages
- Security — access model and hardening