Add iMessage channel integration via Chat SDK. Local (macOS) or remote (Photon API) mode.
69
83%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
High
Do not use without reviewing
Adds iMessage support via the Chat SDK bridge. Two modes: local (macOS with Full
Disk Access) or remote (Photon API). NanoClaw doesn't ship channels in trunk —
this skill copies the iMessage adapter in from the channels branch.
The mechanical steps under Apply carry nc: directive fences: an agent reads
the prose and applies them, and a parser can apply them deterministically from
the same document. Every directive is idempotent, so the whole skill is safe to
re-run; anything a parser can't apply falls back to the prose beside it.
Fetch the channels branch and copy the iMessage adapter into src/channels/
(overwrite — the branch is canonical):
src/channels/imessage.ts
src/channels/imessage-registration.test.tsAppend the self-registration import to the channel barrel (skipped if the line is already present). This one line is the skill's only reach-in into core:
import './imessage.js';Pinned to an exact version — the supply-chain policy rejects ranges and latest:
chat-adapter-imessage@0.1.1Build guards the typed createChatSdkBridge(...) core call and proves the
dependency is installed (the adapter's top-level import from
chat-adapter-imessage throws if it isn't):
pnpm run buildpnpm exec vitest run src/channels/imessage-registration.test.tsimessage-registration.test.ts imports the real channel barrel and asserts the
registry contains imessage — it goes red if the import line is deleted or
drifts, if the barrel fails to evaluate, or if chat-adapter-imessage isn't
installed (the import throws), so it also covers the dependency from step 3.
End-to-end message delivery against a real iMessage account is verified manually once the service is running — see Next Steps.
iMessage runs in one of two modes:
chat.db needs Full Disk Access granted to the
Node binary the host runs under.Mode choice and the Full Disk Access / Photon walkthroughs are human and interactive. Pick the mode first (local is the macOS default; remote is the only option off macOS), then walk only that mode's setup — the other mode's steps are skipped:
How should iMessage run — `local` (this Mac, needs Full Disk Access) or `remote` (a Photon server)?Requirements: macOS, with Full Disk Access granted to the Node binary. Without
it the adapter can't read chat.db and inbound messages never arrive.
Local mode only works on a Mac — it reads this machine's iMessage chat.db
directly, and there is no such database off macOS. On any other OS, stop here and
use remote (Photon) mode instead; otherwise you'd write a local config that can
never receive a message:
[ "$(uname)" = Darwin ]The Node binary path is buried deep (e.g. ~/.nvm/versions/node/v22.x.x/bin/node),
so open its folder in Finder to make the drag-and-drop target obvious. Harmless
off a desktop (SSH/headless) — it just no-ops:
open "$(dirname "$(which node)")" 2>/dev/null || trueThen tell the user:
Grant Full Disk Access to Node so iMessage can read your chat history:
1. Open System Settings > Privacy & Security > Full Disk Access.
2. Click +, then drag the "node" file from the Finder window that just opened.
3. Toggle it on, then come back here.Stop and wait for the user to confirm Full Disk Access is granted before continuing.
Photon is a separate service that owns an iMessage account and exposes it over HTTP; NanoClaw talks to it via its API. Tell the user:
Set up remote iMessage via Photon:
1. Create a Photon server: https://photon.codes
2. Copy the server URL and API key from your Photon dashboard.Then collect the two values:
Your Photon server URL — starts with http:// or https:// (e.g. https://photon.example.com).Your Photon API key — from the Photon dashboard.The two modes use different .env keys. Write only the keys for the chosen
mode, and strip the opposite mode's keys so a stale value can't confuse the
adapter's factory. The configure script owns this upsert-and-remove (a plain
set-if-absent env write can neither replace a stale value nor delete a key):
Local mode — writes IMESSAGE_LOCAL=true and IMESSAGE_ENABLED=true, and
removes IMESSAGE_SERVER_URL / IMESSAGE_API_KEY if present:
bash setup/channels/imessage-configure.sh localRemote mode — writes IMESSAGE_LOCAL=false, IMESSAGE_SERVER_URL, and
IMESSAGE_API_KEY, and removes IMESSAGE_ENABLED if present:
bash setup/channels/imessage-configure.sh remote "{{server_url}}" "{{api_key}}"Restart the service so it loads the iMessage adapter and the credentials you just stored, and wait for its CLI socket before wiring:
bash setup/lib/restart.shThe agent greets you in the iMessage conversation tied to the phone number or Apple ID email you message from — that handle is both your identity and the conversation address. Resolve it so the owner-wiring step can target it.
The phone number or email you iMessage from — a +E.164 number (e.g. +14155551234) or an email / Apple ID (e.g. you@icloud.com).iMessage is a native adapter: it sends the raw handle as the conversation address, with no channel prefix — so the messaging-group platform id is that handle as-is.
echo "{{owner_handle}}"owner_handle and platform_id are what the owner-wiring step needs. The
welcome iMessage goes out through the adapter once the service is running — in
local mode that needs Full Disk Access granted (above); in remote mode it goes
via your Photon server.
If you're in the middle of /setup, return to the setup flow now. Otherwise wire
this channel with /init-first-agent (or /manage-channels).
imessage+15551234567) or email address. For group chats, the ID is assigned by iMessage internally.The mode answer is rejected. It must be exactly local or remote, lowercase. Local only exists on macOS — it reads this Mac's chat.db directly — so on any other OS the platform check stops you and remote (Photon) is the only path.
Local mode: outgoing works but nothing ever arrives. Full Disk Access wasn't granted to the actual Node binary the service runs under — with nvm the path changes per Node version (~/.nvm/versions/node/v22.x.x/bin/node), so an old grant silently stops covering a new binary. Re-open System Settings → Privacy & Security → Full Disk Access, add the binary at $(which node), then restart the service.
Remote mode: Photon values rejected or unreachable. The server URL must start with http:// or https:// — copy it and the API key from your Photon dashboard at photon.codes. If the adapter starts but sends fail, curl the server URL from this machine to rule out a network path issue.
Your handle is rejected at the resolve step. It must be a bare +E.164 number (+14155551234 — no spaces, dashes, or parentheses) or an email/Apple ID. Use the exact handle you actually send iMessages from — a number-vs-email mismatch means your messages never map to the wired conversation.
Adapter installed but silent. Run pnpm exec vitest run src/channels/imessage-registration.test.ts — red means the barrel import or the chat-adapter-imessage install drifted, so re-run the Apply steps. If green, restart the service (bash setup/lib/restart.sh) so it loads the adapter and the mode config, then check logs/nanoclaw.error.log.
641963c
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.