paulyuk/imessage-bridge

OpenClaw and AI Harness compatible bridge to leverage Imessage on MacOS

β˜… 0Forks 0JavaScriptGitHub β†—Compare

README

πŸ“¨ imessage-bridge

Send iMessages from anywhere. Without a Mac on the public internet.

A small, opinionated bridge that lets an inexpensive Linux/cloud "producer" (or your favorite openclaw 🦞) enqueue messages securely and async over AMQP 1.0 into a managed broker, and a tiny agent on your Mac pulls them out and sends them through Messages.app. No inbound ports to attack. No SAS keys. No PATs.

npm version 🦞 openclaw skill AMQP 1.0 Dapr-friendly OAuth only Brady Gaster Squad License: MIT macOS LaunchAgent

Quick start Β· Architecture Β· Standards & portability Β· Install Β· Troubleshooting Β· Security Β· Contributing


✨ Why this exists

iMessage is a walled garden. If you want to send an iMessage programmatically, you need a Mac signed into iCloud running Messages.app. That Mac shouldn't be exposed to the internet, and you shouldn't be juggling long-lived secrets to talk to it.

imessage-bridge solves both:

  • Producer-anywhere, consumer-on-Mac β€” the producer is just a cmdline tool call for humans or agents to send a message securely like you would want to in a claw; the Mac only makes outbound calls to the broker and then forwards to iMessage via Messages.app. Nothing inbound. No port forwarding. No tunnels.
  • OAuth + Microsoft Entra identities β€” no SAS connection strings, no PATs, no .env files full of secrets, or other things to get complicated and get Pwnd. Both ends authenticate via DefaultAzureCredential. Don't let Entra scare you β€” it works with Free Azure accounts and consumer outlook.com accounts. Or just take this pattern and use the ecosystem you prefer for identity and cloud hosting. No biggie.
  • Standards under the hood β€” the wire protocol is AMQP 1.0 (OASIS / ISO/IEC 19464), not a vendor-proprietary format. Your queue is just a queue. See Standards & portability below.
  • Built in the openclaw 🦞 spirit β€” own your data, own your infra, ship one more "claw" into a walled-garden ecosystem so your agents can act on your behalf. Each skills/ entry follows the openclaw skill format so this folder drops cleanly into any openclaw-style runtime (or any other agent framework that consumes SKILL.md).
  • Tiny surface area β€” ~100 lines of producer + ~150 lines of agent. Easy to read, easy to fork, easy to trust.

⚑ Quick start

The 60-second demo. Once you've done the one-time Azure setup below, sending an iMessage from anywhere is one line:

npx imessage-bridge@alpha send --to "+15555550100" --body "hi from the bridge 🐩"

That's it. npx fetches the package on demand and runs it β€” nothing to install, nothing to clean up. (The @alpha tag is pinned through the v0.2 prerelease β€” once we ship stable, drop the @alpha.)

You'll need: Node.js 22+ LTS (recommend nvm install 24), an Azure subscription (free tier is fine), the az CLI, and a Mac signed into iMessage. The Mac can be anywhere with internet access (it makes outbound calls to the broker β€” no inbound exposure needed). Or if your producer is on the same private network, you can swap Service Bus for a local broker (Redis + the Dapr extension, RabbitMQ, ActiveMQ Artemis, …) β€” see Standards & portability.

Two ways to drive this

This project ships both a terminal CLI (via npm) and a set of operational skills under skills/. Pick whichever matches how you work β€” they do exactly the same things.

Goal πŸ–₯️ Terminal πŸ’¬ Or just say to your agent
Send a message npx imessage-bridge@alpha send --to "+15555550100" --body "hi" "send 'hi' to +15555550100 via the bridge" β†’ send-message
Send a Signal message npx imessage-bridge@alpha signal-send --to "+15555550100" --body "hi" "send 'hi' to +15555550100 via Signal"
Run the Mac receiver (foreground) npx imessage-bridge@alpha agent "run the imessage-bridge receiver"
Install on a Linux/cloud/openclaw producer npm i -g imessage-bridge@alpha (or just npx) "install the imessage-bridge producer on this box" β†’ install-producer
Install on the receiving Mac (as a daemon) follow step 6 below "install imessage-bridge on this Mac as a daemon" β†’ install-mac
Health check npx imessage-bridge@alpha doctor "is the imessage-bridge healthy?" / "doctor" β†’ doctor
Find a message in the logs grep <uuid> logs/agent.log "did the iMessage to +1… go through?" / "show me the bridge logs" β†’ logs

🦞 Skill consumers: Copilot CLI, Cursor, Claude Code, and any openclaw runtime pick up the skills/ folder automatically β€” cd into the repo (Copilot CLI / Claude Code), include it in your workspace (Cursor), or symlink: ln -s "$(pwd)/skills" ~/.openclaw/skills/imessage-bridge. Each SKILL.md has trigger_phrases frontmatter so the right runbook surfaces on the right intent.

