Files
adolf/docs/channels/imessage-from-bluebubbles.md
alvis bedb527145
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Vendor OpenClaw source as Adolf fork baseline
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
2026-07-05 09:36:54 +00:00

24 KiB

summary, read_when, title
summary read_when title
Translate old BlueBubbles configs to the bundled iMessage plugin: key mapping, group allowlist gates, and cutover verification.
Planning a move from BlueBubbles to the bundled iMessage plugin
Translating BlueBubbles config keys to iMessage equivalents
Verifying imsg before enabling the iMessage plugin
Coming from BlueBubbles

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.

For the short announcement and operator summary, see [BlueBubbles removal and the imsg iMessage path](/announcements/bluebubbles-imessage).

Migration checklist

The shortest safe path when you already know your old BlueBubbles config:

  1. Verify imsg directly on the Mac that runs Messages.app (imsg chats, imsg history, imsg send, imsg rpc --help).
  2. Copy behavior keys from channels.bluebubbles to channels.imessage: dmPolicy, allowFrom, groupPolicy, groupAllowFrom, groups, includeAttachments, attachmentRoots, mediaMaxMb, textChunkLimit, coalesceSameSenderDms, and actions.
  3. Drop transport keys that no longer exist: serverUrl, password, webhook URLs, and BlueBubbles server setup.
  4. If the Gateway is not running on the Messages Mac, set channels.imessage.cliPath to an SSH wrapper and set remoteHost for remote attachment fetches.
  5. Enable channels.imessage, restart the Gateway, then run openclaw channels status --probe --channel imessage.
  6. Test one DM, one allowed group, attachments if enabled, and every private API action you expect the agent to use.
  7. Delete the BlueBubbles server and the old channels.bluebubbles config 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.db using a read-only SQLite handle.
  • Live inbound messages come from imsg watch / watch.subscribe, which follows chat.db filesystem events with a polling fallback.
  • Sends use Messages.app automation for normal text and file sends.
  • Advanced actions use 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.
  • Linux builds can inspect a copied 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.

Before you start

  1. 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.

  2. 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.

  3. Enable the private API bridge when you need advanced actions:

    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; advanced actions do not.

  4. 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.

  5. Snapshot your config:

    cp ~/.openclaw/openclaw.json ~/.openclaw/openclaw.json.bak
    

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. Does not fall back to allowFrom — an empty groupAllowFrom under groupPolicy: "allowlist" drops all groups regardless of allowFrom.
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.*.

Group registry footgun

The bundled iMessage plugin runs two group gates back to back. A group message must pass both to reach the agent:

  1. Sender / chat-target allowlist (channels.imessage.groupAllowFrom) — matches the sender handle or the chat target (chat_id:, chat_guid:, chat_identifier: entries). Does not fall back to allowFrom: with groupPolicy: "allowlist" and an empty groupAllowFrom, every group message drops here regardless of allowFrom.
  2. Group registry (channels.imessage.groups) — keyed by numeric iMessage chat_id:
    • No groups block (or an empty one): groups pass this gate as long as gate 1 has a non-empty groupAllowFrom; sender filtering governs access. The gateway still logs a startup warning nudging you to add a groups block.
    • 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:

  • Once per account at startup, when groupPolicy: "allowlist" is set and channels.imessage.groups is empty: imessage: groupPolicy="allowlist" but channels.imessage.groups is empty for account "<id>".
  • Once per 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 safe minimum with 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 cannot detect mentions without patterns and skips the mention drop. With agents.list[].groupChat.mentionPatterns (fallback messages.groupChat.mentionPatterns) configured, mention gating applies as expected.

Step-by-step

  1. 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
        },
      },
    }
    
  2. 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.

  3. Verify DMs. Send the agent a direct message; confirm the reply lands.

  4. 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; either one means the groups block is missing, empty, or keyed wrong.

  5. 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.

  6. Remove the BlueBubbles server and the channels.bluebubbles block once iMessage DMs, groups, and actions are verified. OpenClaw does not read channels.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)

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.

Pairing, sessions, and ACP bindings

  • Allowlists carry over by handle. channels.imessage.allowFrom recognizes the same +15555550123 / user@example.com strings 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 per chat_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". The match.peer.id shapes (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, 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.

  • BlueBubbles removal and the imsg iMessage path — short announcement and operator summary.
  • iMessage — full iMessage channel reference, including imsg launch setup 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.