Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Read, search, send, react to, and attach images/stickers in iMessage via the BlueBubbles REST API. Trigger on: "check my texts", "any new texts", "what did [name] say", "text [name]", "send a text", "draft a text", "search my texts", "find that text about [X]", "who is [name]", "react to that", "tapback", "send a sticker", "send a photo / image", "save that attachment", iMessage, BlueBubbles.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | 189% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 387% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 154% | 0% |
| case-16 | ✗→✓ | ▲ Improved | 219% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 169% | 0% |
This is a tool surface for an LLM agent that helps a user with their iMessages. The CLI wraps the local BlueBubbles REST API and returns a JSON envelope; you call it from your agent harness like any other command-line tool.
These have to be done by the human before the agent will be useful:
http://localhost:1234 (typically by SSH'ing to the Mac and running the CLI there).If your agent is already on the Mac with BlueBubbles:
bash./venv/bin/python messages_cli.py <subcommand>
If your agent runs elsewhere and SSHes in:
bashssh <relay-host> "cd /path/to/bluebubbles-relay && ./venv/bin/python messages_cli.py <subcommand>"
All commands return a JSON envelope:
json{ "success": true, "command": "...", "timestamp": "...", "result": "...", "details": { ... } }
Exit code is 0 on success and 1 on failure. Parse the envelope; don't rely on the exit code alone.
The CLI reads the BlueBubbles password at call-time from BlueBubbles' own config DB (~/Library/Application Support/bluebubbles-server/config.db) on the host it runs on. There is no copy of the password in the repo or in any env file. If the CLI runs on a different machine than the BlueBubbles server, BLUEBUBBLES_URL and BLUEBUBBLES_PASSWORD environment variables override the defaults.
| Command | Purpose | |---|---| | whoami | Health check + server info (includes private_api_enabled) | | chats list [--limit N] | Most-recent chats with a last-message preview | | chats info <chat-guid> | Chat metadata (participants, display name) | | chats find <query> [--limit N] [--scan M] | Fuzzy resolution — turn a casual name or partial phone/email into chat GUIDs | | messages list [--since 5m\|1h\|2d] [--chat <guid>] [--limit N] | Recent messages. The bread-and-butter call for "any new texts?". Each message includes inline attachments: [{guid, transferName, mimeType, totalBytes}] so you don't have to round-trip through messages get to know what's attached. Incoming reactions are grouped onto their target under reactions: [...]; orphan reactions are returned separately. | | messages search <query> [--chat <guid>] [--since DUR] [--limit N] | Server-side text search across the iMessage DB (also returns inline attachments) | | messages get <message-guid> | Single message detail | | attachments download <guid> [--out PATH] | Save an attachment to disk (default: ~/Downloads/bluebubbles-relay/). If iMessage has GC'd the local file (old media), this returns a clear error rather than silently producing an empty file. | | stickers list | Names of stickers in the repo's stickers/ directory |
Users talk in names ("Mom", "the boss", "Tim"), not chat GUIDs. Standard resolution flow:
chats find <name> first — fast, ranked, handles partial phone fragments and fuzzy displayNames.chats find returns multiple matches, pick the highest score only if its displayName or participant clearly matches; otherwise ask the user to disambiguate.chats list --limit 50 if chats find returns nothing.iMessage;-;) before sending. Group GUIDs use iMessage;+;.Tapbacks (love / like / dislike / laugh / emphasize / question) appear in BlueBubbles as standalone "messages" with associatedMessageGuid pointing at their target. messages list does the grouping for you: target messages get a reactions: [{type, from, dateCreated}] array, and the reaction-only messages are filtered out of the main timeline so summaries don't get noisy. The -love / -like / etc. types mean the sender removed that reaction.
| Command | Purpose | |---|---| | messages draft <chat-guid> <text> | Preview a text send — does NOT send. Run this first for any text. | | messages send <chat-guid> <text> --confirm | Actually send a text | | messages react <message-guid> <reaction> --confirm | Send a tapback. Valid: love, like, dislike, laugh, emphasize, question, and -love etc. to remove. Requires BlueBubbles Private API to be enabled. whoami should report private_api_enabled: true, otherwise this returns HTTP 500. | | messages send-image <chat-guid> <path-or-URL> [--text caption] --confirm | Send an image. Source can be a local file path on the relay or an http(s):// URL (downloaded to a tempfile, sent, then deleted). Caption is sent as a follow-up text. | | messages send-sticker <chat-guid> <name> [--text caption] --confirm | Send a sticker from the repo's stickers/ library by stem-name | | messages reply <message-guid> <text> --confirm | Convenience: reply to a specific message. Auto-resolves the chat from the message GUID — no need to look up the chat separately. | | messages reply-image <message-guid> <path-or-URL> [--text caption] --confirm | Same auto-resolve, for images | | messages reply-sticker <message-guid> <name> [--text caption] --confirm | Same auto-resolve, for stickers |
Never call a write command without showing the user what's about to go out and getting explicit approval.
messages draft and show the user the preview field verbatim.--confirm.The repo has a stickers/ directory the user populates with their own .png / .webp files (images are local-only, not tracked in git). Each file is discoverable by its stem (filename without extension) via stickers list. Path-traversal is blocked at the CLI level (no /, \, or .. in names), so you can't accidentally send arbitrary files via this command — use send-image for that.
When the user says "send a sticker" without naming one, run stickers list, pick a fitting name from what's actually there based on the chat context, and confirm the choice with the user before sending — a misfired sticker is at minimum awkward.
> Important: iMessage does not understand Discord/Twitch-style emote text like :emoteName:. If you type that, it sends the literal characters as plain text — the recipient sees :emoteName:, not a sticker. To send an actual sticker image, use messages send-sticker or messages reply-sticker.
Two modes, both supported by the same CLI:
messages list --since <window> and summarize anything actionable. Default to the window since you last checked, or --since 4h for a generic "what's new".messages list --since 5m every few minutes and nudges the user on their notification channel (Discord, Slack, etc.) only when something is clearly worth surfacing. Never auto-reply from a scheduled run — the loop is read → notify, not read → respond.When summarizing either way:
fromMe: true is the user's own outbound — skip it in summaries unless context demands it.Match the tone of the chat's prior messages; when in doubt, shorter and plainer. Always show the exact draft text before sending.
--confirm. No exceptions. Approval is the user explicitly saying "send", "yes", "looks good" — not implicit silence or general conversation.iMessage;-;+E164 for 1:1, iMessage;+;... for group. Reject or ask when only a group GUID is found for what should be a 1:1.whoami reports private_api_enabled: false, messages react will fail with HTTP 500. Don't loop or retry — tell the user Private API needs to be turned on in the BlueBubbles app (with the macOS helper bundle installed). Reads of incoming reactions work either way.--confirm flow.success: true → report the result line and summarize useful fields from details (count, messages with from/text/dateCreated).success: false → report the error and a likely next step. Common errors:HTTP 401 — password mismatch (config DB may have been edited; retry).connection refused to localhost:1234 — BlueBubbles app crashed; reopen it.HTTP 500 ... Private API — Private API not enabled (see safety rule #7).messages list with count: 0 and a recent --since window is a normal answer ("no new texts"), not a failure.textUser: "any new texts?" → messages list --since 4h --limit 30 → Filter out fromMe:true and summarize the rest by sender. → Reply: "3 new since [time]: [Sender A] asked X, [Sender B] sent Y, [Sender C] confirmed Z."
textUser: "text Mom back saying I can do 6pm" → chats find mom --limit 3 (or use a recent GUID from messages list) → messages draft <chat-guid> "Yep, 6pm works" → Show the user the preview verbatim. → Wait for "send" / "yes". → messages send <chat-guid> "Yep, 6pm works" --confirm → Report message GUID + timestamp.
textUser: "search my texts for the install steps Sarah sent" → (Optional) chats find sarah to scope the search. → messages search "install" --chat <sarah-guid> --since 30d --limit 20 → Summarize hits by date; offer messages get <guid> for full text.
textUser: "save that screenshot they sent" → Find the most recent message with hasAttachments:true in messages list. → messages get <message-guid> to get the attachment GUID(s). → attachments download <attachment-guid> → Report the saved path.
textUser: "thumbs up the last text from Tim" → Find the target's message GUID from messages list --chat <tim-guid>. → Confirm with the user: "Reacting 'like' to Tim's 'sounds good' from 3:14pm — go?" → On approval: messages react <message-guid> like --confirm → If HTTP 500 mentioning private_api: tell the user Private API isn't enabled and stop.
textUser: "send a sticker to the group chat with the boys" → chats find boys (confirm group GUID with the user) → stickers list to see what's in the library → Confirm: "Sending <sticker-name> to <group-name> — go?" → messages send-sticker <chat-guid> <sticker-name> --confirm
textUser: "reply with a sticker to Sarah's last message" → Find Sarah's most recent message GUID from messages list --chat <sarah-guid> --limit 5 → Confirm: "Replying with sticker <sticker-name> to Sarah's 'fix it nerd' from 3:14pm — go?" → messages reply-sticker <message-guid> <sticker-name> --confirm (No need to separately look up the chat GUID — reply-sticker resolves it.)
| Symptom | Likely cause / fix | |---|---| | HTTP 401 | BlueBubbles password changed in the app. The CLI re-reads on each call — should work next time. | | connection refused to localhost:1234 | BlueBubbles app not running on the host. Reopen it. | | messages list returns 0 messages but you know there are some | Messages.app may have gone idle. Re-open Messages on the Mac. | | Old data, no new messages appearing | iMessage account signed out, or Messages.app needs a periodic poke (see BlueBubbles docs). | | messages send succeeds but recipient didn't get it | AppleScript fallback is best-effort. Check Messages.app for any send-failure red banner. |
Other measured skills in the registry, with their headline benchmark lift.