docs/channels/imessage-from-bluebubbles.md
BlueBubbles support was removed. OpenClaw supports iMessage only through the bundled 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.
The shortest safe path when you already know your old BlueBubbles config:
imsg directly on the Mac that runs Messages.app (imsg chats, imsg history, imsg send, 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, restart the Gateway, then run openclaw channels status --probe --channel imessage.channels.bluebubbles config after the iMessage path is verified.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, polls, 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
brew update && brew upgrade imsg
imsg --version
imsg chats --limit 3
For the usual local setup, OpenClaw setup can offer a user-confirmed Homebrew install or update for imsg on the signed-in Messages Mac. Manual setup and SSH-wrapper topologies remain operator-managed: repeat the Homebrew update in the same local or remote user context that will run imsg. 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. 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. It is strongly encouraged for OpenClaw iMessage because replies, tapbacks, effects, polls, attachment replies, and group actions depend on it:
imsg launch
imsg status --json
imsg launch requires SIP to be disabled (and on modern macOS, library validation relaxed — see Enabling the imsg private API). Basic send, history, and watch work without imsg launch; the full OpenClaw iMessage action surface does not.
After you enable channels.imessage and start the Gateway, verify the bridge through OpenClaw:
openclaw channels status --probe
The iMessage account should report works; with --json, the probe payload includes privateApi.available: true. If it reports false, 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:
cp ~/.openclaw/openclaw.json ~/.openclaw/openclaw.json.bak
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, [email protected]). 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. |
Multi-account configs (channels.bluebubbles.accounts.*) translate one-to-one to channels.imessage.accounts.*.
The bundled iMessage plugin runs two group gates back to back. A group message must pass both to reach the agent:
channels.imessage.groupAllowFrom) — matches the sender handle or the chat target (chat_id:, chat_guid:, chat_identifier: entries). When groupAllowFrom is unset, this gate falls back to allowFrom; an explicit groupAllowFrom: [] disables that fallback and drops every group message under groupPolicy: "allowlist".channels.imessage.groups) — keyed by numeric iMessage chat_id:
groups block (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.groups with entries but no "*": only the listed chat_id keys pass. Listing any group turns the registry into an allowlist even under groupPolicy: "open".groups: { "*": { ... } }: every group passes this gate.The migration trap: BlueBubbles keyed 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:
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 .... Set groupAllowFrom (or allowFrom) to admit senders; adding groups alone does not satisfy the sender gate.chat_id at 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.DMs keep working either way — they take a different code path, so DM success does not prove group routing.
The minimum sender-scoped config with groupPolicy: "allowlist":
{
channels: {
imessage: {
groupPolicy: "allowlist",
groupAllowFrom: ["+15555550123", "chat_guid:any;-;..."],
},
},
}
This admits the configured senders in any group. Add 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.
Translate the config. Keep the new block disabled while you edit; the old channels.bluebubbles block is ignored by current OpenClaw and can sit alongside as reference:
{
channels: {
imessage: {
enabled: false, // flip to true when ready to cut over
cliPath: "/opt/homebrew/bin/imsg",
dmPolicy: "pairing",
allowFrom: ["+15555550123"], // copy from bluebubbles.allowFrom
groupPolicy: "allowlist",
groupAllowFrom: [], // copy from bluebubbles.groupAllowFrom
groups: { "*": { requireMention: true } }, // wildcard copies verbatim; re-key per-chat entries by chat_id
// actions default to enabled; set individual toggles false to disable
},
},
}
Cut over and probe. Set channels.imessage.enabled: true, restart the Gateway, and confirm the channel reports healthy:
openclaw gateway restart
openclaw channels status --probe --channel imessage # expect "works"; --json shows privateApi.available: true
The probe requires a reachable Gateway and only probes configured, enabled accounts. Use the direct imsg commands 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 warn lines from "Group registry footgun" above. The startup warning means the effective sender allowlist is empty; a per-chat_id warning means a populated groups registry 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, run imsg launch again and refresh with openclaw channels status --probe.
Remove the BlueBubbles server and the channels.bluebubbles block once iMessage DMs, groups, and actions are verified. OpenClaw does not read 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) |
| 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) |
iMessage recovers messages missed while the gateway was down: on startup it replays from the last dispatched rowid via 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.
channels.imessage.allowFrom recognizes the same +15555550123 / [email protected] strings BlueBubbles used — copy them verbatim.allowFrom.session.dmScope=main; group sessions stay isolated per chat_id (agent:<agentId>:imessage:group:<chat_id>). Old conversation history under BlueBubbles session keys does not carry into iMessage sessions.match.channel: "bluebubbles" must change 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 in SQLite plugin state. openclaw doctor --fix imports and archives the old imessage/reply-cache.jsonl sidecar when present.
imsg launch setup and capability detection./channels/bluebubbles — legacy URL that redirects to this migration guide.