imessage channel with two pluggable backends. You pick one at install time (or force it with IMESSAGE_BACKEND=local|hosted); only one runs per install.
- Local — the Chat SDK bridge over
chat-adapter-imessage(0.1.1, pinned), reading this Mac’s signed-in iMessage account (chat.db) directly. macOS only, and the Node binary needs Full Disk Access. It reads Messages on this Mac without a hosted iMessage relay, so NanoClaw must run on the Mac that’s signed in. Your configured model provider still processes the messages sent to the agent. - Hosted — a native adapter connecting through Photon, a managed service that owns the iMessage line, delivery, and abuse prevention. The
spectrum-tsSDK (11.0.0, pinned) holds a long-lived gRPC stream to Photon in-process on the host — no Mac relay, no webhook, no public URL. Works on any OS, and Photon offers a free tier to start with.
Prerequisites
- A working NanoClaw install (quickstart)
- Local backend: a Mac signed in to iMessage, and the ability to grant Full Disk Access in System Settings (the wizard walks you through it)
- Hosted backend: a photon.codes account (the wizard signs you in — free tier works) and the phone number you iMessage from, in
+E.164 format
Install
iMessage is offered in the first-run setup wizard (“local Mac or hosted iMessage”), or add it later by running/add-imessage in your coding harness. The flow:
1
Pick a backend
local (this Mac) or hosted (Photon). Local hard-requires macOS — the skill checks for Darwin and stops if you pick local anywhere else.2
Adapter install
The skill copies the unified adapter from the
channels branch and installs the backend’s pinned package — chat-adapter-imessage@0.1.1 for local, spectrum-ts@11.0.0 for hosted. Idempotent: safe to re-run.3
Backend setup
Local: grant Full Disk Access (below), then enter the phone number or email you iMessage from — this becomes the platform ID wired to your first agent.Hosted: enter your phone number in
+E.164 format (email handles are local-only), then the wizard runs the Photon device login (below).4
Configuration write + restart
setup/channels/imessage-configure.sh <backend> writes the backend selection to .env — local sets IMESSAGE_BACKEND=local and IMESSAGE_ENABLED=true; hosted sets IMESSAGE_BACKEND=hosted and removes IMESSAGE_ENABLED — and the service restarts.5
Name the agent and get the welcome message
The wizard asks for your operator role and an agent name (default
Nano), wires your handle to your first agent group, and sends a welcome iMessage. On the hosted backend the welcome only goes out after you’ve texted the line once — see the warning below./manage-channels. Group chats are supported; on the hosted backend a group’s ID is the opaque Spectrum space ID, discovered when the group first messages the line.
Local backend: Full Disk Access
The adapter reads the signed-in account’schat.db, which requires Full Disk Access granted to the Node binary NanoClaw runs under. The wizard opens the folder containing your node binary in Finder, then walks you through System Settings → Privacy & Security → Full Disk Access → click + → drag the node file in → toggle it on. Without this, inbound messages never arrive.
The grant binds to the one binary you dragged in, resolved via which node at setup time. If you switch Node versions (e.g. with nvm), the path changes and the grant no longer covers the binary NanoClaw runs under — re-grant for the new one.
Hosted backend: Photon device login
The wizard drivesscripts/photon-setup.ts, which walks you through connecting a Photon project:
- Sign in — the terminal prints a URL and a short code (valid for about 30 minutes). Complete the sign-in on any device; the token is cached with
0600permissions atdata/photon-auth.json, so re-runs skip this step. - Project provisioning — the wizard creates or reuses a Photon project and its secret, regenerating the secret only when the API doesn’t return one.
- Credentials written —
PHOTON_PROJECT_IDandPHOTON_PROJECT_SECRETland in.envbefore the opt-in step. - Opt in with one text — the wizard registers your phone number with the project (it finds or creates the user entry itself) and prints the iMessage line Photon assigned to it. Send one message from your phone to that line: Photon marks the number opted in only after that first text. The wizard polls (10 minutes by default) until the opt-in lands rather than reporting success early. Re-runs reuse the existing registration, and an already-opted-in number passes immediately. The assigned number is recorded in
data/photon-auth.jsonat the end.
Media and features (hosted)
- Inbound attachments and voice notes are read off the stream (with retry on transient failures) and staged into the session inbox, capped by
PHOTON_MAX_INLINE_ATTACHMENT_BYTES(default 20 MB). Over-cap or failed attachments are skipped and annotated in the message text. - Outbound files are sent as native attachments, before their caption text.
- Markdown is on by default (
PHOTON_MARKDOWN=true) — iMessage renders it natively. - Tapbacks work in both directions, and the adapter reports typing indicators and read receipts.
- Interactive questions (
ask_user_question) arrive as messages whose answer commands are derived from the option labels (e.g./approve,/reject).
Configuration reference
Set in.env. The first two select the backend; the PHOTON_* vars apply to hosted only. The two *_HOST overrides are read by the setup wizard from its process environment (or --dashboard-host / --spectrum-host flags), not from .env.
Platform notes
- Backend resolution is deterministic — an explicit
IMESSAGE_BACKENDwins; otherwise Photon credentials imply hosted andIMESSAGE_ENABLEDimplies local. If both are configured without the selector, hosted wins and the adapter logs a warning. If neither backend has its credentials, the channel is skipped (a debug-level log line names both credential paths — visible only withLOG_LEVEL=debug). - Switching backends — re-running
/add-imessage(orimessage-configure.shdirectly) rewritesIMESSAGE_BACKEND, and the explicit selector always beats leftover keys, so stale credentials from the other backend are ignored. Switching to hosted also removesIMESSAGE_ENABLED; switching to local leaves thePHOTON_*keys in place. - Engage defaults — DMs engage on every message; in group chats the agent engages when its name is mentioned; unknown senders fall under the
strictpolicy. See the entity model for how to change these per wiring. - Platform IDs — DMs use the counterpart’s bare handle (
+15551234567, or an email on local); hosted groups use the opaque Spectrum space ID. - No threads — the adapter sets
supportsThreads: falseon both backends; thread IDs map 1:1 to conversations, soper-threadsession mode behaves likesharedhere.
Troubleshooting
- iMessage channel never starts — the adapter needs its backend’s credentials: local needs
IMESSAGE_ENABLED=true, hosted needs bothPHOTON_PROJECT_IDandPHOTON_PROJECT_SECRET. When they’re missing the channel is skipped quietly — the log line naming both paths is debug-level, so setLOG_LEVEL=debugto see it. On hosted,grep "Photon channel connected" logs/nanoclaw.logconfirms the stream is up. - Inbound messages never arrive (local) — Full Disk Access is missing, or granted to a different
nodebinary than the one the service runs under. Runopen "$(dirname "$(which node)")", re-drag thatnodefile into Full Disk Access, and restart the service. Target not allowed for this project(hosted) — the recipient’s number isn’t opted in to the line. Send one message from that phone to the line assigned to it, then retry. If the number was never registered, re-run/add-imessage;photon-setup.ts statusre-checks the opt-in live.- Device login timed out or the code expired (hosted) — the URL and code last about 30 minutes, and a failed login caches nothing. Re-run
/add-imessagefor a fresh URL and code; once a login has succeeded, re-runs skip sign-in via the cached token and reuse the already-provisioned project. spectrum-tsnot installed (hosted) — the backend’s package didn’t land. Re-run/add-imessageand pick hosted; the install step is idempotent.- Installed but silent — run the adapter’s registration test from your NanoClaw checkout:
pnpm exec vitest run src/channels/imessage-registration.test.ts. - “Couldn’t finish connecting” at the end of setup — the welcome message didn’t go out. Local: double-check Full Disk Access. Hosted: confirm you texted the assigned line once from your phone — that first text is the opt-in.