Compare commits

..

6 Commits

Author SHA1 Message Date
Kenneth Lien
22bd61d01f imessage: bump to 0.1.0 2026-03-26 23:43:51 -07:00
Kenneth Lien
60c3fc36ed imessage: drop SMS/RCS by default, opt-in via IMESSAGE_ALLOW_SMS
SMS sender IDs are spoofable; iMessage is Apple-ID-authenticated and
end-to-end encrypted. The plugin previously treated both identically,
so a forged SMS from the owner's own number would match SELF, bypass
the access gate, and inherit owner-level trust — including permission
approval.

handleInbound now drops anything with service != 'iMessage' unless
IMESSAGE_ALLOW_SMS=true. Default is the safe path; users who want SMS
can opt in after reading the warning in README.
2026-03-26 23:41:39 -07:00
Kenneth Lien
c4274521de imessage: trim comment cruft 2026-03-26 23:16:27 -07:00
Kenneth Lien
8dfc279258 imessage: harden echo filter normalization
The self-chat echo filter matches outbound text against what chat.db
stores on round-trip. Three divergence sources caused false negatives
and duplicate bubbles:

- Signature suffix: "\nSent by Claude" is appended on send, but the
  \n may not round-trip identically through attributedBody
- Emoji variation selectors (U+FE00-FE0F) and ZWJ (U+200D): chat.db
  can add or drop these on emoji characters
- Smart quotes: macOS auto-substitutes straight quotes on the way in

Strip/normalize all three in echoKey() before the existing whitespace
collapse.

Fixes #1024
2026-03-26 23:12:19 -07:00
Kenneth Lien
c29338f276 imessage: drop whitespace-only messages from tapbacks/receipts
Tapback reactions and read receipts synced from linked devices arrive
as chat.db rows with whitespace-only text. The existing empty-check
used falsy comparison which doesn't catch ' ' or invisible chars,
causing unsolicited replies to reaction taps.

Fixes #1041
2026-03-26 23:11:49 -07:00
Kenneth Lien
03a685d5f6 imessage: restrict permission relay to self-chat only
Permission prompts were being broadcast to all allowlisted contacts plus
every DM resolvable from the SELF address set. Two compounding bugs:

1. SELF was polluted by chat.last_addressed_handle, which on machines
   with SMS history returns short codes, business handles, and other
   contacts' numbers — not just the owner's addresses. One reporter's
   query returned 50 addresses (2 actually theirs) resolving to 148 DM
   chats, all of which received permission prompts.

2. Even with a clean SELF, the handler sent to allowFrom + SELF, so
   every allowlisted contact received the prompt and could reply to
   approve tool execution on the owner's machine.

Fix:
- Build SELF from message.account WHERE is_from_me=1 only
- Send permission prompts to self-chat only, not allowFrom
- Accept permission replies from self-chat only

Fixes #1048
Fixes #1010
2026-03-26 23:11:29 -07:00
31 changed files with 49 additions and 73 deletions

View File

