[BUG] imessage: reply loop on self-chats keyed on receive-only aliases
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:
- The user messages from the iPhone alias to
+15551112222. - The Mac receives the inbound, Claude replies via osascript.
- Because no
is_from_me=1row in chat.db has ever hadaccount=+15551112222, the+15551112222alias is not in theSELFset built at boot inserver.ts:177-184. isSelfChatreturns false for this chat, soconsumeEchonever runs.- Claude's outgoing reply re-appears in
chat.dbasis_from_me=0(self-chat copy) and is delivered as a new inbound message. - 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:
- The user messages from the iPhone alias to
+15551112222. - The Mac receives the inbound, Claude replies via osascript.
- Because no
is_from_me=1row in chat.db has ever hadaccount=+15551112222, the+15551112222alias is not in theSELFset built at boot inserver.ts:177-184. isSelfChatreturns false for this chat, soconsumeEchonever runs.- Claude's outgoing reply re-appears in
chat.dbasis_from_me=0(self-chat copy) and is delivered as a new inbound message. - 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.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