A lightweight REST + WebSocket bridge for iMessage on macOS.
Send and receive messages programmatically. No Electron. No Firebase. Just a thin API.
Quick Start • API • WebSocket • Deployment • How It Works
Existing iMessage automation tools are either bloated (BlueBubbles, 200MB+ Electron app) or painfully slow (AppleScript with 120s+ send times). imsg-bridge wraps the imsg CLI — which talks directly to Apple's private iMessage frameworks — and exposes a clean async API that sends messages in under a second.
| BlueBubbles | AppleScript | imsg-bridge | |
|---|---|---|---|
| Send latency | ~2s | ~120s | <1s |
| Memory | ~200MB | ~50MB | ~30MB |
| Dependencies | Electron, Firebase | osascript | FastAPI, uvicorn |
| Real-time receive | Polling | None | WebSocket stream |
- macOS (Sonoma / macOS 14+ recommended)
- Python 3.12+
imsgCLI installed at/opt/homebrew/bin/imsg- iMessage signed in and working in Messages.app
- Full Disk Access granted to your terminal app and Python (for
chat.dbreads)
Use a one-liner like:
claude -p "Install, configure, test, and optionally deploy imsg-bridge from https://github.com/heyfinal/imsg-bridge.git on this macOS machine."
Or:
codex exec "Install, configure, test, and optionally deploy imsg-bridge from https://github.com/heyfinal/imsg-bridge.git on this macOS machine."
gemini -p "Install, configure, test, and optionally deploy imsg-bridge from https://github.com/heyfinal/imsg-bridge.git on this macOS machine."
If you already have the repo locally, generate a tool-specific one-liner and copy it to clipboard:
./setup.sh --ai-prompt codex
# or: ./setup.sh --ai-prompt claude
# or: ./setup.sh --ai-prompt geminiIf you are already inside an interactive AI terminal, generate a plain copy/paste task prompt:
./setup.sh --ai-taskPaste this inside Claude/Codex/Gemini interactive mode:
Install, configure, test, and optionally deploy imsg-bridge from https://github.com/heyfinal/imsg-bridge.git on this macOS machine.
curl -fsSL https://raw.githubusercontent.com/heyfinal/imsg-bridge/main/install.sh | bashThis clones the repo to ~/.imsg-bridge, creates a virtual environment, and installs dependencies using only Python's built-in venv and pip. No Homebrew, no uv, no global installs.
git clone https://github.com/heyfinal/imsg-bridge.git
cd imsg-bridge
uv syncgit clone https://github.com/heyfinal/imsg-bridge.git
cd imsg-bridge
python3 -m venv .venv
.venv/bin/pip install -e .cd imsg-bridge # or ~/.imsg-bridge if you used the one-liner
# Generate auth token and install as a background service
./setup.sh
# Or run manually
imsg-bridge
# → Listening on configured host:5100All endpoints require a bearer token in the Authorization header:
Authorization: Bearer <your-token>
The token is generated by setup.sh and stored in your macOS Keychain.
curl -X POST http://127.0.0.1:5100/send \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"to": "+15551234567", "text": "Hello from the bridge"}'curl http://127.0.0.1:5100/chats \
-H "Authorization: Bearer $TOKEN"curl "http://127.0.0.1:5100/history/23?limit=50" \
-H "Authorization: Bearer $TOKEN"curl http://127.0.0.1:5100/health \
-H "Authorization: Bearer $TOKEN"Returns {"status": "ok", "imsg_version": "<detected-version>"} when everything is working.
curl http://127.0.0.1:5100/pingReturns {"status": "ok"} — useful for monitoring, load balancers, and launchd health checks.
curl "http://127.0.0.1:5100/contact-name?identifier=%2B15551234567" \
-H "Authorization: Bearer $TOKEN"Returns {"identifier": "+15551234567", "name": "John Doe"} from the macOS Contacts database.
curl "http://127.0.0.1:5100/avatar?identifier=%2B15551234567" \
-H "Authorization: Bearer $TOKEN" --output avatar.jpgReturns the contact's photo as image/jpeg, image/png, or image/tiff. Looks across all macOS AddressBook source databases (top-level + each iCloud / Exchange / CardDAV source under Sources/<UUID>/) — required when iCloud Contacts is the primary store.
curl "http://127.0.0.1:5100/attachment?path=$(python3 -c 'import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))' '~/Library/Messages/Attachments/.../photo.jpg')" \
-H "Authorization: Bearer $TOKEN" --output photo.jpgServes the file bytes for a Mac-local attachment path so non-Mac clients (e.g. the Android app) can display inline images, video, audio, and documents. Path-traversal blocked — only files under ~/Library/Messages/Attachments, /var/folders, or /tmp are accepted.
curl "http://127.0.0.1:5100/contacts-status" -H "Authorization: Bearer $TOKEN"Returns the AddressBook state so clients can surface a "no contacts found — enable iCloud Contacts" advisory when names and photos won't resolve:
{
"healthy": true,
"contact_count": 3089,
"phone_count": 3208,
"email_count": 334,
"image_count": 429,
"readable_databases": 2,
"advisory": ""
}Connect to /ws for a real-time stream of incoming messages:
websocat -H "Authorization: Bearer $TOKEN" "ws://127.0.0.1:5100/ws"Each message arrives as a JSON object:
{
"id": 43327,
"guid": "A1B2C3D4-...",
"chat_id": 23,
"text": "Hey, what's up?",
"sender": "+15551234567",
"is_from_me": false,
"created_at": "2026-03-03T06:00:00.000Z",
"attachments": [],
"reactions": []
}Authentication via Authorization: Bearer <token> header.
For backwards compatibility with query-string tokens (less secure), you can set IMSG_BRIDGE_ALLOW_WS_QUERY_TOKEN=1.
The setup.sh script handles everything:
./setup.shThis will:
- Generate a secure auth token and store it in macOS Keychain
- Let you choose bind mode (
0.0.0.0for LAN clients or127.0.0.1for local-only) - Install a LaunchAgent that starts on login and auto-restarts on crash
- Optionally deploy Linux client via:
- LAN scan for SSH hosts (or manual IP entry) + username/password prompt
- USB/external drive scan (or manual mount path)
- If Messages DB access is blocked, automatically open Full Disk Access settings and walk you through enabling it
- Verify the service is running
Logs are written to ~/Library/Logs/imessage-bridge.log and ~/Library/Logs/imessage-bridge.err.
By default, the LaunchAgent binds to 127.0.0.1 (loopback only). If you want to access the bridge from other machines on your LAN (e.g. imsg-gtk on Linux), bind to 0.0.0.0 instead:
IMSG_BRIDGE_BIND_HOST=0.0.0.0 ./setup.shThis exposes the bridge to your local network. Keep the bearer token private and consider using a firewall, Tailscale, or SSH tunneling.
imsg-bridge --host 127.0.0.1 --port 5100Or with uvicorn directly:
uvicorn imsg_bridge.bridge:app --host 127.0.0.1 --port 5100Your app / bot / agent
↕ HTTP + WebSocket
imsg-bridge (FastAPI, port 5100)
↕ async subprocess
imsg CLI (/opt/homebrew/bin/imsg)
↕ private frameworks
~/Library/Messages/chat.db
- REST endpoints spawn
imsgsubprocesses for each request (send, chats, history) - WebSocket runs a persistent
imsg watchsubprocess that streams new messages as JSONL - State persistence tracks the last processed message ROWID in
~/.imessage-bridge/state.jsonso no messages are lost across restarts - Auth uses a bearer token stored in macOS Keychain — no config files with secrets
A native Jetpack Compose tablet/phone app that mirrors macOS Messages.app over your LAN. Source lives in imsg_android/.
- Real iMessage-style UI: blue gradient bubbles (iMessage), green + 🔒 (encrypted RCS), green (SMS), with proper group-stack rounded corners
- Contact name + photo resolution via the bridge (
/contact-name,/avatar) - Pretty US phone formatting
+14053151310 → (405) 315-1310when no contact card matches - Reply quote bubbles above each reply, with sender name resolved through a cache
- Inline image attachments via
/attachment(Mac-local paths proxied with Bearer auth) - Day dividers (
Wednesday Jun 10, 2026 8:06 AM), local-timezone correct - Smart auto-scroll (only follows when you're near the bottom)
- Hardware Back navigation, dark/light mode, edge-to-edge
- Persistent WebSocket foreground service with exponential-backoff reconnect; per-message notifications when the chat isn't visible
- Android 8.0+ (API 26+), tested on Samsung Galaxy Tab A7 Lite (Android 14)
- Network access to the Mac running imsg-bridge
- For builds: JDK 17, Android SDK platform 34, Gradle 8.10 (wrapper included)
cd imsg_android
JAVA_HOME=/path/to/openjdk@17 ./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apkOr use the one-shot installer that finds the device on LAN, USB, or by MAC:
./imsg_android/scripts/install_apk.shOn first launch: open Settings → enter the bridge host (e.g. 192.168.1.211:5100) and Bearer token. The token is the same one stored in macOS Keychain under imessage-bridge.
If the chat list shows phone numbers instead of names and no photos, hit /contacts-status — if contact_count < 5, enable iCloud Contacts on the Mac (System Settings → Apple ID → iCloud → Contacts) and contacts will populate within ~5 minutes.
A native GTK4/libadwaita desktop app for reading and sending iMessages from Linux over your LAN.
- Real-time message streaming via WebSocket
- Contact name resolution and avatar display from macOS Contacts
- Inline image attachments
- Desktop notifications for incoming messages (when window is not focused)
- Reconnection banner with automatic retry
- Send failure indicator with visual feedback
- Search/filter conversations
- Right-click context menu (open, copy contact, clear)
- Dark mode support via libadwaita
- Linux with GTK 4.10+, libadwaita 1.3+
- Python 3.12+ with PyGObject
- Network access to the Mac running imsg-bridge
The easiest way is via setup.sh on the Mac, which offers SSH push or USB copy:
./setup.sh --deploy-ssh user@linux-machine
./setup.sh --deploy-usb /Volumes/USBOr install manually on Linux:
cd imsg_gtk
pip install -e .
imsg-gtkConfiguration is stored in ~/.config/imsg-gtk/config.json:
{
"host": "192.168.1.100",
"port": 5100,
"token": "your-bearer-token"
}- Setup supports both bind modes:
127.0.0.1local-only (recommended default)0.0.0.0LAN-accessible (required for direct Linux client access)
- Bearer token required on all endpoints (REST and WebSocket) except
/ping - Token stored in macOS Keychain, never on disk
- Uses constant-time token comparison for auth checks
- Rate limiting on
/send(20 requests/minute sliding window) imsgbinary path configurable viaIMSG_PATHenvironment variable- For remote access, use Tailscale or an SSH tunnel
# Install dev tooling
python3 -m pip install -e ".[dev]"
# Optional: run checks automatically on commit
pre-commit install
# Lint + tests
ruff check imsg_bridge imsg_gtk tests
pytest -qCI runs these checks on every push and pull request.