The rest of this section is the first-time setup walkthrough: provision the queue once, grant two RBAC roles, drop a tiny config.json next to where you'll run the CLI, and you're done. After that, every send is npx imessage-bridge@alpha send ….

1. Provision the queue (~2 minutes)

The bridge runs on any AMQP 1.0 broker; Azure Service Bus is the immediate default implementation because it's the cheapest managed AMQP-with-OAuth/Entra option (~$0/month at our volume) and zero ops. To use a different broker (RabbitMQ, ActiveMQ Artemis, Solace, …), see Standards & portability below β€” the producer/agent stay the same.

RG=imessage-bridge
NS=$RG-$(whoami)                # namespace must be globally unique
QUEUE=imsg-queue
LOC=westus2

az group create -n $RG -l $LOC
az servicebus namespace create -g $RG -n $NS --sku Basic
az servicebus queue create -g $RG --namespace-name $NS -n $QUEUE

2. Grant least-privilege RBAC roles β€” one role per machine

Each machine logs in as its own Azure AD identity and gets only the role it needs. No shared identities, no Service Principals, no secrets.

SCOPE=$(az servicebus queue show -g $RG --namespace-name $NS -n $QUEUE --query id -o tsv)

On the producer machine (sends only):

az login --use-device-code            # log in as the producer identity
ME=$(az ad signed-in-user show --query id -o tsv)
az role assignment create --assignee $ME --role "Azure Service Bus Data Sender" --scope $SCOPE

On the Mac (receives only):

az login                              # log in as the Mac identity
ME=$(az ad signed-in-user show --query id -o tsv)
az role assignment create --assignee $ME --role "Azure Service Bus Data Receiver" --scope $SCOPE

πŸ” Identity-only auth. This project never uses Service Principals, client secrets, certificates, SAS keys, or PATs. Both sides authenticate with az login (Azure AD user identity); DefaultAzureCredential discovers the cached token. See SECURITY.md.

3. Drop a config.json next to where you'll run the CLI

The npm package reads ./config.json from the current directory (or $IMSG_CONFIG if set). No clone required.

After you provisioned the namespace in step 1, determine the namespace FQDN and put it into config.json. The namespace FQDN is simply:

<your-namespace>.servicebus.windows.net

Example: if you created NS=$RG-$(whoami) and that evaluated to imessage-bridge-yourname, the namespace FQDN is:

imessage-bridge-yourname.servicebus.windows.net

Create the config file from the shell (safe / scriptable):

NS=imessage-bridge-yourname   # or whatever you picked above
FQDN="$NS.servicebus.windows.net"
cat > config.json <<JSON
{
  "namespace_fqdn": "$FQDN",
  "queue": "imsg-queue",
  "poll_interval_s": 3,
  "log_path": "./logs/agent.log",
  "message_prefix": "[m365]",
  "signature": "⚑",
  "allowed_recipients": [
    "+15555550100"
  ]
}
JSON

Prefer to keep config out of the working dir? Set IMSG_CONFIG=~/.config/imessage-bridge.json and the CLI will pick it up.

Notes:

  • config.json is gitignored. Do not commit it.
  • namespace_fqdn must exactly match the Service Bus namespace FQDN (no protocol, no trailing slash).
  • allowed_recipients is an optional E.164 allowlist enforced by the Mac consumer. A queued message addressed to another number is dead-lettered before it can reach Messages.app.
  • message_prefix and signature label every outbound message at the consumer boundary. The producer cannot bypass them.
  • If you’re unsure what the namespace name is, you can list namespaces with:
az servicebus namespace list -g $RG -o table

and inspect the name column β€” append .servicebus.windows.net to form the FQDN.

4. Log in once with OAuth

az login                                 # opens browser; tokens cached in ~/.azure
# OR for headless boxes:
az login --use-device-code

That's it for auth. DefaultAzureCredential picks up the cached az tokens automatically. No connection strings, no PATs, no SAS keys.

5. Send a message

# from anywhere (Linux, Mac, cloud) β€” enqueue:
npx imessage-bridge@alpha send --to "+14255551234" --body "hey from the bridge πŸ‘‹"

# on the Mac β€” start the consumer in the foreground:
npx imessage-bridge@alpha agent

The Mac picks it up within a few seconds and Messages.app sends it. ✨

Signal producer

Add signal_queue to the same producer configuration:

{
  "namespace_fqdn": "your-namespace.servicebus.windows.net",
  "queue": "imsg-queue",
  "signal_queue": "signal-queue"
}

Then enqueue through the independent Signal queue:

npx imessage-bridge@alpha signal-send --to "+14255551234" --body "Signal smoke test"

