[BUG] imessage: reply loop on self-chats keyed on receive-only aliases

Resolved 💬 1 comment Opened May 7, 2026 by mdavistffhrtporg Closed Jun 5, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Summary

The iMessage plugin enters a reply loop when self-chatting from an Apple ID alias that this Mac receives on but never originates from. Claude's reply re-arrives as is_from_me=0 in the same chat, gets delivered as fake inbound, and triggers another reply.

Repro

Setup:

  • Apple ID with multiple receive-at handles (e.g. you@icloud.com, +15551112222, you@altdomain.com).
  • This Mac is the one running Claude Code, but only originates iMessages from a subset of those (e.g. you@icloud.com).
  • Texts are sent from another device using one of the other aliases (e.g. an iPhone composing to +15551112222).

What happens:

  1. The user messages from the iPhone alias to +15551112222.
  2. The Mac receives the inbound, Claude replies via osascript.
  3. Because no is_from_me=1 row in chat.db has ever had account=+15551112222, the +15551112222 alias is not in the SELF set built at boot in server.ts:177-184.
  4. isSelfChat returns false for this chat, so consumeEcho never runs.
  5. Claude's outgoing reply re-appears in chat.db as is_from_me=0 (self-chat copy) and is delivered as a new inbound message.
  6. Claude responds to its own message → loop.

Root cause

The SELF set is built only from message.account of is_from_me=1 rows. That misses aliases the user owns but only receives on (an extra Apple ID receive-at email handled by another device, a phone-only number on a multi-eSIM iPhone, etc.).

What Should Happen?

Claude responds to its own message → loop.

Error Messages/Logs

Steps to Reproduce

Repro

Setup:

  • Apple ID with multiple receive-at handles (e.g. you@icloud.com, +15551112222, you@altdomain.com).
  • This Mac is the one running Claude Code, but only originates iMessages from a subset of those (e.g. you@icloud.com).
  • Texts are sent from another device using one of the other aliases (e.g. an iPhone composing to +15551112222).

What happens:

  1. The user messages from the iPhone alias to +15551112222.
  2. The Mac receives the inbound, Claude replies via osascript.
  3. Because no is_from_me=1 row in chat.db has ever had account=+15551112222, the +15551112222 alias is not in the SELF set built at boot in server.ts:177-184.
  4. isSelfChat returns false for this chat, so consumeEcho never runs.
  5. Claude's outgoing reply re-appears in chat.db as is_from_me=0 (self-chat copy) and is delivered as a new inbound message.
  6. Claude responds to its own message → loop.

Claude Model

Not sure / Multiple models

Is this a regression?

No, this never worked

Last Working Version

_No response_

Claude Code Version

2.1.132 (Claude Code)

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

Root cause

The SELF set is built only from message.account of is_from_me=1 rows. That misses aliases the user owns but only receives on (an extra Apple ID receive-at email handled by another device, a phone-only number on a multi-eSIM iPhone, etc.).

Proposed fix

Allow access.json to declare additional self handles via a selfHandles string array. The server reads it once at boot and merges into SELF alongside the database-derived addresses. Fully backward-compatible: the field is optional and ignored when absent.

 const SELF = new Set<string>()
 {
   type R = { addr: string }
   const norm = (s: string) => (/^[A-Za-z]:/.test(s) ? s.slice(2) : s).toLowerCase()
   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))
+  // Augment with handles declared in access.json's selfHandles. The
+  // is_from_me=1 scan only finds aliases this Mac has actually sent from; an
+  // alias the user receives on but never originates from on this Mac (e.g. a
+  // phone-only number on a multi-eSIM iPhone, or a receive-at email handled
+  // by another device) won't appear. Without it, self-chats keyed on that
+  // alias miss echo filtering, and Claude's own osascript reply re-arrives
+  // as is_from_me=0 in the same chat and gets delivered as fake inbound —
+  // a reply loop.
+  try {
+    const parsed = JSON.parse(readFileSync(ACCESS_FILE, 'utf8')) as { selfHandles?: unknown }
+    if (Array.isArray(parsed.selfHandles)) {
+      for (const h of parsed.selfHandles) if (typeof h === 'string') SELF.add(norm(h))
+    }
+  } catch {}
 }
 process.stderr.write(`imessage channel: self-chat addresses: ${[...SELF].join(', ') || '(none)'}\n`)

Example user access.json:

{
  "dmPolicy": "allowlist",
  "allowFrom": ["+15551112222", "you@altdomain.com"],
  "selfHandles": ["+15551112222", "you@altdomain.com"]
}

Notes

  • I have this patch running locally and it cleanly resolves the loop without affecting any other behavior.
  • Type-checks clean against @types/bun.
  • Happy to open a PR if external bug-fix PRs become accepted; the auto-close workflow currently rejects them and the plugin-directory submission form is for new plugins only.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