diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2bc3ebae3..22ce8f108 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,6 +38,7 @@ Docs: https://docs.clawd.bot
- Matrix: decrypt E2EE media attachments with preflight size guard. (#1744) Thanks @araa47.
- BlueBubbles: route phone-number targets to DMs, avoid leaking routing IDs, and auto-create missing DMs (Private API required). (#1751) Thanks @tyler6204. https://docs.clawd.bot/channels/bluebubbles
- BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing.
+- iMessage: normalize chat_id/chat_guid/chat_identifier prefixes case-insensitively and keep service-prefixed handles stable. (#1708) Thanks @aaronn.
- Signal: repair reaction sends (group/UUID targets + CLI author flags). (#1651) Thanks @vilkasdev.
- Signal: add configurable signal-cli startup timeout + external daemon mode docs. (#1677) https://docs.clawd.bot/channels/signal
- Telegram: set fetch duplex="half" for uploads on Node 22 to avoid sendPhoto failures. (#1684) Thanks @commdata2338.
diff --git a/README.md b/README.md
index 1329c5e2b..ebbdc43d5 100644
--- a/README.md
+++ b/README.md
@@ -477,32 +477,33 @@ Special thanks to [Mario Zechner](https://mariozechner.at/) for his support and
Thanks to all clawtributors:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/extensions/imessage/src/channel.ts b/extensions/imessage/src/channel.ts
index 50615cd22..556c2970a 100644
--- a/extensions/imessage/src/channel.ts
+++ b/extensions/imessage/src/channel.ts
@@ -8,8 +8,10 @@ import {
imessageOnboardingAdapter,
IMessageConfigSchema,
listIMessageAccountIds,
+ looksLikeIMessageTargetId,
migrateBaseNameToDefaultAccount,
normalizeAccountId,
+ normalizeIMessageMessagingTarget,
PAIRING_APPROVED_MESSAGE,
resolveChannelMediaMaxBytes,
resolveDefaultIMessageAccountId,
@@ -110,14 +112,9 @@ export const imessagePlugin: ChannelPlugin = {
resolveToolPolicy: resolveIMessageGroupToolPolicy,
},
messaging: {
+ normalizeTarget: normalizeIMessageMessagingTarget,
targetResolver: {
- looksLikeId: (raw) => {
- const trimmed = raw.trim();
- if (!trimmed) return false;
- if (/^(imessage:|chat_id:)/i.test(trimmed)) return true;
- if (trimmed.includes("@")) return true;
- return /^\+?\d{3,}$/.test(trimmed);
- },
+ looksLikeId: looksLikeIMessageTargetId,
hint: "",
},
},
diff --git a/scripts/clawtributors-map.json b/scripts/clawtributors-map.json
index 7ad1f926c..8899afc93 100644
--- a/scripts/clawtributors-map.json
+++ b/scripts/clawtributors-map.json
@@ -2,6 +2,7 @@
"ensureLogins": [
"odrobnik",
"alphonse-arianee",
+ "aaronn",
"ronak-guliani",
"cpojer",
"carlulsoe",
diff --git a/src/channels/plugins/normalize/imessage.test.ts b/src/channels/plugins/normalize/imessage.test.ts
new file mode 100644
index 000000000..afb2ec358
--- /dev/null
+++ b/src/channels/plugins/normalize/imessage.test.ts
@@ -0,0 +1,15 @@
+import { describe, expect, it } from "vitest";
+
+import { normalizeIMessageMessagingTarget } from "./imessage.js";
+
+describe("imessage target normalization", () => {
+ it("preserves service prefixes for handles", () => {
+ expect(normalizeIMessageMessagingTarget("sms:+1 (555) 222-3333")).toBe("sms:+15552223333");
+ });
+
+ it("drops service prefixes for chat targets", () => {
+ expect(normalizeIMessageMessagingTarget("sms:chat_id:123")).toBe("chat_id:123");
+ expect(normalizeIMessageMessagingTarget("imessage:CHAT_GUID:abc")).toBe("chat_guid:abc");
+ expect(normalizeIMessageMessagingTarget("auto:ChatIdentifier:foo")).toBe("chat_identifier:foo");
+ });
+});
diff --git a/src/channels/plugins/normalize/imessage.ts b/src/channels/plugins/normalize/imessage.ts
new file mode 100644
index 000000000..ec04d6557
--- /dev/null
+++ b/src/channels/plugins/normalize/imessage.ts
@@ -0,0 +1,35 @@
+import { normalizeIMessageHandle } from "../../../imessage/targets.js";
+
+// Service prefixes that indicate explicit delivery method; must be preserved during normalization
+const SERVICE_PREFIXES = ["imessage:", "sms:", "auto:"] as const;
+const CHAT_TARGET_PREFIX_RE =
+ /^(chat_id:|chatid:|chat:|chat_guid:|chatguid:|guid:|chat_identifier:|chatidentifier:|chatident:)/i;
+
+export function normalizeIMessageMessagingTarget(raw: string): string | undefined {
+ const trimmed = raw.trim();
+ if (!trimmed) return undefined;
+
+ // Preserve service prefix if present (e.g., "sms:+1555" → "sms:+15551234567")
+ const lower = trimmed.toLowerCase();
+ for (const prefix of SERVICE_PREFIXES) {
+ if (lower.startsWith(prefix)) {
+ const remainder = trimmed.slice(prefix.length).trim();
+ const normalizedHandle = normalizeIMessageHandle(remainder);
+ if (!normalizedHandle) return undefined;
+ if (CHAT_TARGET_PREFIX_RE.test(normalizedHandle)) return normalizedHandle;
+ return `${prefix}${normalizedHandle}`;
+ }
+ }
+
+ const normalized = normalizeIMessageHandle(trimmed);
+ return normalized || undefined;
+}
+
+export function looksLikeIMessageTargetId(raw: string): boolean {
+ const trimmed = raw.trim();
+ if (!trimmed) return false;
+ if (/^(imessage:|sms:|auto:)/i.test(trimmed)) return true;
+ if (CHAT_TARGET_PREFIX_RE.test(trimmed)) return true;
+ if (trimmed.includes("@")) return true;
+ return /^\+?\d{3,}$/.test(trimmed);
+}
diff --git a/src/imessage/targets.test.ts b/src/imessage/targets.test.ts
index 956dfa321..6350167a3 100644
--- a/src/imessage/targets.test.ts
+++ b/src/imessage/targets.test.ts
@@ -28,6 +28,27 @@ describe("imessage targets", () => {
expect(normalizeIMessageHandle(" +1 (555) 222-3333 ")).toBe("+15552223333");
});
+ it("normalizes chat_id prefixes case-insensitively", () => {
+ expect(normalizeIMessageHandle("CHAT_ID:123")).toBe("chat_id:123");
+ expect(normalizeIMessageHandle("Chat_Id:456")).toBe("chat_id:456");
+ expect(normalizeIMessageHandle("chatid:789")).toBe("chat_id:789");
+ expect(normalizeIMessageHandle("CHAT:42")).toBe("chat_id:42");
+ });
+
+ it("normalizes chat_guid prefixes case-insensitively", () => {
+ expect(normalizeIMessageHandle("CHAT_GUID:abc-def")).toBe("chat_guid:abc-def");
+ expect(normalizeIMessageHandle("ChatGuid:XYZ")).toBe("chat_guid:XYZ");
+ expect(normalizeIMessageHandle("GUID:test-guid")).toBe("chat_guid:test-guid");
+ });
+
+ it("normalizes chat_identifier prefixes case-insensitively", () => {
+ expect(normalizeIMessageHandle("CHAT_IDENTIFIER:iMessage;-;chat123")).toBe(
+ "chat_identifier:iMessage;-;chat123",
+ );
+ expect(normalizeIMessageHandle("ChatIdentifier:test")).toBe("chat_identifier:test");
+ expect(normalizeIMessageHandle("CHATIDENT:foo")).toBe("chat_identifier:foo");
+ });
+
it("checks allowFrom against chat_id", () => {
const ok = isAllowedIMessageSender({
allowFrom: ["chat_id:9"],
diff --git a/src/imessage/targets.ts b/src/imessage/targets.ts
index befb3f6d6..03fdcf306 100644
--- a/src/imessage/targets.ts
+++ b/src/imessage/targets.ts
@@ -34,6 +34,27 @@ export function normalizeIMessageHandle(raw: string): string {
if (lowered.startsWith("imessage:")) return normalizeIMessageHandle(trimmed.slice(9));
if (lowered.startsWith("sms:")) return normalizeIMessageHandle(trimmed.slice(4));
if (lowered.startsWith("auto:")) return normalizeIMessageHandle(trimmed.slice(5));
+
+ // Normalize chat_id/chat_guid/chat_identifier prefixes case-insensitively
+ for (const prefix of CHAT_ID_PREFIXES) {
+ if (lowered.startsWith(prefix)) {
+ const value = trimmed.slice(prefix.length).trim();
+ return `chat_id:${value}`;
+ }
+ }
+ for (const prefix of CHAT_GUID_PREFIXES) {
+ if (lowered.startsWith(prefix)) {
+ const value = trimmed.slice(prefix.length).trim();
+ return `chat_guid:${value}`;
+ }
+ }
+ for (const prefix of CHAT_IDENTIFIER_PREFIXES) {
+ if (lowered.startsWith(prefix)) {
+ const value = trimmed.slice(prefix.length).trim();
+ return `chat_identifier:${value}`;
+ }
+ }
+
if (trimmed.includes("@")) return trimmed.toLowerCase();
const normalized = normalizeE164(trimmed);
if (normalized) return normalized;
diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts
index 3e213746f..60782ff6d 100644
--- a/src/plugin-sdk/index.ts
+++ b/src/plugin-sdk/index.ts
@@ -197,12 +197,6 @@ export {
} from "../channels/plugins/setup-helpers.js";
export { formatPairingApproveHint } from "../channels/plugins/helpers.js";
export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js";
-export {
- listIMessageAccountIds,
- resolveDefaultIMessageAccountId,
- resolveIMessageAccount,
- type ResolvedIMessageAccount,
-} from "../imessage/accounts.js";
export type {
ChannelOnboardingAdapter,
@@ -210,7 +204,6 @@ export type {
} from "../channels/plugins/onboarding-types.js";
export { addWildcardAllowFrom, promptAccountId } from "../channels/plugins/onboarding/helpers.js";
export { promptChannelAccessConfig } from "../channels/plugins/onboarding/channel-access.js";
-export { imessageOnboardingAdapter } from "../channels/plugins/onboarding/imessage.js";
export {
createActionGate,
@@ -264,6 +257,19 @@ export {
} from "../channels/plugins/normalize/discord.js";
export { collectDiscordStatusIssues } from "../channels/plugins/status-issues/discord.js";
+// Channel: iMessage
+export {
+ listIMessageAccountIds,
+ resolveDefaultIMessageAccountId,
+ resolveIMessageAccount,
+ type ResolvedIMessageAccount,
+} from "../imessage/accounts.js";
+export { imessageOnboardingAdapter } from "../channels/plugins/onboarding/imessage.js";
+export {
+ looksLikeIMessageTargetId,
+ normalizeIMessageMessagingTarget,
+} from "../channels/plugins/normalize/imessage.js";
+
// Channel: Slack
export {
listEnabledSlackAccounts,