Bi-directional iMessage support for Isaac.
Runs as an Isaac module on a macOS host that is logged into the
Messages app. Inbound messages are polled from
~/Library/Messages/chat.db and dispatched to the configured crew;
outbound replies are sent through Messages via osascript.
See:
PLAN.mdโ architecture and MVP scopeROADMAP.mdโ phased delivery plan
iMessage support requires two privacy grants on the host running Isaac. Both are configured under System Settings โ Privacy & Security:
- Full Disk Access โ needed to read
~/Library/Messages/chat.db. Grant to the binary that runs Isaac (your terminal,clojure,bb, or the Java executable the JVM resolves to). - Automation โ Messages โ needed to send iMessages via
osascript. Grant the same binary the ability to control Messages. The first time Isaac callsosascript, macOS may prompt; the prompt only appears in a GUI session.
The integration must run in a logged-in GUI session on macOS โ not a headless daemon โ because Messages.app must be running for sends and the chat.db is owned by the GUI user.
Add comms.imessage to your ~/.isaac/config/isaac.edn:
{:comms {:imessage {:imessage/db-path "/Users/zane/Library/Messages/chat.db"
:imessage/bin "/usr/local/bin/imsg"
:imessage/allow-from ["+15551234567" "[email protected]"]}}}All slice keys live in the :imessage/ keyword namespace so the comm
config doesn't collide with anything Isaac (or another module)
might inject into the same map.
-
:imessage/serviceโ transport imsg should use:"auto","sms", or"imessage". Omit it unless you mean to pin the transport; Isaac then passes no service and imsg picks, which is the path that works. Do not set"imessage": through imsg's AppleScript transport an explicit imessage send reports-32001 "Delivery outcome unknown"and never arrives (measured 2026-09-24 against a live iMessage chat), while the same send with no service โ or"auto"โ delivers on the first attempt. -
:imessage/db-pathโ absolute path to the Messages chat database. Required to spawn the imsg subprocess; omitting it leaves the comm dormant (handy for non-Mac dev, and a guard so tests can't accidentally hit a real chat.db). -
:imessage/binโ path to the imsg binary. Defaults to whatever's onPATH; set explicitly when the process launching isaac doesn't see/usr/local/binor/opt/homebrew/bin(common for headless processes like launchd jobs). Ignored when:imessage/commandis set. -
:imessage/commandโ optional vector of strings forming the full launch prefix beforerpcand--db. Use this to run imsg through a stdio wrapper (for example SSH to a remote Mac)::imessage/command ["ssh" "-T" "[email protected]" "/usr/local/bin/imsg"]
Isaac appends
rpcand, when configured,--db <path>to this prefix. With a wrapper,:imessage/db-pathis the path on the machine where imsg runs (not checked for local existence); omit the wrapper to keep the default local chat.db readiness check. -
:imessage/allow-fromโ phone numbers / emails (string allowlist). Notifications from senders not in this list are dropped at debug log level (:imessage.intake/drop-sender). Fail-closed: an empty list drops everything; omit:imessage/allow-fromto skip filtering entirely.
If the machine running Isaac cannot reliably send Apple Events to
Messages under launchd, move the imsg boundary onto a different
logged-in Mac and tunnel stdio over SSH:
{:comms {:imessage {:imessage/command ["ssh" "-T" "[email protected]" "/usr/local/bin/imsg"]
:imessage/db-path "/Users/zane/Library/Messages/chat.db"
:imessage/allow-from ["[email protected]"]}}}This does not "fix" TCC on the local Isaac host. It avoids the local
permission boundary by running imsg on the remote Mac instead. In
that setup:
- the remote Mac must be logged in to Messages and hold the needed Full Disk Access / Automation grants
:imessage/db-pathis the remote machine'schat.dbpath- the local Isaac host only needs SSH access to the remote
imsgcommand - same-host self-SSH is possible in principle, but a separate Messages Mac is the cleaner deployment
Optional:
:imessage/message-capโ split replies above this character count into multiple sends. Default 2000.:imessage/max-chunksโ hard cap on how many chunks a single reply produces. Above the cap the tail is dropped with a truncation notice so a runaway LLM response can't flood Messages. Default 3 (โ6000 chars total at the default:imessage/message-cap).
Run the smoke check before bringing Isaac up:
bb smoke
Verifies osascript, sqlite3, and a successful read of
chat.db. To also confirm Automation permission for Messages,
pass a handle + body:
bb smoke +15551234567 "test from isaac-imessage"
A real iMessage should appear in your Messages window. If the
send fails with Not authorized to send Apple events, you
haven't granted Automation. If the chat.db read prints
exit=1 err=unable to open database, you haven't granted Full
Disk Access.
When using :imessage/command with an SSH wrapper, run the equivalent
probe against the remote Mac instead of relying on the local bb smoke
result. For example:
ssh -T [email protected] /usr/local/bin/imsg send --to +15551234567 --text "remote smoke" --service imessage --jsonisaac server
On startup, Isaac discovers this module via classpath manifest,
registers the imessage Comm in the comm registry, and starts
the poller. Look for these log events to confirm it's wired:
:comm/activatedcomm=imessage:imessage.poller/startedinterval-ms=โฆ
Inbound iMessages from allow-listed senders should now route to
sessions named imessage:<chat-guid>, and crew responses should
land back in the Messages thread.
bb spec # unit specs (66+ examples)
bb features # gherclj feature suite
Both should be green before pushing. The pre-push hook (if installed) will re-run them.
MIT
Copyright (c) 2026 Micah Martin
See LICENSE.
