docs/channels/imessage-from-bluebubbles.md
The bundled imessage plugin now reaches the same private API surface as BlueBubbles (react, edit, unsend, reply, sendWithEffect, group management, attachments) by driving steipete/imsg over JSON-RPC. If you already run a Mac with imsg installed, you can drop the BlueBubbles server and let the plugin talk to Messages.app directly.
BlueBubbles support was removed. OpenClaw supports iMessage through imsg only. This guide is for migrating old channels.bluebubbles configs to channels.imessage; there is no other supported migration path.
Use this checklist when you already know your old BlueBubbles config and want the shortest safe path:
imsg directly on the Mac that runs Messages.app (imsg chats, imsg history, imsg send, and imsg rpc --help).channels.bluebubbles to channels.imessage: dmPolicy, allowFrom, groupPolicy, groupAllowFrom, groups, includeAttachments, attachmentRoots, mediaMaxMb, textChunkLimit, coalesceSameSenderDms, and actions.serverUrl, password, webhook URLs, and BlueBubbles server setup.channels.imessage.cliPath to an SSH wrapper and set remoteHost for remote attachment fetches.channels.imessage, then run openclaw channels status --probe --channel imessage.channels.bluebubbles config after the iMessage path is verified.imsg on the same Mac (or one reachable over SSH) where Messages.app is signed in.imsg build where the private API probe reports available: true.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.
~/Library/Messages/chat.db using a read-only SQLite handle.imsg watch / watch.subscribe, which follows chat.db filesystem events with a polling fallback.imsg launch to inject the imsg helper into Messages.app. That is what unlocks read receipts, typing indicators, rich sends, edit, unsend, threaded reply, tapbacks, and group management.chat.db, but cannot send, watch the live Mac database, or drive Messages.app. For OpenClaw iMessage, run imsg on the signed-in Mac or through an SSH wrapper to that Mac.Install imsg on the Mac that runs Messages.app:
brew install steipete/tap/imsg
imsg --version
imsg chats --limit 3
If imsg chats fails with unable to open database file, empty output, or authorization denied, grant Full Disk Access to the terminal, editor, Node process, Gateway service, or SSH parent process that launches imsg, then reopen that parent process.
Verify the read, watch, send, and RPC surfaces before changing OpenClaw config:
imsg chats --limit 10 --json | jq -s
imsg history --chat-id 42 --limit 10 --attachments --json | jq -s
imsg watch --chat-id 42 --reactions --json
imsg send --chat-id 42 --text "OpenClaw imsg test"
imsg rpc --help
Replace 42 with a real chat id from imsg 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.
Enable the private API bridge when you need advanced actions:
imsg launch
imsg status --json
imsg launch requires SIP to be disabled. Basic send, history, and watch work without imsg launch; advanced actions do not.
After you add an enabled channels.imessage config, verify the bridge through OpenClaw:
openclaw channels status --probe
You want imessage.privateApi.available: true. If it reports false, fix that first — see Capability detection. channels status --probe only probes configured, enabled accounts.
Snapshot your config:
cp ~/.openclaw/openclaw.json5 ~/.openclaw/openclaw.json5.bak
iMessage and BlueBubbles share a lot of channel-level config. The keys that change are mostly transport (REST server vs local CLI). Behavior keys (dmPolicy, groupPolicy, allowFrom, etc.) keep the same meaning.
| BlueBubbles | bundled iMessage | Notes |
|---|---|---|
channels.bluebubbles.enabled | channels.imessage.enabled | Same semantics. |
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). |
channels.bluebubbles.allowFrom | channels.imessage.allowFrom | Pairing approvals carry over by handle, not by token. |
channels.bluebubbles.groupPolicy | channels.imessage.groupPolicy | Same values (allowlist / open / disabled). |
channels.bluebubbles.groupAllowFrom | channels.imessage.groupAllowFrom | Same. |
channels.bluebubbles.groups | channels.imessage.groups | Copy this verbatim, including any groups: { "*": { ... } } wildcard entry. Per-group requireMention, tools, toolsBySender carry over. With groupPolicy: "allowlist", an empty or missing groups block silently drops every group message — see "Group registry footgun" below. |
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 you had attachments flowing on BlueBubbles you must re-set this explicitly on the iMessage block — it does not carry over implicitly, and inbound photos/media will be silently dropped with 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 if you want 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 — group chats keep instant per-message dispatch on both channels. Widens the default inbound debounce to 2500 ms when enabled without an explicit messages.inbound.byChannel.imessage. See iMessage docs § Coalescing split-send DMs. |
channels.bluebubbles.enrichGroupParticipantsFromContacts | (N/A) | iMessage already reads sender display names from chat.db. |
channels.bluebubbles.actions.* | channels.imessage.actions.* | Per-action toggles: reactions, edit, unsend, reply, sendWithEffect, renameGroup, setGroupIcon, addParticipant, removeParticipant, leaveGroup, sendAttachment. |
Multi-account configs (channels.bluebubbles.accounts.*) translate one-to-one to channels.imessage.accounts.*.
The bundled iMessage plugin runs two separate group allowlist gates back-to-back. Both must pass for a group message to reach the agent:
channels.imessage.groupAllowFrom) — checked by isAllowedIMessageSender. Matches inbound messages by sender handle, chat_guid, chat_identifier, or chat_id. Same shape as BlueBubbles.channels.imessage.groups) — checked by resolveChannelGroupPolicy from inbound-processing.ts:199. With groupPolicy: "allowlist", this gate requires either:
groups: { "*": { ... } } wildcard entry (sets allowAll = true), orchat_id entry under groups.If gate 1 passes but gate 2 fails, the message is dropped. The plugin emits two warn-level signals so this is no longer silent at default log level:
warn per account when groupPolicy: "allowlist" is set but channels.imessage.groups is empty (no "*" wildcard, no per-chat_id entries) — fired before any messages land.chat_id warn the first time a specific group is dropped at runtime, naming the chat_id and the exact key to add to groups to allow it.DMs continue to work because they take a different code path.
This is the most common BlueBubbles → bundled-iMessage migration failure mode: operators copy groupAllowFrom and groupPolicy but skip the groups block, because BlueBubbles' groups: { "*": { "requireMention": true } } looks like an unrelated mention setting. It's actually load-bearing for the registry gate.
The minimum config to keep group messages flowing after groupPolicy: "allowlist":
{
channels: {
imessage: {
groupPolicy: "allowlist",
groupAllowFrom: ["+15555550123", "chat_guid:any;-;..."],
groups: {
"*": { requireMention: true },
},
},
},
}
requireMention: true under * is harmless when no mention patterns are configured: the runtime sets canDetectMention = false and short-circuits the mention drop at inbound-processing.ts:512. With mention patterns configured (agents.list[].groupChat.mentionPatterns), it works as expected.
If the gateway logs imessage: dropping group message from chat_id=<id> or the startup line imessage: groupPolicy="allowlist" but channels.imessage.groups is empty, gate 2 is dropping — add the groups block.
Add an iMessage block alongside the existing BlueBubbles block. Keep it disabled while the Gateway is still routing BlueBubbles traffic:
{
channels: {
bluebubbles: {
enabled: true,
// ... existing config ...
},
imessage: {
enabled: false,
cliPath: "/opt/homebrew/bin/imsg",
dmPolicy: "pairing",
allowFrom: ["+15555550123"], // copy from bluebubbles.allowFrom
groupPolicy: "allowlist",
groupAllowFrom: [], // copy from bluebubbles.groupAllowFrom
groups: { "*": { requireMention: true } }, // copy from bluebubbles.groups — silently drops groups if missing, see "Group registry footgun" above
actions: {
reactions: true,
edit: true,
unsend: true,
reply: true,
sendWithEffect: true,
sendAttachment: true,
},
},
},
}
Probe before traffic matters — stop the Gateway, temporarily enable the iMessage block, and confirm iMessage reports healthy from the CLI:
openclaw gateway stop
# edit config: channels.imessage.enabled = true
openclaw channels status --probe --channel imessage # expect imessage.privateApi.available: true
channels status --probe only probes configured, enabled accounts. Do not restart the Gateway with both BlueBubbles and iMessage enabled unless you intentionally want both channel monitors running. If you are not cutting over immediately, set channels.imessage.enabled back to false before restarting the Gateway. Use the direct imsg commands in Before you start to validate the Mac before enabling OpenClaw traffic.
Cut over. Once the enabled iMessage account reports healthy, remove the BlueBubbles config and keep iMessage enabled:
{
channels: {
imessage: { enabled: true /* ... */ },
},
}
Restart the gateway. Inbound iMessage traffic now flows through the bundled plugin.
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 the agent a message in a paired group chat and confirm the reply lands. If the group goes silent (no agent reply, no error), check the gateway log for imessage: dropping group message from chat_id=<id> or the startup imessage: groupPolicy="allowlist" but channels.imessage.groups is empty line — both fire at the default log level. If either appears, your groups block is missing or empty — see "Group registry footgun" above.
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 / add or remove a participant. Each action should land natively in Messages.app. If any throws "iMessage <action> requires the imsg private API bridge", run imsg launch again and refresh channels status --probe.
Remove the BlueBubbles server and config once iMessage DMs, groups, and actions are verified. OpenClaw will not use channels.bluebubbles.
| 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) |
| 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) |
| Catchup of inbound messages received while gateway is down | ✅ (webhook replay + history fetch) | ✅ (opt-in via channels.imessage.catchup.enabled; closes #78649) |
iMessage catchup is now available as an opt-in feature on the bundled plugin. On gateway startup, if channels.imessage.catchup.enabled is true, the gateway runs one chats.list + per-chat messages.history pass against the same JSON-RPC client used by imsg watch, replays each missed inbound row through the live dispatch path (allowlists, group policy, debouncer, echo cache), and persists a per-account cursor so subsequent startups pick up where they left off. See Catching up after gateway downtime for tuning.
channels.imessage.allowFrom recognizes the same +15555550123 / [email protected] strings BlueBubbles used.session.dmScope=main; group sessions stay isolated per chat_id. The session keys differ (agent:<id>:imessage:group:<chat_id> vs the BlueBubbles equivalent) — old conversation history under BlueBubbles session keys does not carry into iMessage sessions.match.channel: "bluebubbles" need to be updated to "imessage". The match.peer.id shapes (chat_id:, chat_guid:, chat_identifier:, bare handle) are identical.There is no supported BlueBubbles runtime to switch back to. If iMessage verification fails, set channels.imessage.enabled: false, restart the Gateway, fix the imsg blocker, and retry the cutover.
The reply cache lives at ~/.openclaw/state/imessage/reply-cache.jsonl (mode 0600, parent dir 0700). It is safe to delete if you want a clean slate.
imsg launch setup and capability detection./channels/bluebubbles — legacy URL that redirects to this migration guide.