iMessage Channel: Native Integration via imsg for OpenClaw
Configure OpenClaw's native iMessage channel using imsg, covering private API actions, group management, and automatic inbound recovery. Essential for new iMessage setups on macOS hosts.
Read this when
- Setting up iMessage support
- Debugging iMessage send/receive
Note
For the standard OpenClaw iMessage setup, the Gateway and
imsgshould both run on the same signed-in macOS Messages host. When the Gateway is hosted elsewhere, directchannels.imessage.cliPathto a transparent SSH wrapper that executesimsgon the Mac.Inbound recovery happens automatically. Once a bridge or gateway restarts, iMessage replays any messages that were missed during the downtime and filters out the stale "backlog bomb" that Apple may flush following a Push recovery, with deduplication preventing double delivery. No configuration is needed to activate this, see Inbound recovery after a bridge or gateway restart for details.
Warning
BlueBubbles support has been dropped. Move
channels.bluebubblesconfigurations over tochannels.imessage; iMessage support in OpenClaw is provided exclusively throughimsg. Check BlueBubbles removal and the imsg iMessage path for the brief notice, or Coming from BlueBubbles for the complete migration reference.
Status: native external CLI integration. The Gateway launches imsg rpc and communicates via JSON-RPC over stdio, with no separate daemon or port involved. Private API mode is strongly recommended for a fully functional iMessage channel; features like replies, tapbacks, effects, polls, attachment replies, and group actions depend on imsg launch plus a successful private API probe.
For the typical local configuration, OpenClaw setup may offer a user-approved Homebrew install or update for imsg on the signed-in Messages Mac. Manual setup and SSH-wrapper topologies stay operator-managed: install or update imsg under the same user context that will execute the Gateway or wrapper.
Install the plugin
Install the official iMessage plugin on the Gateway host, then restart the Gateway:
openclaw plugins install @openclaw/imessage
-
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)
Install and verify imsg
brew install steipete/tap/imsg
brew update && brew upgrade imsg
imsg rpc --help
imsg launch
openclaw channels status --probe
When the local setup wizard finds that the default imsg command is missing, it may offer to install steipete/tap/imsg via Homebrew. If it detects a Homebrew-managed imsg, it may offer to reinstall or update it. Custom cliPath wrappers are left untouched.
Configure OpenClaw
{
channels: {
imessage: {
enabled: true,
cliPath: "/usr/local/bin/imsg",
dbPath: "/Users/user/Library/Messages/chat.db",
},
},
}
Start gateway
openclaw gateway
Approve first DM pairing (default dmPolicy)
openclaw pairing list imessage
openclaw pairing approve imessage <CODE>
Pairing requests expire after 1 hour.
Remote Mac over SSH
Most setups do not require SSH. Choose this topology only when the Gateway cannot operate on the signed-in Messages Mac. Point cliPath at a stdio-compatible wrapper on the Gateway host that SSHes to the Messages Mac and runs imsg. Use the wrapper's absolute path so service launches do not rely on shell home expansion.
Install and update imsg on that remote Mac, not on the Gateway host:
ssh messages-mac 'brew install steipete/tap/imsg && brew update && brew upgrade imsg'
#!/usr/bin/env bash
exec ssh -T messages-mac imsg "$@"
Recommended config when attachments are enabled:
{
channels: {
imessage: {
enabled: true,
cliPath: "/home/openclaw/.openclaw/scripts/imsg-ssh",
remoteHost: "user@messages-mac", // Mac that runs Messages.app and imsg
// This path is interpreted on the Messages Mac, not on the Gateway host.
dbPath: "/Users/user/Library/Messages/chat.db",
includeAttachments: true,
// Optional: extra allowed attachment roots (merged with the default
// /Users/*/Library/Messages/Attachments).
attachmentRoots: ["/Users/*/Library/Messages/Attachments"],
remoteAttachmentRoots: ["/Users/*/Library/Messages/Attachments"],
},
},
}
remoteHost identifies the Messages Mac. OpenClaw relies on it for both inbound attachment fetches and outbound attachment staging. For outbound files, OpenClaw creates an owner-only temporary path on that Mac, copies the file over the existing strict SSH/SCP transport, passes only the remote path to imsg, and attempts removal after success, failure, or timeout. A failed cleanup SSH call emits a warning and can leave the owner-only temporary directory behind.
An explicit remoteHost is recommended and wins when set. For compatibility, OpenClaw auto-detects the existing transparent exec ssh ... imsg "$@" wrapper shape once per process and reuses that host across monitoring, probes, sends, and private actions. Auto-detection covers only the simple documented transparent wrapper; option-rich wrappers such as ProxyJump/ProxyCommand must configure remoteHost.
remoteHost must be host or user@host (no spaces or SSH options); unsafe values are ignored.
OpenClaw uses strict host-key checking for SSH/SCP, so the Messages Mac host key must already exist in ~/.ssh/known_hosts on the Gateway host.
Attachment paths are validated against allowed roots (attachmentRoots / remoteAttachmentRoots).
Warning
Any
cliPathwrapper or SSH proxy placed in front ofimsgmust act as a transparent stdio pipe for long-lived JSON-RPC. Throughout the channel's lifetime, OpenClaw sends small newline-framed JSON-RPC messages through the wrapper's stdin and stdout:
- Relay each stdin chunk or line the moment bytes arrive, without waiting for EOF.
- Immediately relay each stdout chunk or line in the opposite direction.
- Keep newlines intact.
- Steer clear of fixed-size blocking reads (
read(4096),cat | buffer, default shellread) that can starve small frames.- Isolate stderr from the JSON-RPC stdout stream.
If a wrapper buffers stdin until a large block fills, the resulting symptoms can mimic an iMessage outage, such as
imsg rpc timeout (chats.list)or repeated channel restarts, even whenimsg rpcitself is functioning normally.ssh -T host imsg "$@"(above) avoids this because it forwards OpenClaw'scliPatharguments, includingrpcand--db. Pipelines likessh host imsg | grep -v '^DEBUG'are unsafe: line-buffered tools can still hold frames, so applystdbuf -oL -eLto every stage if filtering is necessary.
Requirements and permissions (macOS)
- The Mac running
imsgmust have messages signed in. - The process context executing OpenClaw or
imsgneeds Full Disk Access (for Messages DB access). - Sending messages via Messages.app requires Automation permission.
- For advanced actions (react / edit / unsend / threaded reply / effects / polls / group ops), System Integrity Protection must be turned off, as described in Enabling the imsg private API. Basic text and media send/receive function without it.
Tip
Permissions apply per process context. When the gateway runs headless (LaunchAgent/SSH), run a one-time interactive command in that same context to trigger the prompts:
imsg chats --limit 1 # or imsg send <handle> "test"
SSH wrapper sends fail with AppleEvents -1743
With a remote-SSH setup, reading chats, passing channels status --probe, and handling inbound messages can all work while outbound sends still fail with an AppleEvents authorization error:
Not authorized to send Apple events to Messages. (-1743)
Inspect the signed-in Mac user's TCC database or System Settings > Privacy & Security > Automation. If the Automation entry is logged for /usr/libexec/sshd-keygen-wrapper rather than the imsg or local shell process, macOS may not show a usable Messages toggle for that SSH server-side client:
kTCCServiceAppleEvents | /usr/libexec/sshd-keygen-wrapper | auth_value=0 | com.apple.MobileSMS
In this situation, repeating tccutil reset AppleEvents or rerunning imsg send through the same SSH wrapper may continue to fail, because the process context needing Messages Automation is the SSH wrapper, not an app the UI can grant.
Instead, use one of the supported imsg process contexts:
- 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 keeping the two-user SSH topology, confirm that a real outbound
imsg sendsucceeds through the exact wrapper before enabling the channel. If Automation cannot be granted, reconfigure to a single-userimsgsetup rather than depending on the SSH wrapper for sends.
Enabling the imsg private API
imsg comes in two operational modes. For OpenClaw, Private API mode is the recommended choice because it provides the native iMessage actions users expect. Basic mode remains suitable for low-risk installs, initial verification, or hosts where SIP cannot be disabled.
- Basic mode (default, no SIP changes needed): outbound text and media via
send, inbound watch/history, chat list. This is what a freshbrew install steipete/tap/imsgplus the standard macOS permissions above delivers out of the box. - Private API mode:
imsginjects a helper dylib intoMessages.appto call internalIMCorefunctions. This enablesreact,edit,unsend,reply(threaded),sendWithEffect,pollandpoll-vote(native Messages polls),renameGroup,setGroupIcon,addParticipant,removeParticipant,leaveGroup, plus typing indicators and read receipts.
The recommended action surface on this page requires Private API mode. The imsg README states the requirement clearly:
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 intoMessages.app.imsg launchrefuses to inject when SIP is enabled.
The helper-injection technique relies on imsg's own dylib to reach Messages private APIs. No third-party server or BlueBubbles runtime exists in the OpenClaw iMessage path.
Warning
Disabling SIP is a real security tradeoff. SIP is one of macOS's core protections against running modified system code; turning it off system-wide opens up additional attack surface and side effects. Notably, disabling SIP on Apple Silicon Macs also disables the ability to install and run iOS apps on your Mac.
Treat this as a deliberate operational choice, especially on a primary personal Mac. For production-quality OpenClaw iMessage, prefer a dedicated Mac or bot macOS user where you are comfortable enabling the bridge. If your threat model cannot tolerate SIP being off anywhere, the iMessage plugin is limited to basic mode, text and media send/receive only, no reactions / edit / unsend / effects / group ops.
Setup
-
Install (or upgrade)
imsgon the Mac that runs Messages.app:brew install steipete/tap/imsg brew update && brew upgrade imsg imsg --version imsg status --json
The imsg status --json output provides bridge_version, rpc_methods, and method-specific selectors data, giving you a clear picture of what the current build supports before you begin.
-
Turn off System Integrity Protection, and on newer macOS releases, also Library Validation. Injecting a third-party helper dylib into the Apple-signed
Messages.apprequires SIP to be off and library validation to be relaxed. The Recovery-mode SIP procedure varies by macOS version:- macOS 10.13-10.15 (Sierra-Catalina): turn off Library Validation through Terminal, reboot into Recovery Mode, execute
csrutil disable, then restart. - macOS 11+ (Big Sur and later), Intel: use Recovery Mode (or Internet Recovery), run
csrutil disable, and restart. - macOS 11+, Apple Silicon: use the power-button startup sequence to enter Recovery; on recent macOS versions, hold the Left Shift key when you click Continue, then run
csrutil disable. Virtual-machine environments follow a different procedure, so create a VM snapshot beforehand.
On macOS 11 and later, turning off
csrutil disableby itself is typically insufficient. Apple still enforces library validation onMessages.appbecause it is a platform binary, so an adhoc-signed helper gets rejected (Library Validation failed: ... platform binary, but mapped file is not) even when SIP is disabled. After disabling SIP, you must also disable library validation and reboot:sudo defaults write /Library/Preferences/com.apple.security.libraryvalidation.plist DisableLibraryValidation -bool truemacOS 26 (Tahoe), confirmed on 26.5.1: disabling SIP plus running the
DisableLibraryValidationcommand above is enough to inject the helper across versions 26.0 through 26.5.x. No boot-args are needed. The plist is the key factor, and its absence is the most frequent reason injection fails on Tahoe:- With the plist:
imsg launchinjects successfully, andimsg statusreportsadvanced_features: true. - Without the plist (even with SIP off):
imsg launchfails withFailed to launch: Timeout waiting for Messages.app to initialize. AMFI blocks the adhoc helper during load, so the bridge never becomes ready and the launch times out. That timeout is the symptom most users encounter on Tahoe; the solution is the plist above, not anything more aggressive.
If
imsg launchinjection or specificselectorscalls start returning false after a macOS upgrade, this gate is the likely culprit. Verify your SIP and library-validation settings before assuming the SIP step itself failed. If those settings are correct and the bridge still cannot inject, gatherimsg status --jsonalong with theimsg launchoutput and submit it to theimsgproject rather than disabling additional system-wide security protections. - macOS 10.13-10.15 (Sierra-Catalina): turn off Library Validation through Terminal, reboot into Recovery Mode, execute
-
Inject the helper. With SIP disabled and Messages.app signed in:
imsg launchimsg launchrefuses to inject while SIP is enabled, so this also serves as a check that step 2 was completed. -
Verify the bridge from OpenClaw:
openclaw channels status --probeThe iMessage entry should report
works, andimsg status --json | jq '{rpc_methods, selectors}'should display the capabilities your macOS build exposes. Poll creation requiresselectors.pollPayloadMessage; voting needs bothselectors.pollVoteMessageand thepoll.voteRPC method. The OpenClaw plugin only advertises actions supported by the cached probe, while an empty cache stays optimistic and probes on the first dispatch.
If 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 drop out (due to 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 does not fit your threat model:
imsgfalls back to basic mode, which handles text, media, and receive only.- The OpenClaw plugin still advertises text/media send and inbound monitoring; it hides
react,edit,unsend,reply,sendWithEffect, and group operations from the action surface (following 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
channels.imessage.dmPolicy controls direct messages:
pairing(default)allowlist(requires at least oneallowFromentry)open(requiresallowFromto include"*")disabled
Allowlist field: channels.imessage.allowFrom.
Allowlist configuration requires each entry to specify a sender: either a handle or a static sender access group (accessGroup:<name>). For chat destinations like chat_id:*, chat_guid:*, or chat_identifier:*, apply channels.imessage.groupAllowFrom; for numeric chat_id registry keys, use channels.imessage.groups.
Group policy and mentions
Group handling is governed by channels.imessage.groupPolicy:
allowlist(default)opendisabled
The group sender allowlist is defined via channels.imessage.groupAllowFrom.
Entries in groupAllowFrom may also point to static sender access groups (accessGroup:<name>).
Fallback at runtime: when groupAllowFrom is not configured, group sender checks in iMessage rely on allowFrom; if DM and group admission need separate behavior, set groupAllowFrom. An intentionally empty groupAllowFrom: [] does not trigger fallback, it blocks all group senders under allowlist.
Runtime note: if channels.imessage is absent entirely, the runtime defaults to groupPolicy="allowlist" and emits a warning (even when channels.defaults.groupPolicy is present).
Warning
Under
groupPolicy: "allowlist", group routing applies two gates in sequence:
- Sender allowlist (
channels.imessage.groupAllowFrom), handle,accessGroup:<name>,chat_guid,chat_identifier, orchat_id. When the effective list is empty (neithergroupAllowFromnorallowFromfallback exists), every group sender is rejected.- Group registry (
channels.imessage.groups), active once the map contains entries: the chat must match a specific per-chat_identry or agroups: { "*": { ... } }wildcard. Ifgroupsis empty or absent, admission is decided solely by the sender allowlist.With no effective group sender allowlist in place, all group messages are discarded before reaching the registry gate. Each gate emits its own
warn-level signal at the default log level, and each points to a distinct remedy:
- once per account at startup, when the effective group sender allowlist is empty:
imessage: groupPolicy="allowlist" for account "<id>" but no group sender allowlist is configured ..., resolve by configuringchannels.imessage.groupAllowFrom(orallowFrom); populatinggroupsentries alone keeps gate 1 blocking all senders.- once per
chat_idat runtime, when a sender clears gate 1 but the chat is absent from a non-emptygroupsregistry:imessage: dropping group message from chat_id=<id> ..., resolve by adding thatchat_id(or"*") underchannels.imessage.groups.DMs are unaffected, as they follow a separate code path.
Suggested setup for group flow under
groupPolicy: "allowlist":{ channels: { imessage: { groupPolicy: "allowlist", groupAllowFrom: ["+15555550123"], groups: { "*": { "requireMention": true } }, }, }, }
groupAllowFromby itself permits those senders in any group; include thegroupsblock to restrict which chats are allowed (and to define per-chat settings such asrequireMention).
Mention gating for groups:
- iMessage provides no native mention metadata
- mention detection relies on regex patterns (
agents.entries.*.groupChat.mentionPatterns, fallbackmessages.groupChat.mentionPatterns) - without configured patterns, mention gating cannot be applied
- control commands from authorized senders skip mention gating
Per-group systemPrompt:
Each item under channels.imessage.groups.* can include an optional systemPrompt string, which is inserted into the agent's system prompt for every turn handling a message in that group. Resolution follows the same logic as channels.whatsapp.groups:
- Group-specific system prompt (
groups["<chat_id>"].systemPrompt): this one applies when a matching entry for the group exists in the map and itssystemPromptkey has a value. IfsystemPromptcomes through as an empty string (""), the wildcard gets suppressed, and no system prompt is assigned to that group. - Group wildcard system prompt (
groups["*"].systemPrompt): this is used when the map has no entry for the group at all, or when an entry exists but lacks asystemPromptkey.
{
channels: {
imessage: {
groupPolicy: "allowlist",
groupAllowFrom: ["+15555550123"],
groups: {
"*": { systemPrompt: "Use British spelling." },
"8421": {
requireMention: true,
systemPrompt: "This is the on-call rotation chat. Keep replies under 3 sentences.",
},
"9907": {
// explicit suppression: the wildcard "Use British spelling." does not apply here
systemPrompt: "",
},
},
},
},
}
Per-group prompts are limited to group messages; direct messages do not receive them.
Sessions and deterministic replies
- DMs follow direct routing, while groups follow group routing.
- With the default
session.dmScope=main, iMessage DMs are folded into the agent main session. - Group sessions stay isolated from each other (
agent:<agentId>:imessage:group:<chat_id>). - Replies are sent back to iMessage based on the originating channel/target metadata.
Group-ish thread behavior:
Certain multi-participant iMessage threads may show up with is_group=false.
If that chat_id is set explicitly under channels.imessage.groups, OpenClaw handles it as group traffic, which means group gating and group session isolation both apply.
ACP conversation bindings
iMessage chats can be linked to ACP sessions.
Fast operator flow:
- Execute
/acp spawn codex --bind herewithin the DM or an allowed group chat. - Subsequent messages in that same iMessage conversation get directed to the spawned ACP session.
/newand/resetreset the bound ACP session in place./acp closeterminates the ACP session and clears the binding.
Configured persistent bindings rely on top-level bindings[] entries that include type: "acp" and match.channel: "imessage".
match.peer.id supports:
- a normalized DM handle, for example
+15555550123oruser@example.com chat_id:<id>(the recommended option for stable group bindings)chat_guid:<guid>chat_identifier:<identifier>
Example:
{
agents: {
entries: {
codex: {
default: true,
runtime: {
type: "acp",
acp: { agent: "codex", backend: "acpx", mode: "persistent" },
},
},
},
},
bindings: [
{
type: "acp",
agentId: "codex",
match: {
channel: "imessage",
accountId: "default",
peer: { kind: "group", id: "chat_id:123" },
},
acp: { label: "codex-group" },
},
],
}
Check ACP Agents for shared ACP binding behavior.
Deployment patterns
Dedicated bot macOS user (separate iMessage identity)
Set up a separate Apple ID and macOS user so bot traffic stays separate from your personal Messages profile.
Typical flow:
- Create or sign into a dedicated macOS user.
- In that user, sign into Messages with the bot Apple ID.
- Install
imsgunder that user. - Build an SSH wrapper so OpenClaw can run
imsgin that user's context. - Direct
channels.imessage.accounts.<id>.cliPathand.dbPathto that user profile.
The first run may need GUI approvals (Automation and Full Disk Access) inside that bot user session.
Remote Mac over Tailscale (example)
Common topology:
- gateway operates on Linux/VM
- iMessage plus
imsgruns on a Mac within your tailnet cliPathwrapper relies on SSH to executeimsgremoteHostallows inbound fetches and owner-only outbound staging over SSH/SCP
Example:
{
channels: {
imessage: {
enabled: true,
cliPath: "/home/openclaw/.openclaw/scripts/imsg-ssh",
remoteHost: "bot@mac-mini.tailnet-1234.ts.net",
includeAttachments: true,
dbPath: "/Users/bot/Library/Messages/chat.db",
},
},
}
#!/usr/bin/env bash
exec ssh -T bot@mac-mini.tailnet-1234.ts.net imsg "$@"
cliPath is an absolute wrapper path local to the Gateway. remoteHost and dbPath point to the Messages Mac; do not rewrite the remote database path using the Gateway user's home directory.
Use SSH keys so both SSH and SCP run without interaction.
Make sure the host key is trusted beforehand (for instance ssh bot@mac-mini.tailnet-1234.ts.net) so that known_hosts gets filled in.
Multi-account pattern
iMessage supports per-account configuration under channels.imessage.accounts.
Each account can override fields like cliPath, dbPath, allowFrom, dmPolicy, groupPolicy, mediaMaxMb, history settings, and attachment root allowlists. When an account omits a policy, it inherits the channel root; when an account sets one explicitly, that wins. If neither scope defines them, DMs fall back to pairing and groups to allowlist.
Direct-message history
Set channels.imessage.dmHistoryLimit to populate new direct-message threads with recent decoded imsg history for that specific conversation. Per-sender overrides are handled via channels.imessage.dms["<sender>"].historyLimit, where 0 can turn history off for a given sender.
DM history is pulled on demand from imsg. If dmHistoryLimit stays unset, global DM history seeding is disabled; however, a positive per-sender channels.imessage.dms["<sender>"].historyLimit still activates seeding for that sender.
Media, chunking, and delivery targets
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 inbound attachment paths can be fetched via SCP when
remoteHostis set - outbound files are staged into an owner-only temporary path on the configured or auto-detected Messages Mac, passed to
imsgby that remote path, and cleaned up best-effort after success, failure, or timeout; cleanup failure emits a warning and can leave owner-only residue - 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
- text chunk limit:
channels.imessage.textChunkLimit(default 4000) - chunk mode:
channels.imessage.streaming.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
Preferred explicit targets:
chat_id:123(recommended for stable routing)chat_guid:...chat_identifier:...
Direct handles are also supported:
+1555...tel:+1555...imessage:+1555...sms:+1555...user@example.com
Use a service-qualified target for a contact name or mixed alphanumeric alias:
auto:<contact>lets Messages choose iMessage or SMSimessage:<contact>requires iMessagesms:<contact>requires SMS
Bare contact names and mixed alphanumeric aliases are rejected instead of being converted to
a phone number. If an existing automation uses one, add auto:, imessage:, or sms: to
make the intended delivery service explicit.
imsg chats --limit 20
Private API actions
When imsg 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:
{
channels: {
imessage: {
actions: {
reactions: true,
edit: true,
unsend: true,
reply: true,
sendWithEffect: true,
sendAttachment: true,
renameGroup: true,
setGroupIcon: true,
addParticipant: true,
removeParticipant: true,
leaveGroup: true,
polls: true,
},
},
},
}
Available actions
- react: Manage iMessage tapbacks by adding or removing them (
messageId,emoji,remove). The supported reactions correspond to love, like, dislike, laugh, emphasize, and question. If you remove one without specifying an emoji, the existing tapback gets cleared. - reply: Send a threaded response to a prior message (
messageId,textormessage, along withchatGuid,chatId,chatIdentifier, orto). For local reply-with-attachment, animsgbuild is required, and itssend-richmust support--file. With remoteimsgv0.13.4, attachment replies rely on JSON-RPC and can target the full message or a part index0; nonzero attachment part indices are not supported by the RPC method. - sendWithEffect: Deliver text with an iMessage effect (
textormessage,effectoreffectId). Short names include slam, loud, gentle, invisibleink, confetti, lasers, fireworks, balloon, heart, echo, happybirthday, shootingstar, sparkles, spotlight. - edit: Modify a sent message on macOS/private API versions that support it (
messageId,textornewText). Editing is limited to messages that the gateway itself transmitted. - unsend: Withdraw a sent message on macOS/private API versions that support it (
messageId). Only messages sent by the gateway itself can be withdrawn. - upload-file: Transmit media or files (
bufferas base64 or a hydratedmedia/path/filePath,filename, optionalasVoice). The legacy alias issendAttachment. - renameGroup, setGroupIcon, addParticipant, removeParticipant, leaveGroup: Handle group chat administration when the current target is a group conversation. These operations alter the host's Messages identity, so they demand an owner sender or an
operator.adminGateway client. - poll: Generate a native Apple Messages poll (
pollQuestion,pollOptionrepeated 2 to 12 times, pluschatGuid,chatId,chatIdentifier, orto). Recipients on iOS/iPadOS/macOS 26+ can view and vote natively; older OS versions see a "Sent a poll" text fallback.selectors.pollPayloadMessageis required. - poll-vote: Cast a vote on an existing poll (
pollIdormessageId, plus exactly one ofpollOptionIndex,pollOptionId, orpollOptionText). This needsselectors.pollVoteMessageand thepoll.voteRPC method. Remoteimsgv0.13.4 RPC only accepts the option ID, so remote setups must usepollOptionId; index and text selectors remain available for local setups.
Accepted inbound polls are shown to the agent with the question, option labels, vote counts, and the poll message ID that poll-vote needs. Remote accounts additionally include each stable option ID and instruct the agent to use pollOptionId.
Message IDs
Inbound iMessage context provides both short MessageSid values and full message GUIDs (MessageSidFull) when they are available. Short IDs are limited to the recent SQLite-backed reply cache and are validated against the current chat before use. If a short ID expires, retry with its MessageSidFull while targeting the conversation that provided it. Full IDs do not skip conversation or account binding, so replace an ID from another chat with one from the current target. Remote delegated calls may reject stale full IDs when current-conversation evidence is missing.
Capability detection
OpenClaw hides private API actions only when the cached probe status indicates the bridge is unavailable. If the status is unknown, actions stay visible and dispatch probes lazily, allowing the first action to succeed after imsg launch without a separate manual status refresh.
Read receipts and typing
When the private API bridge is active, accepted inbound chats are marked as read, and direct chats show a typing bubble as soon as the turn is accepted, while the agent prepares context and generates. To disable read-marking, use:
{
channels: {
imessage: {
sendReadReceipts: false,
},
},
}
Older imsg builds that predate the per-method capability list gate off typing/read silently; OpenClaw logs a one-time warning per restart so the missing receipt can be traced.
Inbound tapbacks
OpenClaw subscribes to iMessage tapbacks and routes accepted reactions as system events rather than regular message text, so a user tapback does not trigger a standard reply loop.
Notification mode is governed by channels.imessage.reactionNotifications:
"own"(default): alert only when users react to messages the bot authored."all": alert on every inbound tapback coming from approved senders."off": disregard inbound tapbacks.
Overrides on a per-account basis rely on channels.imessage.accounts.<id>.reactionNotifications.
Approval polls and reactions
When approvals.exec.enabled or approvals.plugin.enabled is set and the request reaches iMessage through its native route, the gateway presents an approval prompt using native controls:
- On a probed private API bridge that supports polls and caption suppression, the prompt shows a Messages poll listing each permitted decision. Older
imsgbuilds lackingpoll send --no-commentfall back to text controls. - If polls are turned off via
channels.imessage.actions.polls: false, the bridge cannot handle polls, the poll send fails, or fewer than two decisions exist, the prompt falls back to text and tapback controls. - The text fallback translates
👍(Like) intoallow-onceand👎(Dislike) intodeny. It also offers/approve <id> <decision>commands, withallow-alwaysincluded when the request allows it.
For poll votes and reactions to work, the acting user's handle must be listed as an explicit approver. The approver list comes from channels.imessage.allowFrom (or channels.imessage.accounts.<id>.allowFrom); add the user's phone number in E.164 format or their Apple ID email (chat targets such as chat_id:* are not valid approver entries). The wildcard "*" is respected but lets any sender approve; an empty approver list disables poll and reaction shortcuts completely. These shortcuts deliberately skip reactionNotifications, dmPolicy, and groupAllowFrom because the explicit-approver allowlist is the only gate controlling approval resolution.
Native poll controls are restricted to channel-native delivery within the original iMessage session or an iMessage approver DM. Explicit forwarding targets chosen by approvals.exec.mode: "targets" (and the target half of "both") keep using the existing forwarded approval message rather than an iMessage poll.
/approve text command authorization follows the same list: when channels.imessage.allowFrom has entries, /approve <id> <decision> is checked against that approver list (not the broader DM allowlist), and senders allowed on the DM allowlist but absent from allowFrom get a clear denial. When allowFrom is empty, the same-chat fallback remains active and /approve authorizes anyone the DM allowlist permits. Add every operator who should approve, whether via /approve or via reactions, to allowFrom.
Operator notes:
- Poll and reaction bindings live both in memory and in the gateway's persistent keyed store (TTL matches the approval expiry), and the gateway also polls pending prompts for tapbacks. After a gateway restart, tapping an old control is recognized and swallowed instead of reaching agent chat, but the restart terminates the in-flight command; request a new approval instead of expecting the old control to resume.
- The operator's own
is_from_me=truetapback (for instance from a paired Apple device) resolves the approval when that handle is an explicit approver. - Approval prompts enter 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 lack a message GUID; reaction resolution needs the structured tapback metadata that current macOS / iOS clients emit.
Question reactions (1️⃣ / 2️⃣ / 3️⃣ / 4️⃣)
For an ask_user prompt with one non-secret, single-select question and one to four options, OpenClaw adds numbered emoji choices. React to the delivered prompt with the matching number to answer. The reaction must carry the stable GUID of the bot-authored message; OpenClaw then maps the number to the canonical option through the Gateway. Stale or duplicate taps are ignored.
Multi-question, multi-select, and free-text prompts remain text-reply-only. Question reactions follow normal iMessage DM/group admission rules. They are recognized even when general reactionNotifications is "off", without turning unrelated reactions into agent events.
Config writes
iMessage allows channel-initiated config writes by default (for /config set|unset when commands.config: true).
Disable:
{
channels: {
imessage: {
configWrites: false,
},
},
}
Coalescing split-send DMs (command + URL in one composition)
Apple can store a command and its URL preview as separate physical chat.db rows. imsg 0.13.1 and newer coalesces those rows before watch, history, or search returns the message, so OpenClaw receives one logical inbound message without adding channel-specific DM latency.
No iMessage coalescing setting is needed. The retired channels.imessage.coalesceSameSenderDms key is removed by openclaw doctor --fix. Generic messages.inbound debounce remains available when you intentionally want to batch rapid text messages across a channel.
If command-plus-URL sends arrive as separate agent turns, update imsg on the Messages Mac:
brew update && brew upgrade imsg
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 durable ingress plus an age fence.
- Durable replay protection. Before advancing the recovery cursor, OpenClaw journals each raw row in the shared SQLite ingress queue with its Apple GUID as the event ID. A completed row leaves a tombstone for about 4 hours, capped at 10,000 entries, so a replay with the same GUID is dropped even after a restart. A pending row stays recoverable until dispatch adopts it.
- Downtime recovery. On startup the monitor remembers the last durably admitted
chat.dbrowid (a persisted per-account cursor) and passes it toimsg watch.subscribeassince_rowid, so imsg replays rows that were not yet journaled and then tails live. Rows journaled before a crash resume from SQLite. Replay is bounded to the most recent 500 rows and to messages up to ~2 hours old, and GUID tombstones drop 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.
Recovery works over both local and remote 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
Backlogged messages are recorded at the default logging level and are never discarded without a trace. The recovery flag indicates which window was applied:
imessage: suppressed stale inbound backlog account=<id> sent=<iso> recovery=<bool> (<N> suppressed since start)
Migration
The channels.imessage.catchup.* option is no longer supported. Recovery from downtime happens automatically, and fresh installations require no configuration. For existing setups that still reference catchup.enabled: true, that setting continues to work as a compatibility profile controlling the replay window. Catchup blocking through enabled: false or the absence of enabled: true is no longer available; use openclaw doctor --fix to clear those entries.
Troubleshooting
imsg not found or RPC unsupported
Confirm that the binary and RPC are functional:
imsg rpc --help
imsg status --json
openclaw channels status --probe
When the probe says RPC is not supported, adjust imsg. If private API actions are missing, execute imsg launch within the macOS user session that is logged in, then probe once more. When the Gateway is absent from macOS, rely on the Remote Mac over SSH setup described earlier rather than the default local imsg location.
Messages send but inbound iMessages do not arrive
Start by checking whether the message actually arrived on the local Mac. If chat.db stays unchanged, OpenClaw cannot receive the message even when imsg status --json claims the bridge is healthy.
imsg chats --limit 10 --json
imsg watch --chat-id <chat-id> --json
sqlite3 ~/Library/Messages/chat.db \
"select datetime(max(date)/1000000000 + 978307200, 'unixepoch', 'localtime'), max(ROWID) from message;"
When phone-originated messages fail to produce new rows, fix the macOS Messages and Apple Push components before touching OpenClaw settings. A single service refresh frequently resolves the issue:
launchctl kickstart -k system/com.apple.apsd
launchctl kickstart -k gui/$(id -u)/com.apple.CommCenter
launchctl kickstart -k gui/$(id -u)/com.apple.identityservicesd
launchctl kickstart -k gui/$(id -u)/com.apple.imagent
imsg launch
openclaw gateway restart
Send another iMessage from the phone and verify that a fresh chat.db row or imsg watch event appears before you start debugging OpenClaw sessions. Avoid turning this into a recurring bridge-relaunch routine; frequent imsg launch combined with gateway restarts during active operations can disrupt message delivery and leave channel runs stuck.
Gateway is not running on macOS
The default cliPath: "imsg" needs to execute on the Mac that is signed into Messages. For Linux or Windows, point channels.imessage.cliPath at a wrapper script that SSHes into that Mac and invokes imsg "$@".
#!/usr/bin/env bash
exec ssh -T messages-mac imsg "$@"
Afterwards, execute:
openclaw channels status --probe --channel imessage
DMs are ignored
Verify the following:
channels.imessage.dmPolicychannels.imessage.allowFrom- pairing approval status (
openclaw pairing list imessage)
Group messages are ignored
Verify the following:
channels.imessage.groupPolicychannels.imessage.groupAllowFromchannels.imessage.groupsallowlist behavior- mention pattern configuration (
agents.entries.*.groupChat.mentionPatterns)
Remote attachments fail
Verify the following:
channels.imessage.remoteHostchannels.imessage.remoteAttachmentRoots- SSH/SCP key authentication from the gateway host
- host key present in
~/.ssh/known_hostson the gateway host - remote path readability on the Mac that runs Messages
macOS permission prompts were missed
Run again inside an interactive GUI terminal under the same user and session context, then accept any prompts:
imsg chats --limit 1
imsg send <handle> "test"
Ensure Full Disk Access and Automation are granted to the process context that executes 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