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 imsg should both run on the same signed-in macOS Messages host. When the Gateway is hosted elsewhere, direct channels.imessage.cliPath to a transparent SSH wrapper that executes imsg on 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.bluebubbles configurations over to channels.imessage; iMessage support in OpenClaw is provided exclusively through imsg. 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

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 cliPath wrapper or SSH proxy placed in front of imsg must 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 shell read) 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 when imsg rpc itself is functioning normally. ssh -T host imsg "$@" (above) avoids this because it forwards OpenClaw's cliPath arguments, including rpc and --db. Pipelines like ssh host imsg | grep -v '^DEBUG' are unsafe: line-buffered tools can still hold frames, so apply stdbuf -oL -eL to every stage if filtering is necessary.

Requirements and permissions (macOS)

  • The Mac running imsg must have messages signed in.
  • The process context executing OpenClaw or imsg needs 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 imsg bridge, 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 send succeeds through the exact wrapper before enabling the channel. If Automation cannot be granted, reconfigure to a single-user imsg setup 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 fresh brew install steipete/tap/imsg plus the standard macOS permissions above delivers out of the box.
  • Private API mode: imsg injects a helper dylib into Messages.app to call internal IMCore functions. This enables react, edit, unsend, reply (threaded), sendWithEffect, poll and poll-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 into Messages.app. imsg launch refuses 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

  1. Install (or upgrade) imsg on 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.

  1. Turn off System Integrity Protection, and on newer macOS releases, also Library Validation. Injecting a third-party helper dylib into the Apple-signed Messages.app requires 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 disable by itself is typically insufficient. Apple still enforces library validation on Messages.app because 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 true
    

    macOS 26 (Tahoe), confirmed on 26.5.1: disabling SIP plus running the DisableLibraryValidation command 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 launch injects successfully, and imsg status reports advanced_features: true.
    • Without the plist (even with SIP off): imsg launch fails with Failed 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 launch injection or specific selectors calls 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, gather imsg status --json along with the imsg launch output and submit it to the imsg project rather than disabling additional system-wide security protections.

  2. Inject the helper. With SIP disabled and Messages.app signed in:

    imsg launch
    

    imsg launch refuses to inject while SIP is enabled, so this also serves as a check that step 2 was completed.

  3. Verify the bridge from OpenClaw:

    openclaw channels status --probe
    

    The iMessage entry should report works, and imsg status --json | jq '{rpc_methods, selectors}' should display the capabilities your macOS build exposes. Poll creation requires selectors.pollPayloadMessage; voting needs both selectors.pollVoteMessage and the poll.vote RPC 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:

  • imsg falls 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 one allowFrom entry)
  • open (requires allowFrom to 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)
  • open
  • disabled

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:

  1. Sender allowlist (channels.imessage.groupAllowFrom), handle, accessGroup:<name>, chat_guid, chat_identifier, or chat_id. When the effective list is empty (neither groupAllowFrom nor allowFrom fallback exists), every group sender is rejected.
  2. Group registry (channels.imessage.groups), active once the map contains entries: the chat must match a specific per-chat_id entry or a groups: { "*": { ... } } wildcard. If groups is 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 configuring channels.imessage.groupAllowFrom (or allowFrom); populating groups entries alone keeps gate 1 blocking all senders.
  • once per chat_id at runtime, when a sender clears gate 1 but the chat is absent from a non-empty groups registry: imessage: dropping group message from chat_id=<id> ..., resolve by adding that chat_id (or "*") under channels.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 } },
    },
  },
}

groupAllowFrom by itself permits those senders in any group; include the groups block to restrict which chats are allowed (and to define per-chat settings such as requireMention).

Mention gating for groups:

  • iMessage provides no native mention metadata
  • mention detection relies on regex patterns (agents.entries.*.groupChat.mentionPatterns, fallback messages.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:

  1. Group-specific system prompt (groups["<chat_id>"].systemPrompt): this one applies when a matching entry for the group exists in the map and its systemPrompt key has a value. If systemPrompt comes through as an empty string (""), the wildcard gets suppressed, and no system prompt is assigned to that group.
  2. 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 a systemPrompt key.
{
  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 here within the DM or an allowed group chat.
  • Subsequent messages in that same iMessage conversation get directed to the spawned ACP session.
  • /new and /reset reset the bound ACP session in place.
  • /acp close terminates 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 +15555550123 or user@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:

  1. Create or sign into a dedicated macOS user.
  2. In that user, sign into Messages with the bot Apple ID.
  3. Install imsg under that user.
  4. Build an SSH wrapper so OpenClaw can run imsg in that user's context.
  5. Direct channels.imessage.accounts.<id>.cliPath and .dbPath to 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 imsg runs on a Mac within your tailnet
  • cliPath wrapper relies on SSH to execute imsg
  • remoteHost allows 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: true to 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 no Inbound message log line at all.
  • remote inbound attachment paths can be fetched via SCP when remoteHost is set
  • outbound files are staged into an owner-only temporary path on the configured or auto-detected Messages Mac, passed to imsg by 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.chunkMode
    • length (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 (auto default, bridge, applescript) selects how imsg delivers 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 SMS
  • imessage:<contact> requires iMessage
  • sms:<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, text or message, along with chatGuid, chatId, chatIdentifier, or to). For local reply-with-attachment, an imsg build is required, and its send-rich must support --file. With remote imsg v0.13.4, attachment replies rely on JSON-RPC and can target the full message or a part index 0; nonzero attachment part indices are not supported by the RPC method.
  • sendWithEffect: Deliver text with an iMessage effect (text or message, effect or effectId). 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, text or newText). 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 (buffer as base64 or a hydrated media/path/filePath, filename, optional asVoice). The legacy alias is sendAttachment.
  • 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.admin Gateway client.
  • poll: Generate a native Apple Messages poll (pollQuestion, pollOption repeated 2 to 12 times, plus chatGuid, chatId, chatIdentifier, or to). Recipients on iOS/iPadOS/macOS 26+ can view and vote natively; older OS versions see a "Sent a poll" text fallback. selectors.pollPayloadMessage is required.
  • poll-vote: Cast a vote on an existing poll (pollId or messageId, plus exactly one of pollOptionIndex, pollOptionId, or pollOptionText). This needs selectors.pollVoteMessage and the poll.vote RPC method. Remote imsg v0.13.4 RPC only accepts the option ID, so remote setups must use pollOptionId; 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 imsg builds lacking poll send --no-comment fall 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) into allow-once and 👎 (Dislike) into deny. It also offers /approve <id> <decision> commands, with allow-always included 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=true tapback (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.db rowid (a persisted per-account cursor) and passes it to imsg watch.subscribe as since_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.dmPolicy
  • channels.imessage.allowFrom
  • pairing approval status (openclaw pairing list imessage)

Group messages are ignored

Verify the following:

  • channels.imessage.groupPolicy
  • channels.imessage.groupAllowFrom
  • channels.imessage.groups allowlist behavior
  • mention pattern configuration (agents.entries.*.groupChat.mentionPatterns)

Remote attachments fail

Verify the following:

  • channels.imessage.remoteHost
  • channels.imessage.remoteAttachmentRoots
  • SSH/SCP key authentication from the gateway host
  • host key present in ~/.ssh/known_hosts on 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

6,388 words · updated Sep 1, 2026