Add imessage channel plugin

iMessage bridge for Claude Code. Reads ~/Library/Messages/chat.db
directly for history and new-message polling; sends via AppleScript
to Messages.app. macOS only.

Built-in access control: inbound messages are gated by an allowlist
(default: self-chat only), outbound sends are scoped to the same
allowlist. The /imessage:access skill manages allowlists and policy.

Requires Full Disk Access and Automation TCC grants — both prompted
by macOS on first use.

Ships full source — server.ts runs locally via bun, started by the
.mcp.json command.
This commit is contained in:
Kenneth Lien
2026-03-18 16:23:29 -07:00
parent 6b70f99f76
commit 1c95fc662b
11 changed files with 1588 additions and 0 deletions

View File

@@ -0,0 +1,142 @@
# iMessage — Access & Delivery
This channel reads your Messages database (`~/Library/Messages/chat.db`) directly. Every text to this Mac — from any contact, in any chat — reaches the gate. Access control selects which conversations the assistant should see.
Texting yourself always works. **Self-chat bypasses the gate** with no setup: the server learns your own addresses at boot and lets them through unconditionally. For other senders, the default policy is **`allowlist`**: nothing passes until you add the handle with `/imessage:access allow <address>`.
All state lives in `~/.claude/channels/imessage/access.json`. The `/imessage:access` skill commands edit this file; the server re-reads it on every inbound message, so changes take effect without a restart. Set `IMESSAGE_ACCESS_MODE=static` to pin config to what was on disk at boot.
## At a glance
| | |
| --- | --- |
| Default policy | `allowlist` |
| Self-chat | Bypasses the gate; no config needed |
| Sender ID | Handle address: `+15551234567` or `someone@icloud.com` |
| Group key | Chat GUID: `iMessage;+;chat…` |
| Mention quirk | Regex only; iMessage has no structured @mentions |
| Config file | `~/.claude/channels/imessage/access.json` |
## Self-chat
Open Messages on any device signed into your Apple ID, start a conversation with yourself, and text. It reaches the assistant.
The server identifies your addresses at boot by reading `message.account` and `chat.last_addressed_handle` from `chat.db`. Messages from those addresses skip the gate entirely. To distinguish your input from its own replies — both appear in `chat.db` as from-me — it maintains a 15-second window of recently sent text and matches against it.
## DM policies
`dmPolicy` controls how texts from senders other than you, not on the allowlist, are handled.
| Policy | Behavior |
| --- | --- |
| `allowlist` (default) | Drop silently. Safe default for a personal account. |
| `pairing` | Reply with a pairing code, drop the message. Every contact who texts this Mac will receive one; only use this if very few people have the number. |
| `disabled` | Drop everything except self-chat, which always bypasses. |
```
/imessage:access policy pairing
```
## Handle addresses
iMessage identifies senders by **handle addresses**: either a phone number in `+country` format or the Apple ID email. The form matches what appears at the top of the conversation in Messages.app.
| Contact shown as | Handle address |
| --- | --- |
| Phone number | `+15551234567` (keep the `+`, no spaces or dashes) |
| Email | `someone@icloud.com` |
If the exact form is unclear, check the `chat_messages` tool output or (under `pairing` policy) the pending entry in `access.json`.
```
/imessage:access allow +15551234567
/imessage:access allow friend@icloud.com
/imessage:access remove +15551234567
```
## Groups
Groups are off by default. Opt each one in individually, keyed on the chat GUID.
Chat GUIDs look like `iMessage;+;chat123456789012345678`. They're not exposed in Messages.app; get them from the `chat_id` field in `chat_messages` tool output or from the server's stderr log when it drops a group message.
```
/imessage:access group add "iMessage;+;chat123456789012345678"
```
Quote the GUID; the semicolons are shell metacharacters.
iMessage has **no structured @mentions**. The `@Name` highlight in group chats is presentational styling — nothing in `chat.db` marks it as a mention. With the default `requireMention: true`, the only trigger is a `mentionPatterns` regex match. Set at least one pattern before opting a group in, or no message will ever match.
```
/imessage:access set mentionPatterns '["^claude\\b", "@assistant"]'
```
Pass `--no-mention` to process every message in the group, or `--allow addr1,addr2` to restrict which members can trigger it.
```
/imessage:access group add "iMessage;+;chat123456789012345678" --no-mention
/imessage:access group add "iMessage;+;chat123456789012345678" --allow +15551234567,friend@icloud.com
/imessage:access group rm "iMessage;+;chat123456789012345678"
```
## Delivery
AppleScript can send messages but cannot tapback, edit, or thread-reply; those require private API. Delivery config is correspondingly limited. Set with `/imessage:access set <key> <value>`.
**`textChunkLimit`** sets the split threshold. iMessage has no length cap; chunking is for readability. Defaults to 10000.
**`chunkMode`** chooses the split strategy: `length` cuts exactly at the limit; `newline` prefers paragraph boundaries.
There is no `ackReaction` or `replyToMode` on this channel.
## Skill reference
| Command | Effect |
| --- | --- |
| `/imessage:access` | Print current state: policy, allowlist, pending pairings, enabled groups. |
| `/imessage:access pair a4f91c` | Approve a pending code (relevant only under `pairing` policy). |
| `/imessage:access deny a4f91c` | Discard a pending code. |
| `/imessage:access allow +15551234567` | Add a handle. The primary entry point under the default `allowlist` policy. |
| `/imessage:access remove +15551234567` | Remove from the allowlist. |
| `/imessage:access policy pairing` | Set `dmPolicy`. Values: `pairing`, `allowlist`, `disabled`. |
| `/imessage:access group add "iMessage;+;chat…"` | Enable a group. Quote the GUID. Flags: `--no-mention`, `--allow a,b`. |
| `/imessage:access group rm "iMessage;+;chat…"` | Disable a group. |
| `/imessage:access set textChunkLimit 5000` | Set a config key: `textChunkLimit`, `chunkMode`, `mentionPatterns`. |
## Config file
`~/.claude/channels/imessage/access.json`. Absent file is equivalent to `allowlist` policy with empty lists: only self-chat passes.
```jsonc
{
// Handling for texts from senders not in allowFrom.
// Defaults to allowlist since this reads your personal chat.db.
// Self-chat bypasses regardless.
"dmPolicy": "allowlist",
// Handle addresses allowed to reach the assistant.
"allowFrom": ["+15551234567", "friend@icloud.com"],
// Group chats the assistant participates in. Empty object = DM-only.
"groups": {
"iMessage;+;chat123456789012345678": {
// true: respond only on mentionPatterns match.
// iMessage has no structured @mentions; regex is the only trigger.
"requireMention": true,
// Restrict triggers to these senders. Empty = any member (subject to requireMention).
"allowFrom": []
}
},
// Case-insensitive regexes that count as a mention.
// Required for groups with requireMention, since there are no structured mentions.
"mentionPatterns": ["^claude\\b", "@assistant"],
// Split threshold. No length cap; this is about readability.
"textChunkLimit": 10000,
// length = cut at limit. newline = prefer paragraph boundaries.
"chunkMode": "newline"
}
```