Bundled plugin
Current OpenClaw releases bundle BlueBubbles, so normal packaged builds do not need a separateopenclaw plugins install step.
Overview
- Runs on macOS via the BlueBubbles helper app (bluebubbles.app).
- Recommended/tested: macOS Sequoia (15). macOS Tahoe (26) works; edit is currently broken on Tahoe, and group icon updates may report success but not sync.
- OpenClaw talks to it through its REST API (
GET /api/v1/ping,POST /message/text,POST /chat/:id/*). - Incoming messages arrive via webhooks; outgoing replies, typing indicators, read receipts, and tapbacks are REST calls.
- Attachments and stickers are ingested as inbound media (and surfaced to the agent when possible).
- Auto-TTS replies that synthesize MP3 or CAF audio are delivered as iMessage voice memo bubbles instead of plain file attachments.
- Pairing/allowlist works the same way as other channels (
/channels/pairingetc) withchannels.bluebubbles.allowFrom+ pairing codes. - Reactions are surfaced as system events just like Slack/Telegram so agents can “mention” them before replying.
- Advanced features: edit, unsend, reply threading, message effects, group management.
Quick start
- Install the BlueBubbles server on your Mac (follow the instructions at bluebubbles.app/install).
- In the BlueBubbles config, enable the web API and set a password.
-
Run
openclaw onboardand select BlueBubbles, or configure manually: -
Point BlueBubbles webhooks to your gateway (example:
https://your-gateway-host:3000/bluebubbles-webhook?password=<password>). - Start the gateway; it will register the webhook handler and start pairing.
- Always set a webhook password.
- Webhook authentication is always required. OpenClaw rejects BlueBubbles webhook requests unless they include a password/guid that matches
channels.bluebubbles.password(for example?password=<password>orx-password), regardless of loopback/proxy topology. - Password authentication is checked before reading/parsing full webhook bodies.
Keeping Messages.app alive (VM / headless setups)
Some macOS VM / always-on setups can end up with Messages.app going “idle” (incoming events stop until the app is opened/foregrounded). A simple workaround is to poke Messages every 5 minutes using an AppleScript + LaunchAgent.1) Save the AppleScript
Save this as:~/Scripts/poke-messages.scpt
2) Install a LaunchAgent
Save this as:~/Library/LaunchAgents/com.user.poke-messages.plist
- This runs every 300 seconds and on login.
- The first run may trigger macOS Automation prompts (
osascript→ Messages). Approve them in the same user session that runs the LaunchAgent.
Onboarding
BlueBubbles is available in interactive onboarding:- Server URL (required): BlueBubbles server address (e.g.,
http://192.168.1.100:1234) - Password (required): API password from BlueBubbles Server settings
- Webhook path (optional): Defaults to
/bluebubbles-webhook - DM policy: pairing, allowlist, open, or disabled
- Allow list: Phone numbers, emails, or chat targets
Access control (DMs + groups)
DMs:- Default:
channels.bluebubbles.dmPolicy = "pairing". - Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
- Approve via:
openclaw pairing list bluebubblesopenclaw pairing approve bluebubbles <CODE>
- Pairing is the default token exchange. Details: Pairing
channels.bluebubbles.groupPolicy = open | allowlist | disabled(default:allowlist).channels.bluebubbles.groupAllowFromcontrols who can trigger in groups whenallowlistis set.
Contact name enrichment (macOS, optional)
BlueBubbles group webhooks often only include raw participant addresses. If you wantGroupMembers context to show local contact names instead, you can opt in to local Contacts enrichment on macOS:
channels.bluebubbles.enrichGroupParticipantsFromContacts = trueenables the lookup. Default:false.- Lookups run only after group access, command authorization, and mention gating have allowed the message through.
- Only unnamed phone participants are enriched.
- Raw phone numbers remain as the fallback when no local match is found.
Mention gating (groups)
BlueBubbles supports mention gating for group chats, matching iMessage/WhatsApp behavior:- Uses
agents.list[].groupChat.mentionPatterns(ormessages.groupChat.mentionPatterns) to detect mentions. - When
requireMentionis enabled for a group, the agent only responds when mentioned. - Control commands from authorized senders bypass mention gating.
Command gating
- Control commands (e.g.,
/config,/model) require authorization. - Uses
allowFromandgroupAllowFromto determine command authorization. - Authorized senders can run control commands even without mentioning in groups.
Per-group system prompt
Each entry underchannels.bluebubbles.groups.* accepts an optional systemPrompt string. The value is injected into the agent’s system prompt on every turn that handles a message in that group, so you can set per-group persona or behavioral rules without editing agent prompts:
chatGuid / chatIdentifier / numeric chatId for the group, and a "*" wildcard entry provides a default for every group without an exact match (same pattern used by requireMention and per-group tool policies). Exact matches always win over the wildcard. DMs ignore this field; use agent-level or account-level prompt customization instead.
Worked example: threaded replies and tapback reactions (Private API)
With the BlueBubbles Private API enabled, inbound messages arrive with short message IDs (for example[[reply_to:5]]) and the agent can call action=reply to thread into a specific message or action=react to drop a tapback. A per-group systemPrompt is a reliable way to keep the agent choosing the right tool:
ACP conversation bindings
BlueBubbles chats can be turned into durable ACP workspaces without changing the transport layer. Fast operator flow:- Run
/acp spawn codex --bind hereinside the DM or allowed group chat. - Future messages in that same BlueBubbles conversation route to the spawned ACP session.
/newand/resetreset the same bound ACP session in place./acp closecloses the ACP session and removes the binding.
bindings[] entries with type: "acp" and match.channel: "bluebubbles".
match.peer.id can use any supported BlueBubbles target form:
- normalized DM handle such as
+15555550123oruser@example.com chat_id:<id>chat_guid:<guid>chat_identifier:<identifier>
chat_id:* or chat_identifier:*.
Example:
Typing + read receipts
- Typing indicators: Sent automatically before and during response generation.
- Read receipts: Controlled by
channels.bluebubbles.sendReadReceipts(default:true). - Typing indicators: OpenClaw sends typing start events; BlueBubbles clears typing automatically on send or timeout (manual stop via DELETE is unreliable).
Advanced actions
BlueBubbles supports advanced message actions when enabled in config:- react: Add/remove tapback reactions (
messageId,emoji,remove). iMessage’s native tapback set islove,like,dislike,laugh,emphasize, andquestion. When an agent picks an emoji outside that set (for example👀), the reaction tool falls back toloveso the tapback still renders instead of failing the whole request. Configured ack reactions still validate strictly and error on unknown values. - edit: Edit a sent message (
messageId,text) - unsend: Unsend a message (
messageId) - reply: Reply to a specific message (
messageId,text,to) - sendWithEffect: Send with iMessage effect (
text,to,effectId) - renameGroup: Rename a group chat (
chatGuid,displayName) - setGroupIcon: Set a group chat’s icon/photo (
chatGuid,media) — flaky on macOS 26 Tahoe (API may return success but the icon does not sync). - addParticipant: Add someone to a group (
chatGuid,address) - removeParticipant: Remove someone from a group (
chatGuid,address) - leaveGroup: Leave a group chat (
chatGuid) - upload-file: Send media/files (
to,buffer,filename,asVoice)- Voice memos: set
asVoice: truewith MP3 or CAF audio to send as an iMessage voice message. BlueBubbles converts MP3 → CAF when sending voice memos.
- Voice memos: set
- Legacy alias:
sendAttachmentstill works, butupload-fileis the canonical action name.
Message IDs (short vs full)
OpenClaw may surface short message IDs (e.g.,1, 2) to save tokens.
MessageSid/ReplyToIdcan be short IDs.MessageSidFull/ReplyToIdFullcontain the provider full IDs.- Short IDs are in-memory; they can expire on restart or cache eviction.
- Actions accept short or full
messageId, but short IDs will error if no longer available.
- Templates:
{{MessageSidFull}},{{ReplyToIdFull}} - Context:
MessageSidFull/ReplyToIdFullin inbound payloads
Coalescing split-send DMs (command + URL in one composition)
When a user types a command and a URL together in iMessage — e.g.Dump https://example.com/article — Apple splits the send into two separate webhook deliveries:
- A text message (
"Dump"). - A URL-preview balloon (
"https://...") with OG-preview images as attachments.
channels.bluebubbles.coalesceSameSenderDms opts a DM into merging consecutive same-sender webhooks into a single agent turn. Group chats continue to key per-message so multi-user turn structure is preserved.
When to enable
Enable when:- You ship skills that expect
command + payloadin one message (dump, paste, save, queue, etc.). - Your users paste URLs, images, or long content alongside commands.
- You can accept the added DM turn latency (see below).
- You need minimum command latency for single-word DM triggers.
- All your flows are one-shot commands without payload follow-ups.
Enabling
messages.inbound.byChannel.bluebubbles, the debounce window widens to 2500 ms (the default for non-coalescing is 500 ms). The wider window is required — Apple’s split-send cadence of 0.8-2.0 s does not fit in the tighter default.
To tune the window yourself:
Trade-offs
- Added latency for DM control commands. With the flag on, DM control-command messages (like
Dump,Save, etc.) now wait up to the debounce window before dispatching, in case a payload webhook is coming. Group-chat commands keep instant dispatch. - Merged output is bounded — merged text caps at 4000 chars with an explicit
…[truncated]marker; attachments cap at 20; source entries cap at 10 (first-plus-latest retained beyond that). Every sourcemessageIdstill reaches inbound-dedupe so a later MessagePoller replay of any individual event is recognized as a duplicate. - Opt-in, per-channel. Other channels (Telegram, WhatsApp, Slack, …) are unaffected.
Scenarios and what the agent sees
| User composes | Apple delivers | Flag off (default) | Flag on + 2500 ms window |
|---|---|---|---|
Dump https://example.com (one send) | 2 webhooks ~1 s apart | Two agent turns: “Dump” alone, then URL | One turn: merged text Dump https://example.com |
Save this 📎image.jpg caption (attachment + text) | 2 webhooks | Two turns | One turn: text + image |
/status (standalone command) | 1 webhook | Instant dispatch | Wait up to window, then dispatch |
| URL pasted alone | 1 webhook | Instant dispatch | Instant dispatch (only one entry in bucket) |
| Text + URL sent as two deliberate separate messages, minutes apart | 2 webhooks outside window | Two turns | Two turns (window expires between them) |
| Rapid flood (>10 small DMs inside window) | N webhooks | N turns | One turn, bounded output (first + latest, text/attachment caps applied) |
Split-send coalescing troubleshooting
If the flag is on and split-sends still arrive as two turns, check each layer:-
Config actually loaded.
Then
openclaw gateway restart— the flag is read at debouncer-registry creation. -
Debounce window wide enough for your setup. Look at the BlueBubbles server log under
~/Library/Logs/bluebubbles-server/main.log:Measure the gap between the"Dump"-style text dispatch and the"https://..."; Attachments:dispatch that follows. Raisemessages.inbound.byChannel.bluebubblesto comfortably cover that gap. -
Session JSONL timestamps ≠ webhook arrival. Session event timestamps (
~/.openclaw/agents/<id>/sessions/*.jsonl) reflect when the gateway hands a message to the agent, not when the webhook arrived. A queued-second message tagged[Queued messages while agent was busy]means the first turn was still running when the second webhook arrived — the coalesce bucket had already flushed. Tune the window against the BB server log, not the session log. -
Memory pressure slowing reply dispatch. On smaller machines (8 GB), agent turns can take long enough that the coalesce bucket flushes before the reply completes, and the URL lands as a queued second turn. Check
memory_pressureandps -o rss -p $(pgrep openclaw-gateway); if the gateway is over ~500 MB RSS and the compressor is active, close other heavy processes or bump to a larger host. -
Reply-quote sends are a different path. If the user tapped
Dumpas a reply to an existing URL-balloon (iMessage shows a “1 Reply” badge on the Dump bubble), the URL lives inreplyToBody, not in a second webhook. Coalescing does not apply — that’s a skill/prompt concern, not a debouncer concern.
Block streaming
Control whether responses are sent as a single message or streamed in blocks:Media + limits
- Inbound attachments are downloaded and stored in the media cache.
- Media cap via
channels.bluebubbles.mediaMaxMbfor inbound and outbound media (default: 8 MB). - Outbound text is chunked to
channels.bluebubbles.textChunkLimit(default: 4000 chars).
Configuration reference
Full configuration: Configuration Provider options:channels.bluebubbles.enabled: Enable/disable the channel.channels.bluebubbles.serverUrl: BlueBubbles REST API base URL.channels.bluebubbles.password: API password.channels.bluebubbles.webhookPath: Webhook endpoint path (default:/bluebubbles-webhook).channels.bluebubbles.dmPolicy:pairing | allowlist | open | disabled(default:pairing).channels.bluebubbles.allowFrom: DM allowlist (handles, emails, E.164 numbers,chat_id:*,chat_guid:*).channels.bluebubbles.groupPolicy:open | allowlist | disabled(default:allowlist).channels.bluebubbles.groupAllowFrom: Group sender allowlist.channels.bluebubbles.enrichGroupParticipantsFromContacts: On macOS, optionally enrich unnamed group participants from local Contacts after gating passes. Default:false.channels.bluebubbles.groups: Per-group config (requireMention, etc.).channels.bluebubbles.sendReadReceipts: Send read receipts (default:true).channels.bluebubbles.blockStreaming: Enable block streaming (default:false; required for streaming replies).channels.bluebubbles.textChunkLimit: Outbound chunk size in chars (default: 4000).channels.bluebubbles.sendTimeoutMs: Per-request timeout in ms for outbound text sends via/api/v1/message/text(default: 30000). Raise on macOS 26 setups where Private API iMessage sends can stall for 60+ seconds inside the iMessage framework; for example45000or60000. Probes, chat lookups, reactions, edits, and health checks currently keep the shorter 10s default; broadening coverage to reactions and edits is planned as a follow-up. Per-account override:channels.bluebubbles.accounts.<accountId>.sendTimeoutMs.channels.bluebubbles.chunkMode:length(default) splits only when exceedingtextChunkLimit;newlinesplits on blank lines (paragraph boundaries) before length chunking.channels.bluebubbles.mediaMaxMb: Inbound/outbound media cap in MB (default: 8).channels.bluebubbles.mediaLocalRoots: Explicit allowlist of absolute local directories permitted for outbound local media paths. Local path sends are denied by default unless this is configured. Per-account override:channels.bluebubbles.accounts.<accountId>.mediaLocalRoots.channels.bluebubbles.coalesceSameSenderDms: Merge consecutive same-sender DM webhooks into one agent turn so Apple’s text+URL split-send arrives as a single message (default:false). See Coalescing split-send DMs for scenarios, window tuning, and trade-offs. Widens the default inbound debounce window from 500 ms to 2500 ms when enabled without an explicitmessages.inbound.byChannel.bluebubbles.channels.bluebubbles.historyLimit: Max group messages for context (0 disables).channels.bluebubbles.dmHistoryLimit: DM history limit.channels.bluebubbles.actions: Enable/disable specific actions.channels.bluebubbles.accounts: Multi-account configuration.
agents.list[].groupChat.mentionPatterns(ormessages.groupChat.mentionPatterns).messages.responsePrefix.
Addressing / delivery targets
Preferchat_guid for stable routing:
chat_guid:iMessage;-;+15555550123(preferred for groups)chat_id:123chat_identifier:...- Direct handles:
+15555550123,user@example.com- If a direct handle does not have an existing DM chat, OpenClaw will create one via
POST /api/v1/chat/new. This requires the BlueBubbles Private API to be enabled.
- If a direct handle does not have an existing DM chat, OpenClaw will create one via
iMessage vs SMS routing
When the same handle has both an iMessage and an SMS chat on the Mac (for example a phone number that is iMessage-registered but has also received green-bubble fallbacks), OpenClaw prefers the iMessage chat and never silently downgrades to SMS. To force the SMS chat, use an explicitsms: target prefix (for example sms:+15555550123). Handles without a matching iMessage chat still send through whatever chat BlueBubbles reports.
Security
- Webhook requests are authenticated by comparing
guid/passwordquery params or headers againstchannels.bluebubbles.password. - Keep the API password and webhook endpoint secret (treat them like credentials).
- There is no localhost bypass for BlueBubbles webhook auth. If you proxy webhook traffic, keep the BlueBubbles password on the request end-to-end.
gateway.trustedProxiesdoes not replacechannels.bluebubbles.passwordhere. See Gateway security. - Enable HTTPS + firewall rules on the BlueBubbles server if exposing it outside your LAN.
Troubleshooting
- If typing/read events stop working, check the BlueBubbles webhook logs and verify the gateway path matches
channels.bluebubbles.webhookPath. - Pairing codes expire after one hour; use
openclaw pairing list bluebubblesandopenclaw pairing approve bluebubbles <code>. - Reactions require the BlueBubbles private API (
POST /api/v1/message/react); ensure the server version exposes it. - Edit/unsend require macOS 13+ and a compatible BlueBubbles server version. On macOS 26 (Tahoe), edit is currently broken due to private API changes.
- Group icon updates can be flaky on macOS 26 (Tahoe): the API may return success but the new icon does not sync.
- OpenClaw auto-hides known-broken actions based on the BlueBubbles server’s macOS version. If edit still appears on macOS 26 (Tahoe), disable it manually with
channels.bluebubbles.actions.edit=false. coalesceSameSenderDmsenabled but split-sends (e.g.Dump+ URL) still arrive as two turns: see the split-send coalescing troubleshooting checklist — common causes are too-tight debounce window, session-log timestamps misread as webhook arrival, or a reply-quote send (which usesreplyToBody, not a second webhook).- For status/health info:
openclaw status --alloropenclaw status --deep.
Related
- Channels Overview — all supported channels
- Pairing — DM authentication and pairing flow
- Groups — group chat behavior and mention gating
- Channel Routing — session routing for messages
- Security — access model and hardening