Skip to main content
NanoClaw connects to Apple’s Messages network through a single 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-ts SDK (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.
Breaking change in v2.1.54. The legacy Chat-SDK remote mode (IMESSAGE_SERVER_URL / IMESSAGE_API_KEY) and the IMESSAGE_LOCAL selector are removed. An install that used remote mode should re-run /add-imessage and pick the hosted backend, then remove the legacy keys from .env — the re-run does not strip them.

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 .envlocal 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.
To wire more conversations later, run /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’s chat.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 drives scripts/photon-setup.ts, which walks you through connecting a Photon project:
  1. 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 0600 permissions at data/photon-auth.json, so re-runs skip this step.
  2. Project provisioning — the wizard creates or reuses a Photon project and its secret, regenerating the secret only when the API doesn’t return one.
  3. Credentials writtenPHOTON_PROJECT_ID and PHOTON_PROJECT_SECRET land in .env before the opt-in step.
  4. 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.json at the end.
The hosted line cannot send the first message. Text the agent’s number once from your phone before expecting anything outbound — until then, sends fail (Photon reports Target not allowed for this project), including the setup welcome message.

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_BACKEND wins; otherwise Photon credentials imply hosted and IMESSAGE_ENABLED implies 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 with LOG_LEVEL=debug).
  • Switching backends — re-running /add-imessage (or imessage-configure.sh directly) rewrites IMESSAGE_BACKEND, and the explicit selector always beats leftover keys, so stale credentials from the other backend are ignored. Switching to hosted also removes IMESSAGE_ENABLED; switching to local leaves the PHOTON_* 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 strict policy. 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: false on both backends; thread IDs map 1:1 to conversations, so per-thread session mode behaves like shared here.

Troubleshooting

  • iMessage channel never starts — the adapter needs its backend’s credentials: local needs IMESSAGE_ENABLED=true, hosted needs both PHOTON_PROJECT_ID and PHOTON_PROJECT_SECRET. When they’re missing the channel is skipped quietly — the log line naming both paths is debug-level, so set LOG_LEVEL=debug to see it. On hosted, grep "Photon channel connected" logs/nanoclaw.log confirms the stream is up.
  • Inbound messages never arrive (local) — Full Disk Access is missing, or granted to a different node binary than the one the service runs under. Run open "$(dirname "$(which node)")", re-drag that node file 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 status re-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-imessage for 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-ts not installed (hosted) — the backend’s package didn’t land. Re-run /add-imessage and 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.
For service-level checks (logs, restarts, wiring queries), see troubleshooting.
Last modified on September 4, 2026