@@ -1,6 +1,5 @@
{
"name": "asana",
"version": "1.0.0",
"description": "Asana project management integration. Create and manage tasks, search projects, update assignments, track progress, and integrate your development workflow with Asana's work management platform.",
"author": {
"name": "Asana"

View File

@@ -1,6 +1,5 @@
{
"name": "context7",
"version": "1.0.0",
"description": "Upstash Context7 MCP server for up-to-date documentation lookup. Pull version-specific documentation and code examples directly from source repositories into your LLM context.",
"author": {
"name": "Upstash"

View File

@@ -1,6 +1,5 @@
{
"name": "firebase",
"version": "1.0.0",
"description": "Google Firebase MCP integration. Manage Firestore databases, authentication, cloud functions, hosting, and storage. Build and manage your Firebase backend directly from your development workflow.",
"author": {
"name": "Google"

View File

@@ -1,6 +1,5 @@
{
"name": "github",
"version": "1.0.0",
"description": "Official GitHub MCP server for repository management. Create issues, manage pull requests, review code, search repositories, and interact with GitHub's full API directly from Claude Code.",
"author": {
"name": "GitHub"

View File

@@ -1,6 +1,5 @@
{
"name": "gitlab",
"version": "1.0.0",
"description": "GitLab DevOps platform integration. Manage repositories, merge requests, CI/CD pipelines, issues, and wikis. Full access to GitLab's comprehensive DevOps lifecycle tools.",
"author": {
"name": "GitLab"

View File

@@ -1,17 +1,10 @@
{
"name": "greptile",
"version": "1.0.0",
"description": "AI code review agent for GitHub and GitLab. View and resolve Greptile's PR review comments directly from Claude Code.",
"author": {
"name": "Greptile",
"url": "https://greptile.com"
},
"homepage": "https://greptile.com/docs",
"keywords": [
"code-review",
"pull-requests",
"github",
"gitlab",
"ai"
]
"keywords": ["code-review", "pull-requests", "github", "gitlab", "ai"]
}

View File

@@ -1,7 +1,7 @@
{
"name": "imessage",
"description": "iMessage channel for Claude Code \u2014 reads chat.db directly, sends via AppleScript. Built-in access control; manage pairing, allowlists, and policy via /imessage:access.",
"version": "0.0.1",
"version": "0.1.0",
"keywords": [
"imessage",
"messaging",

View File

@@ -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. |

View File

@@ -1,6 +1,6 @@
{
"name": "claude-channel-imessage",
"version": "0.0.1",
"version": "0.1.0",
"license": "Apache-2.0",
"type": "module",
"bin": "./server.ts",

View File

@@ -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<Row, [number]>(`
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<Row, [number]>(`
const qHistory = db.query<Row, [string, number]>(`
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
@@ -149,12 +154,10 @@ const qAttachments = db.query<AttRow, [number]>(`
WHERE maj.message_id = ?
`)
// Your own addresses. message.account ("E:you@icloud.com" / "p:+1555...") is
// the identity you sent *from* on each row — but an Apple ID can be reachable
// at both an email and a phone, and account only shows whichever you sent
// from. chat.last_addressed_handle covers the rest: it's the per-chat "which
// of your addresses reaches this person" field, so it accumulates every
// identity you've actually used. Union both.
// Your own addresses, from message.account ("E:you@icloud.com" / "p:+1555...")
// on rows you sent. Don't supplement with chat.last_addressed_handle — on
// machines with SMS history that column is polluted with short codes and
// other people's numbers, not just your own identities.
const SELF = new Set<string>()
{
type R = { addr: string }
@@ -162,9 +165,6 @@ const SELF = new Set<string>()
for (const { addr } of db.query<R, []>(
`SELECT DISTINCT account AS addr FROM message WHERE is_from_me = 1 AND account IS NOT NULL AND account != '' LIMIT 50`,
).all()) SELF.add(norm(addr))
for (const { addr } of db.query<R, []>(
`SELECT DISTINCT last_addressed_handle AS addr FROM chat WHERE last_addressed_handle IS NOT NULL AND last_addressed_handle != '' LIMIT 50`,
).all()) SELF.add(norm(addr))
}
process.stderr.write(`imessage channel: self-chat addresses: ${[...SELF].join(', ') || '(none)'}\n`)
@@ -416,7 +416,14 @@ const ECHO_WINDOW_MS = 15000
const echo = new Map<string, number>()
function echoKey(raw: string): string {
return raw.trim().replace(/\s+/g, ' ').slice(0, 120)
return raw
.replace(/\s*Sent by Claude\s*$/, '')
.replace(/[\u200d\ufe00-\ufe0f]/g, '') // ZWJ + variation selectors — chat.db is inconsistent about these
.replace(/[\u2018\u2019]/g, "'")
.replace(/[\u201c\u201d]/g, '"')
.trim()
.replace(/\s+/g, ' ')
.slice(0, 120)
}
function trackEcho(chatGuid: string, key: string): void {
@@ -496,11 +503,10 @@ const mcp = new Server(
tools: {},
experimental: {
'claude/channel': {},
// Permission-relay opt-in (anthropics/claude-cli-internal#23061).
// Declaring this asserts we authenticate the replier — which we do:
// gate()/access.allowFrom already drops non-allowlisted senders before
// handleInbound delivers. Self-chat is the owner by definition. A
// server that can't authenticate the replier should NOT declare this.
// Permission-relay opt-in. Declaring this asserts we authenticate the
// replier — which we do: prompts go to self-chat only and replies are
// accepted from self-chat only (see handleInbound). A server that
// can't authenticate the replier should NOT declare this.
'claude/channel/permission': {},
},
},
@@ -518,11 +524,9 @@ const mcp = new Server(
},
)
// Receive permission_request from CC → format → send to all allowlisted DMs.
// Groups are intentionally excluded — the security thread resolution was
// "single-user mode for official plugins." Anyone in access.allowFrom
// already passed explicit pairing; group members haven't. Self-chat is
// always included (owner).
// Permission prompts go to self-chat only. A "yes" grants tool execution on
// this machine — that authority is the owner's alone, not allowlisted
// contacts'.
mcp.setNotificationHandler(
z.object({
method: z.literal('notifications/claude/channel/permission_request'),
@@ -535,7 +539,6 @@ mcp.setNotificationHandler(
}),
async ({ params }) => {
const { request_id, tool_name, description, input_preview } = params
const access = loadAccess()
// input_preview is unbearably long for Write/Edit; show only for Bash
// where the command itself is the dangerous part.
const preview = tool_name === 'Bash' ? `${input_preview}\n\n` : '\n'
@@ -544,14 +547,17 @@ mcp.setNotificationHandler(
`${tool_name}: ${description}\n` +
preview +
`Reply "yes ${request_id}" to allow or "no ${request_id}" to deny.`
// allowFrom holds handle IDs, not chat GUIDs — resolve via qChatsForHandle.
// Include SELF addresses so the owner's self-chat gets the prompt even
// when allowFrom is empty (default config).
const handles = new Set([...access.allowFrom.map(h => h.toLowerCase()), ...SELF])
const targets = new Set<string>()
for (const h of handles) {
for (const h of SELF) {
for (const { guid } of qChatsForHandle.all(h)) targets.add(guid)
}
if (targets.size === 0) {
process.stderr.write(
`imessage channel: permission_request ${request_id} not relayed — no self-chat found. ` +
`Send yourself an iMessage to create one.\n`,
)
return
}
for (const guid of targets) {
const err = sendText(guid, text)
if (err) {
@@ -709,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.
@@ -720,7 +727,9 @@ function handleInbound(r: Row): void {
const text = messageText(r)
const hasAttachments = r.cache_has_attachments === 1
if (!text && !hasAttachments) return
// trim() catches tapbacks/receipts synced from other devices — those land
// as whitespace-only rows.
if (!text.trim() && !hasAttachments) return
// Never deliver our own sends. In self-chat the is_from_me=1 rows are empty
// sent-receipts anyway — the content lands on the is_from_me=0 copy below.
@@ -756,12 +765,9 @@ function handleInbound(r: Row): void {
}
}
// Permission-reply intercept: if this looks like "yes xxxxx" for a
// pending permission request, emit the structured event instead of
// relaying as chat. The sender is already gate()-approved at this point
// (non-allowlisted senders were dropped above; self-chat is the owner),
// so we trust the reply.
const permMatch = PERMISSION_REPLY_RE.exec(text)
// Permission replies: emit the structured event instead of relaying as
// chat. Owner-only — same gate as the send side.
const permMatch = isSelfChat ? PERMISSION_REPLY_RE.exec(text) : null
if (permMatch) {
void mcp.notification({
method: 'notifications/claude/channel/permission',

View File

@@ -1,6 +1,5 @@
{
"name": "laravel-boost",
"version": "1.0.0",
"description": "Laravel development toolkit MCP server. Provides intelligent assistance for Laravel applications including Artisan commands, Eloquent queries, routing, migrations, and framework-specific code generation.",
"author": {
"name": "Laravel"

View File

@@ -1,6 +1,5 @@
{
"name": "linear",
"version": "1.0.0",
"description": "Linear issue tracking integration. Create issues, manage projects, update statuses, search across workspaces, and streamline your software development workflow with Linear's modern issue tracker.",
"author": {
"name": "Linear"

View File

@@ -1,6 +1,5 @@
{
"name": "playwright",
"version": "1.0.0",
"description": "Browser automation and end-to-end testing MCP server by Microsoft. Enables Claude to interact with web pages, take screenshots, fill forms, click elements, and perform automated browser testing workflows.",
"author": {
"name": "Microsoft"

View File

@@ -1,6 +1,5 @@
{
"name": "serena",
"version": "1.0.0",
"description": "Semantic code analysis MCP server providing intelligent code understanding, refactoring suggestions, and codebase navigation through language server protocol integration.",
"author": {
"name": "Oraios"

View File

@@ -1,6 +1,5 @@
{
"name": "supabase",
"version": "1.0.0",
"description": "Supabase MCP integration for database operations, authentication, storage, and real-time subscriptions. Manage your Supabase projects, run SQL queries, and interact with your backend directly.",
"author": {
"name": "Supabase"

View File

@@ -1,6 +1,5 @@
{
"name": "terraform",
"version": "1.0.0",
"description": "The Terraform MCP Server provides seamless integration with Terraform ecosystem, enabling advanced automation and interaction capabilities for Infrastructure as Code (IaC) development.",
"author": {
"name": "HashiCorp"

View File

@@ -1,6 +1,5 @@
{
"name": "agent-sdk-dev",
"version": "1.0.0",
"description": "Claude Agent SDK Development Plugin",
"author": {
"name": "Anthropic",

View File

@@ -1,9 +1,9 @@
{
"name": "code-review",
"version": "1.0.0",
"description": "Automated code review for pull requests using multiple specialized agents with confidence-based scoring",
"author": {
"name": "Anthropic",
"email": "support@anthropic.com"
}
}

View File

@@ -1,9 +1,9 @@
{
"name": "commit-commands",
"version": "1.0.0",
"description": "Streamline your git workflow with simple commands for committing, pushing, and creating pull requests",
"author": {
"name": "Anthropic",
"email": "support@anthropic.com"
}
}

View File

@@ -1,6 +1,5 @@
{
"name": "explanatory-output-style",
"version": "1.0.0",
"description": "Adds educational insights about implementation choices and codebase patterns (mimics the deprecated Explanatory output style)",
"author": {
"name": "Anthropic",

View File

@@ -1,6 +1,5 @@
{
"name": "feature-dev",
"version": "1.0.0",
"description": "Comprehensive feature development workflow with specialized agents for codebase exploration, architecture design, and quality review",
"author": {
"name": "Anthropic",

View File

@@ -1,6 +1,5 @@
{
"name": "frontend-design",
"version": "1.0.0",
"description": "Frontend design skill for UI/UX implementation",
"author": {
"name": "Anthropic",

View File

@@ -1,6 +1,5 @@
{
"name": "hookify",
"version": "1.0.0",
"description": "Easily create hooks to prevent unwanted behaviors by analyzing conversation patterns",
"author": {
"name": "Anthropic",

View File

@@ -1,6 +1,5 @@
{
"name": "learning-output-style",
"version": "1.0.0",
"description": "Interactive learning mode that requests meaningful code contributions at decision points (mimics the unshipped Learning output style)",
"author": {
"name": "Anthropic",

View File

@@ -1,7 +1,6 @@
{
"name": "mcp-server-dev",
"version": "1.0.0",
"description": "Skills for designing and building MCP servers that work seamlessly with Claude \u2014 guides you through deployment models (remote HTTP, MCPB, local), tool design patterns, auth, and interactive MCP apps.",
"description": "Skills for designing and building MCP servers that work seamlessly with Claude — guides you through deployment models (remote HTTP, MCPB, local), tool design patterns, auth, and interactive MCP apps.",
"author": {
"name": "Anthropic",
"email": "support@anthropic.com"

View File

@@ -1,7 +1,6 @@
{
"name": "playground",
"version": "1.0.0",
"description": "Creates interactive HTML playgrounds \u2014 self-contained single-file explorers with visual controls, live preview, and prompt output with copy button",
"description": "Creates interactive HTML playgrounds — self-contained single-file explorers with visual controls, live preview, and prompt output with copy button",
"author": {
"name": "Anthropic",
"email": "support@anthropic.com"

View File

@@ -1,6 +1,5 @@
{
"name": "plugin-dev",
"version": "1.0.0",
"description": "Plugin development toolkit with skills for creating agents, commands, hooks, MCP integrations, and comprehensive plugin structure guidance",
"author": {
"name": "Anthropic",

View File

@@ -1,6 +1,5 @@
{
"name": "pr-review-toolkit",
"version": "1.0.0",
"description": "Comprehensive PR review agents specializing in comments, tests, error handling, type design, code quality, and code simplification",
"author": {
"name": "Anthropic",

View File

@@ -1,6 +1,5 @@
{
"name": "ralph-loop",
"version": "1.0.0",
"description": "Continuous self-referential AI loops for interactive iterative development, implementing the Ralph Wiggum technique. Run Claude in a while-true loop with the same prompt until task completion.",
"author": {
"name": "Anthropic",

View File

@@ -1,6 +1,5 @@
{
"name": "security-guidance",
"version": "1.0.0",
"description": "Security reminder hook that warns about potential security issues when editing files, including command injection, XSS, and unsafe code patterns",
"author": {
"name": "Anthropic",

View File

@@ -1,6 +1,5 @@
{
"name": "skill-creator",
"version": "1.0.0",
"description": "Create new skills, improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, update or optimize an existing skill, run evals to test a skill, or benchmark skill performance with variance analysis.",
"author": {
"name": "Anthropic",