The producer needs Azure Service Bus Data Sender scoped to signal-queue. signal-send does not require a Signal account or signal-cli; those are only needed by the receiving Signal consumer. To configure that receiver, including its Signal account, signal-cli, and Azure Service Bus Data Receiver role, follow the Mac deployment guide.

6. Make the Mac agent permanent

The safe path is one clear arc: foreground-test β†’ install β†’ verify β†’ done.

⚠️ Do the foreground run first. macOS must show the Automation prompt for Messages.app, and you must click Allow. Under launchd, macOS cannot show that prompt β€” the agent will just fail with "Not authorized."

npx imessage-bridge@alpha agent     # ctrl-C after you click Allow on the Automation prompt

For the persistent LaunchAgent installer, clone the repo (it ships the plist template + installer):

gh repo clone paulyuk/imessage-bridge && cd imessage-bridge
./mac/launchd/install.sh      # detects node, installs deps, renders plist, registers with launchd, starts now
launchctl print gui/$(id -u)/com.imessage-bridge.agent | head -20 # status
tail -F logs/agent.log                                          # follow app logs

The installer is idempotent, so re-run it after git pull to pick up new code. Full daemon setup and common commands: INSTALL.md. Troubleshooting starts with the two log files: TROUBLESHOOTING.md.

Persistent Messages automation permission

The default osascript path can cause macOS to repeatedly prompt for Messages automation because it is an unbundled command-line process. On a Mac with Xcode Command Line Tools, install the included signed local helper before the first foreground send:

./mac/automation/install-helper.sh

Add the printed path to automation_helper_path in config.json, then run one foreground smoke test and approve iMessage Bridge when macOS asks to control Messages. The helper has the stable local identifier com.paulyuk.imessage-bridge.automation, so the Automation approval persists across daemon restarts.

πŸ— Architecture

flowchart LR
    subgraph anywhere["πŸ’» Anywhere β€” Linux / cloud / your bot"]
        P["Producer<br/><code>npx imessage-bridge send</code>"]
    end

    subgraph azure["☁️ Cloud β€” managed AMQP 1.0 broker (Azure Service Bus)"]
        AAD[("πŸ” Azure AD")]
        SB["Service Bus Queue<br/><b>imsg-queue</b><br/><sub>AMQP 1.0 over TLS</sub>"]
    end

    subgraph mac["πŸ–₯️ Your Mac β€” signed into iMessage"]
        A["Agent<br/><code>npx imessage-bridge agent</code>"]
        M["Messages.app"]
    end

    iMsg(["πŸ’¬ iMessage recipient"])

    P -- "OAuth token" --> AAD
    A -- "OAuth token" --> AAD
    P == "send (AMQP 1.0) β€” Data Sender role" ==> SB
    SB == "long-poll receive (AMQP 1.0) β€” Data Receiver role" ==> A
    A -- "signed local helper" --> M
    M -. "send" .-> iMsg

    classDef az fill:#0078d4,stroke:#005a9e,color:#fff
    classDef host fill:#f4f4f5,stroke:#a1a1aa,color:#18181b
    classDef ext fill:#fff,stroke:#10b981,color:#065f46
    class SB,AAD az
    class P,A,M host
    class iMsg ext
Loading
  • Producer authenticates with DefaultAzureCredential and has only the Azure Service Bus Data Sender role on the queue.
  • Mac agent authenticates the same way and has only the Azure Service Bus Data Receiver role.
  • Two distinct Azure AD identities, one role each β€” least privilege, no shared secrets, no Service Principals.
  • The Mac only makes outbound calls β€” no inbound ports, no tunnels, no exposed surface.
  • Service Bus tier: Basic. ~$0/month at our volume (no base fee, ~$0.05/M ops).

Message lifecycle

sequenceDiagram
    autonumber
    participant P as Producer
    participant SB as Service Bus
    participant A as Mac Agent
    participant M as Messages.app
    participant R as Recipient

    P->>SB: send(payload, message_id)
    Note over SB: durable, retry-friendly
    A->>SB: receive (long-poll)
    SB-->>A: message (peek-lock)
    A->>M: osascript "send"
    alt success
        M-->>R: iMessage delivered
        A->>SB: complete(message)
    else automation helper fails
        A->>SB: abandon(message) β€” retry
    else bad payload
        A->>SB: dead_letter(message)
    end
Loading

πŸ“ Standards & portability

This project is built on standards, not vendor primitives. The default deployment uses Azure Service Bus because it's a cheap, managed AMQP broker with first-class Azure AD auth β€” but the wire format and patterns are portable.

