diff --git a/external_plugins/imessage/README.md b/external_plugins/imessage/README.md index 4c246ec..dcd40e1 100644 --- a/external_plugins/imessage/README.md +++ b/external_plugins/imessage/README.md @@ -62,6 +62,7 @@ Handles are phone numbers (`+15551234567`) or Apple ID emails (`them@icloud.com` | Variable | Default | Effect | | --- | --- | --- | | `IMESSAGE_APPEND_SIGNATURE` | `true` | Appends `\nSent by Claude` to outbound messages. Set to `false` to disable. | +| `IMESSAGE_ALLOW_SMS` | `false` | Accept inbound SMS/RCS in addition to iMessage. **Off by default because SMS sender IDs are spoofable** — a forged SMS from your own number would otherwise bypass access control. Only enable if you understand the risk. | | `IMESSAGE_ACCESS_MODE` | — | Set to `static` to disable runtime pairing and read `access.json` only. | | `IMESSAGE_STATE_DIR` | `~/.claude/channels/imessage` | Override where `access.json` and pairing state live. | diff --git a/external_plugins/imessage/server.ts b/external_plugins/imessage/server.ts index 21f9fc3..795ad2e 100644 --- a/external_plugins/imessage/server.ts +++ b/external_plugins/imessage/server.ts @@ -32,6 +32,10 @@ import { join, basename, sep } from 'path' const STATIC = process.env.IMESSAGE_ACCESS_MODE === 'static' const APPEND_SIGNATURE = process.env.IMESSAGE_APPEND_SIGNATURE !== 'false' +// SMS sender IDs are spoofable; iMessage is Apple-ID-authenticated. Default +// drops SMS/RCS so a forged sender can't reach the gate. Opt in only if you +// understand the risk. +const ALLOW_SMS = process.env.IMESSAGE_ALLOW_SMS === 'true' const SIGNATURE = '\nSent by Claude' const CHAT_DB = join(homedir(), 'Library', 'Messages', 'chat.db') @@ -104,6 +108,7 @@ type Row = { date: number is_from_me: number cache_has_attachments: number + service: string | null handle_id: string | null chat_guid: string chat_style: number | null @@ -113,7 +118,7 @@ const qWatermark = db.query<{ max: number | null }, []>('SELECT MAX(ROWID) AS ma const qPoll = db.query(` SELECT m.ROWID AS rowid, m.guid, m.text, m.attributedBody, m.date, m.is_from_me, - m.cache_has_attachments, h.id AS handle_id, c.guid AS chat_guid, c.style AS chat_style + m.cache_has_attachments, m.service, h.id AS handle_id, c.guid AS chat_guid, c.style AS chat_style FROM message m JOIN chat_message_join cmj ON cmj.message_id = m.ROWID JOIN chat c ON c.ROWID = cmj.chat_id @@ -124,7 +129,7 @@ const qPoll = db.query(` const qHistory = db.query(` SELECT m.ROWID AS rowid, m.guid, m.text, m.attributedBody, m.date, m.is_from_me, - m.cache_has_attachments, h.id AS handle_id, c.guid AS chat_guid, c.style AS chat_style + m.cache_has_attachments, m.service, h.id AS handle_id, c.guid AS chat_guid, c.style AS chat_style FROM message m JOIN chat_message_join cmj ON cmj.message_id = m.ROWID JOIN chat c ON c.ROWID = cmj.chat_id @@ -710,6 +715,7 @@ function expandTilde(p: string): string { function handleInbound(r: Row): void { if (!r.chat_guid) return + if (!ALLOW_SMS && r.service !== 'iMessage') return // style 45 = DM, 43 = group. Drop unknowns rather than risk routing a // group message through the DM gate and leaking a pairing code.