imessage plugin, which drives steipete/imsg over JSON-RPC and reaches the same private API surface BlueBubbles had (react, edit, unsend, reply, sendWithEffect, native polls, group management, attachments). One CLI binary replaces the BlueBubbles server + client app + webhook plumbing: no REST endpoint, no webhook auth.
This guide migrates old channels.bluebubbles configs to channels.imessage. There is no other supported migration path. On current OpenClaw a leftover channels.bluebubbles block is inert — no runtime reads it.
For the short announcement and operator summary, see BlueBubbles removal and the imsg iMessage path.
Migration checklist
The shortest safe path when you already know your old BlueBubbles config:- Verify
imsgdirectly on the Mac that runs Messages.app (imsg chats,imsg history,imsg send,imsg rpc --help). - Copy behavior keys from
channels.bluebubblestochannels.imessage:dmPolicy,allowFrom,groupPolicy,groupAllowFrom,groups,includeAttachments,attachmentRoots,mediaMaxMb,textChunkLimit,coalesceSameSenderDms, andactions. - Drop transport keys that no longer exist:
serverUrl,password, webhook URLs, and BlueBubbles server setup. - If the Gateway is not running on the Messages Mac, set
channels.imessage.cliPathto an SSH wrapper and setremoteHostfor remote attachment fetches. - Enable
channels.imessage, restart the Gateway, then runopenclaw channels status --probe --channel imessage. - Test one DM, one allowed group, attachments if enabled, and every private API action you expect the agent to use.
- Delete the BlueBubbles server and the old
channels.bluebubblesconfig after the iMessage path is verified.
What imsg does
imsg is a local macOS CLI for Messages. OpenClaw starts imsg rpc as a child process and talks JSON-RPC over stdin/stdout. There is no HTTP server, webhook URL, background daemon, launch agent, or port to expose.
- Reads come from
~/Library/Messages/chat.dbusing a read-only SQLite handle. - Live inbound messages come from
imsg watch/watch.subscribe, which followschat.dbfilesystem events with a polling fallback. - Sends use Messages.app automation for normal text and file sends.
- Advanced actions use
imsg launchto inject theimsghelper into Messages.app. That is what unlocks read receipts, typing indicators, rich sends, edit, unsend, threaded reply, tapbacks, polls, and group management. - Linux builds can inspect a copied
chat.db, but cannot send, watch the live Mac database, or drive Messages.app. For OpenClaw iMessage, runimsgon the signed-in Mac or through an SSH wrapper to that Mac.
Before you start
-
Install
imsgon the Mac that runs Messages.app:Ifimsg chatsfails withunable to open database file, empty output, orauthorization denied, grant Full Disk Access to the terminal, editor, Node process, Gateway service, or SSH parent process that launchesimsg, then reopen that parent process. -
Verify the read, watch, send, and RPC surfaces before changing OpenClaw config:
Replace
42with a real chat id fromimsg chats. Sending requires Automation permission for Messages.app. If OpenClaw will run through SSH, run these commands through the same SSH wrapper or user context that OpenClaw will use. If reads work but sends fail with AppleEvents-1743, check whether Automation landed on/usr/libexec/sshd-keygen-wrapper; see SSH wrapper sends fail with AppleEvents -1743. -
Enable the private API bridge when you need advanced actions:
imsg launchrequires SIP to be disabled (and on modern macOS, library validation relaxed — see Enabling the imsg private API). Basic send, history, and watch work withoutimsg launch; advanced actions do not. -
After you enable
channels.imessageand start the Gateway, verify the bridge through OpenClaw:The iMessage account should reportworks; with--json, the probe payload includesprivateApi.available: true. If it reportsfalse, fix that first — see Capability detection. Probing needs a reachable Gateway (the CLI falls back to config-only output otherwise) and only probes configured, enabled accounts. -
Snapshot your config:
Config translation
iMessage and BlueBubbles share most channel-level behavior keys. What changes is transport (REST server vs local CLI) and the group registry key format.| BlueBubbles | bundled iMessage | Notes |
|---|---|---|
channels.bluebubbles.enabled | channels.imessage.enabled | Same semantics (default true once the block exists). |
channels.bluebubbles.serverUrl | (removed) | No REST server — the plugin spawns imsg rpc over stdio. |
channels.bluebubbles.password | (removed) | No webhook authentication needed. |
| (implicit) | channels.imessage.cliPath | Path to imsg (default imsg); use a wrapper script for SSH. |
| (implicit) | channels.imessage.dbPath | Optional Messages.app chat.db override; auto-detected when omitted. |
| (implicit) | channels.imessage.remoteHost | host or user@host — only needed when cliPath is an SSH wrapper and you want SCP attachment fetches. |
channels.bluebubbles.dmPolicy | channels.imessage.dmPolicy | Same values (pairing / allowlist / open / disabled); default pairing. |
channels.bluebubbles.allowFrom | channels.imessage.allowFrom | Same handle formats (+15555550123, user@example.com). Pairing-store approvals do not transfer — see below. |
channels.bluebubbles.groupPolicy | channels.imessage.groupPolicy | Same values (allowlist / open / disabled); default allowlist. |
channels.bluebubbles.groupAllowFrom | channels.imessage.groupAllowFrom | Same. When unset, iMessage falls back to allowFrom; an explicitly empty groupAllowFrom: [] blocks all groups under groupPolicy: "allowlist". |
channels.bluebubbles.groups | channels.imessage.groups | Copy the "*" wildcard entry verbatim; re-key per-group entries by numeric iMessage chat_id — see “Group registry footgun”. requireMention, tools, toolsBySender, systemPrompt carry over. |
channels.bluebubbles.sendReadReceipts | channels.imessage.sendReadReceipts | Default true. With the bundled plugin this only fires when the private API probe is up. |
channels.bluebubbles.includeAttachments | channels.imessage.includeAttachments | Same shape, same off-by-default. If attachments flowed on BlueBubbles, set this explicitly — inbound photos/media are silently dropped (no Inbound message log line) until you do. |
channels.bluebubbles.attachmentRoots | channels.imessage.attachmentRoots | Local roots; same wildcard rules. |
| (N/A) | channels.imessage.remoteAttachmentRoots | Only used when remoteHost is set for SCP fetches. |
channels.bluebubbles.mediaMaxMb | channels.imessage.mediaMaxMb | Default 16 MB on iMessage (BlueBubbles default was 8 MB). Set explicitly to keep the lower cap. |
channels.bluebubbles.textChunkLimit | channels.imessage.textChunkLimit | Default 4000 on both. |
channels.bluebubbles.coalesceSameSenderDms | channels.imessage.coalesceSameSenderDms | Same opt-in. DM-only — groups keep per-message dispatch. Widens the default inbound debounce to 7000 ms unless messages.inbound.byChannel.imessage or a global messages.inbound.debounceMs is set. See Coalescing split-send DMs. |
channels.bluebubbles.enrichGroupParticipantsFromContacts | (N/A) | imsg already surfaces sender display names from chat.db. |
channels.bluebubbles.actions.* | channels.imessage.actions.* | Same per-action toggles (reactions, edit, unsend, reply, sendWithEffect, renameGroup, setGroupIcon, addParticipant, removeParticipant, leaveGroup, sendAttachment) plus new polls. All default to enabled; private API actions still require the bridge. |
channels.bluebubbles.accounts.*) translate one-to-one to channels.imessage.accounts.*.
Group registry footgun
The bundled iMessage plugin runs two group gates back to back. A group message must pass both to reach the agent:- Sender / chat-target allowlist (
channels.imessage.groupAllowFrom) — matches the sender handle or the chat target (chat_id:,chat_guid:,chat_identifier:entries). WhengroupAllowFromis unset, this gate falls back toallowFrom; an explicitgroupAllowFrom: []disables that fallback and drops every group message undergroupPolicy: "allowlist". - Group registry (
channels.imessage.groups) — keyed by numeric iMessagechat_id:- No
groupsblock (or an empty one): groups pass this gate as long as gate 1 has a non-empty effective sender allowlist; sender filtering governs access and no drop-all startup warning fires. groupswith entries but no"*": only the listedchat_idkeys pass. Listing any group turns the registry into an allowlist even undergroupPolicy: "open".groups: { "*": { ... } }: every group passes this gate.
- No
groups entries by chat GUID / chat identifier, while the iMessage registry keys by numeric chat_id. Per-group entries copied verbatim create a non-empty registry whose keys never match, so every group message drops at gate 2. Copy the "*" wildcard verbatim; re-key specific group entries with chat_id values from imsg chats.
Both drop paths are visible at the default log level via warn lines:
- Once per account at startup, when
groupPolicy: "allowlist"is set and the effective group sender allowlist is empty:imessage: groupPolicy="allowlist" for account "<id>" but no group sender allowlist is configured .... SetgroupAllowFrom(orallowFrom) to admit senders; addinggroupsalone does not satisfy the sender gate. - Once per
chat_idat runtime, when the registry drops a group:imessage: dropping group message from chat_id=<id> ... not in channels.imessage.groups allowlist, naming the exact key to add.
groupPolicy: "allowlist":
groups entries to scope allowed chats or set per-chat options such as requireMention; copy the BlueBubbles "*" entry verbatim, but re-key specific entries with numeric iMessage chat_id values.
Step-by-step
-
Translate the config. Keep the new block disabled while you edit; the old
channels.bluebubblesblock is ignored by current OpenClaw and can sit alongside as reference: -
Cut over and probe. Set
channels.imessage.enabled: true, restart the Gateway, and confirm the channel reports healthy:The probe requires a reachable Gateway and only probes configured, enabled accounts. Use the directimsgcommands in Before you start to validate the Mac itself. - Verify DMs. Send the agent a direct message; confirm the reply lands.
-
Verify groups separately. DMs and groups take different code paths — DM success does not prove groups are routing. Send a message in an allowed group chat and confirm the reply lands. If the group goes silent (no agent reply, no error), check the gateway log for the two
warnlines from “Group registry footgun” above. The startup warning means the effective sender allowlist is empty; a per-chat_idwarning means a populatedgroupsregistry does not contain that chat. -
Verify the action surface. From a paired DM, ask the agent to react, edit, unsend, reply, send a photo, and (in a group) rename the group or add/remove a participant. Each action should land natively in Messages.app. If any action throws
iMessage <action> requires the imsg private API bridge, runimsg launchagain and refresh withopenclaw channels status --probe. -
Remove the BlueBubbles server and the
channels.bluebubblesblock once iMessage DMs, groups, and actions are verified. OpenClaw does not readchannels.bluebubbles.
Action parity at a glance
| Action | legacy BlueBubbles | bundled iMessage |
|---|---|---|
| Send text / SMS fallback | ✅ | ✅ |
| Send media (photo, video, file, voice) | ✅ | ✅ |
Threaded reply (reply_to_guid) | ✅ | ✅ (closes #51892) |
Tapback (react) | ✅ | ✅ |
| Edit / unsend (macOS 13+ recipients) | ✅ | ✅ |
| Send with screen effect | ✅ | ✅ (closes part of #9394) |
| Rich text bold / italic / underline / strikethrough | ✅ | ✅ (typed-run formatting via attributedBody) |
| Native Messages polls (create and vote) | ❌ | ✅ (actions.polls; recipients need iOS/macOS 26+ for native rendering) |
| Rename group / set group icon | ✅ | ✅ |
| Add / remove participant, leave group | ✅ | ✅ |
| Read receipts and typing indicator | ✅ | ✅ (gated on private API probe) |
| Same-sender DM coalescing | ✅ | ✅ (DM-only; opt-in via channels.imessage.coalesceSameSenderDms) |
| Inbound recovery after a restart | ✅ | ✅ (automatic: since_rowid replay + GUID dedupe; wider window on local) |
imsg watch.subscribe since_rowid, dedupes by GUID, and a stale-backlog age fence suppresses the Push-flush “backlog bomb”. This runs over the imsg RPC connection, so it works for remote SSH cliPath setups too; local setups get a wider recovery window because they can read chat.db. See Inbound recovery after a bridge or gateway restart.
Pairing, sessions, and ACP bindings
- Allowlists carry over by handle.
channels.imessage.allowFromrecognizes the same+15555550123/user@example.comstrings BlueBubbles used — copy them verbatim. - Pairing-store approvals do not transfer. The pairing store is per channel and nothing migrates the old BlueBubbles store. Senders who were approved only through pairing must pair once more under iMessage, or you add their handles to
allowFrom. - Sessions stay scoped per agent + chat. DMs collapse into the agent main session under default
session.dmScope=main; group sessions stay isolated perchat_id(agent:<agentId>:imessage:group:<chat_id>). Old conversation history under BlueBubbles session keys does not carry into iMessage sessions. - ACP bindings referencing
match.channel: "bluebubbles"must change to"imessage". Thematch.peer.idshapes (chat_id:,chat_guid:,chat_identifier:, bare handle) are identical.
No rollback channel
There is no supported BlueBubbles runtime to switch back to. If iMessage verification fails, setchannels.imessage.enabled: false, restart the Gateway, fix the imsg blocker, and retry the cutover.
The reply cache lives in SQLite plugin state. openclaw doctor --fix imports and archives the old imessage/reply-cache.jsonl sidecar when present.
Related
- BlueBubbles removal and the imsg iMessage path — short announcement and operator summary.
- iMessage — full iMessage channel reference, including
imsg launchsetup and capability detection. /channels/bluebubbles— legacy URL that redirects to this migration guide.- Pairing — DM authentication and pairing flow.
- Channel Routing — how the gateway picks a channel for outbound replies.