Layer Standard / Spec Why it matters
Wire protocol AMQP 1.0 (OASIS, ISO/IEC 19464) Same protocol RabbitMQ, ActiveMQ Artemis, Solace, IBM MQ, AWS MQ, and Azure Service Bus all speak. Your queue is just a queue.
Auth OAuth 2.0 + OpenID Connect (Microsoft Entra as the IdP) No SAS keys, no PATs, no client secrets. Token-based, instantly revocable.
Phone format E.164 The same format Twilio, Telegram, WhatsApp, and SMS gateways all expect.
Message shape JSON, AMQP message_id for idempotency Trivial to interop with anything.
Skill format SKILL.md frontmatter + sections, as used by openclaw 🦞 The whole skills/ folder drops cleanly into any openclaw runtime β€” install-mac, install-producer, send-message, doctor, logs are all reusable claws.

Swap the broker

The default flow uses azure-servicebus (an AMQP 1.0 client). To run against a different AMQP 1.0 broker (e.g. RabbitMQ, ActiveMQ Artemis, Apache Qpid), swap the client library β€” the producer/consumer logic stays the same. Roughly ~30 lines change, mostly imports and connection setup.

Or use Dapr

For full broker portability without swapping client libraries, run the producer behind a Dapr sidecar and use the pubsub building block. Dapr ships pluggable pubsub components for Service Bus, RabbitMQ, Kafka, NATS, Redis, AWS SNS/SQS, GCP Pub/Sub, and ~15 others β€” same producer code, change one config file to switch broker. A examples/dapr/ reference implementation is on the roadmap.

TL;DR: This isn't an Azure-only toy. Azure Service Bus is the default because it's the cheapest AMQP-with-AAD-OAuth broker on the market for low volumes (~$0/month at our scale). Everything else is standards.

πŸ” Security

Auth model: identity-only. No long-lived secrets, anywhere, ever.

  • βœ… DefaultAzureCredential everywhere β€” backed by az login (Azure AD user identity).
  • βœ… Producer and consumer (Mac) are two distinct Azure AD users, each with only one RBAC role on the queue (Sender on the producer host, Receiver on the Mac). True least privilege.
  • βœ… Tokens cached + refreshed by Azure CLI in ~/.azure. Revoke instantly via az logout or by removing the role assignment.
  • ❌ No Service Principals. No client secrets, no certificate-as-secret, no federated credentials with PATs.
  • ❌ No SAS connection strings. Anywhere.
  • ❌ No PATs. GitOps uses gh auth login (OAuth web flow) only.
  • ❌ No secrets committed. PRs are PII-scanned for phone numbers, emails, and key patterns.

Upgrade path β€” only these are acceptable when user identity isn't enough:

  • Managed Identity (workload runs in Azure)
  • Workload Identity Federation (workload outside Azure, federated to AAD without secrets)
  • Azure Arc (bring the host into Azure as a managed resource)

See SECURITY.md for the full rule + threat model.

πŸ“ Repo layout

imessage-bridge/
β”œβ”€β”€ producer/
β”‚   β”œβ”€β”€ __init__.py
β”‚   └── cli.py               # enqueue CLI β€” runs anywhere
β”œβ”€β”€ mac/
β”‚   β”œβ”€β”€ agent.py             # long-running consumer
β”‚   β”œβ”€β”€ send_applescript.py  # osascript wrapper
β”‚   β”œβ”€β”€ requirements.txt
β”‚   └── launchd/
β”‚       β”œβ”€β”€ com.imessage-bridge.agent.plist  # LaunchAgent template
β”‚       β”œβ”€β”€ install.sh                     # render + bootstrap
β”‚       └── uninstall.sh                   # bootout + remove
β”œβ”€β”€ infra/
β”‚   └── azure-quickstart.md  # az cli provisioning
β”œβ”€β”€ extensions/              # opt-in sibling packages β€” see extensions/README.md
β”‚   └── dapr/                # Dapr pubsub variant (Redis / Service Bus / Kafka / …)
β”œβ”€β”€ .squad/                  # Brady Gaster Squad config
β”œβ”€β”€ AGENTS.md                # binding rules for human + bot agents
β”œβ”€β”€ README.md                # you are here
└── config.example.json

Want a different broker? extensions/dapr/ ships a Dapr-pubsub variant that runs over Redis / Azure Service Bus / Kafka / RabbitMQ / AWS SNS+SQS / GCP Pub/Sub via a config swap. Source-only for v0.2.x β€” clone the repo and follow extensions/dapr/README.md.

🀝 Contributing

PRs welcome. This repo runs the Brady Gaster Squad β€” every PR triggers squad validation (PII flag + schema check).

  • Default branch: main. Never master.
  • DevRel agent owns this README. Improvements to docs are extra-welcome.
  • Read CONTRIBUTING.md for the PR flow.

πŸ“„ License

MIT β€” see LICENSE.


Built with 🐩 by the Brady Gaster Squad.

πŸ‰ ABB

Contributors

paulyuk

Issues