claude-code - 💡(How to fix) Fix [BUG] imessage: reply loop on self-chats keyed on receive-only aliases

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

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. [email protected], +15551112222, [email protected]).
  • This Mac is the one running Claude Code, but only originates iMessages from a subset of those (e.g. [email protected]).
  • 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.

Error Message

Error Messages/Logs

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

Fix Action

Fix / Workaround

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

Code Example



---

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`)

---

{
  "dmPolicy": "allowlist",
  "allowFrom": ["+15551112222", "[email protected]"],
  "selfHandles": ["+15551112222", "[email protected]"]
}
RAW_BUFFERClick to expand / collapse

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • 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. [email protected], +15551112222, [email protected]).
  • This Mac is the one running Claude Code, but only originates iMessages from a subset of those (e.g. [email protected]).
  • 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. [email protected], +15551112222, [email protected]).
  • This Mac is the one running Claude Code, but only originates iMessages from a subset of those (e.g. [email protected]).
  • 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", "[email protected]"],
  "selfHandles": ["+15551112222", "[email protected]"]
}

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.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING