diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5c502405a1..4ed5064fc69 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -350,6 +350,10 @@ Docs: https://docs.openclaw.ai
- Reply execution: prefer the active runtime snapshot over stale queued reply config during embedded reply and follow-up execution so SecretRef-backed reply turns stop crashing after secrets have already resolved. (#62693) Thanks @mbelinky.
- Android/manual connect: allow blank port input only for TLS manual gateway endpoints so standard HTTPS Tailscale hosts default to `443` without silently changing cleartext manual connects. (#63134) Thanks @Tyler-RNG.
- Matrix/agents: hide owner-only `set-profile` from embedded agent channel-action discovery so non-owner runs stop advertising profile updates they cannot execute. (#62662) Thanks @eleqtrizit.
+- iOS/gateway: replace string-matched connection error UI with structured gateway connection problems, preserve actionable pairing/auth failures over later generic disconnect noise, and surface reusable problem banners and details across onboarding, settings, and root status surfaces. (#62650) Thanks @ngutman.
+- Git/env sanitization: block additional Git repository-plumbing env variables such as `GIT_DIR`, `GIT_WORK_TREE`, `GIT_COMMON_DIR`, `GIT_INDEX_FILE`, `GIT_OBJECT_DIRECTORY`, `GIT_ALTERNATE_OBJECT_DIRECTORIES`, and `GIT_NAMESPACE` so host-run Git commands cannot be redirected to attacker-chosen repository state through inherited or request-scoped env. (#62002) Thanks @eleqtrizit.
+- Host exec/env sanitization: block additional request-scoped credential and config-path overrides such as `KUBECONFIG`, cloud credential-path env, `CARGO_HOME`, and `HELM_HOME` so host-run tools can no longer be redirected to attacker-chosen config or state. (#59119) Thanks @eleqtrizit.
+- Matrix/mentions: keep room mention gating strict while accepting visible `@displayName` Matrix URI labels, so `requireMention` works for non-OpenClaw Matrix clients again. (#64796) Thanks @hclsys.
## 2026.4.5
diff --git a/extensions/matrix/src/matrix/monitor/handler.test.ts b/extensions/matrix/src/matrix/monitor/handler.test.ts
index d975081baa7..6c14accf290 100644
--- a/extensions/matrix/src/matrix/monitor/handler.test.ts
+++ b/extensions/matrix/src/matrix/monitor/handler.test.ts
@@ -180,7 +180,7 @@ describe("matrix monitor handler pairing account scope", () => {
await handler("!room:example.org", makeEvent("$event1"));
await handler("!room:example.org", makeEvent("$event2"));
expect(sendMessageMatrixMock).toHaveBeenCalledTimes(1);
- expect(sendMessageMatrixMock.mock.calls[0]?.[1]).toContain(
+ expect(JSON.stringify(sendMessageMatrixMock.mock.calls[0]?.[1] ?? {})).toContain(
"Pairing request is still pending approval.",
);
@@ -468,6 +468,30 @@ describe("matrix monitor handler pairing account scope", () => {
expect(recordInboundSession).toHaveBeenCalled();
});
+ it("processes room messages mentioned via @displayName in Unicode formatted_body", async () => {
+ const recordInboundSession = vi.fn(async () => {});
+ const { handler } = createMatrixHandlerTestHarness({
+ isDirectMessage: false,
+ getMemberDisplayName: async () => "欢欢",
+ recordInboundSession,
+ });
+
+ await handler(
+ "!room:example.org",
+ createMatrixRoomMessageEvent({
+ eventId: "$unicode-display-name-mention",
+ content: {
+ msgtype: "m.text",
+ body: "@欢欢 please reply",
+ formatted_body: '@欢欢 please reply',
+ "m.mentions": { user_ids: ["@bot:example.org"] },
+ },
+ }),
+ );
+
+ expect(recordInboundSession).toHaveBeenCalled();
+ });
+
it("does not fetch self displayName for plain-text room mentions", async () => {
const getMemberDisplayName = vi.fn(async () => "Tom Servo");
const { handler, recordInboundSession } = createMatrixHandlerTestHarness({
diff --git a/extensions/matrix/src/matrix/monitor/mentions.test.ts b/extensions/matrix/src/matrix/monitor/mentions.test.ts
index 5553f131292..14aae68654e 100644
--- a/extensions/matrix/src/matrix/monitor/mentions.test.ts
+++ b/extensions/matrix/src/matrix/monitor/mentions.test.ts
@@ -34,10 +34,7 @@ describe("resolveMentions", () => {
expect(result.hasExplicitMention).toBe(true);
});
- it("detects mention via m.mentions.user_ids even without visible text mention (#64785)", () => {
- // MSC3952: m.mentions.user_ids is the authoritative mention source.
- // Non-OpenClaw Matrix clients (Element, standalone bots) may set
- // m.mentions without including @bot in the visible message body.
+ it("does not trust m.mentions.user_ids without a visible text or formatted mention", () => {
const result = resolveMentions({
content: {
msgtype: "m.text",
@@ -48,8 +45,8 @@ describe("resolveMentions", () => {
text: "please reply",
mentionRegexes,
});
- expect(result.wasMentioned).toBe(true);
- expect(result.hasExplicitMention).toBe(true);
+ expect(result.wasMentioned).toBe(false);
+ expect(result.hasExplicitMention).toBe(false);
});
it("detects room mention via visible @room text", () => {
@@ -212,6 +209,24 @@ describe("resolveMentions", () => {
expect(result.wasMentioned).toBe(true);
});
+ it("detects mention when the visible label is @displayName with Unicode text", () => {
+ const result = resolveMentions({
+ content: {
+ msgtype: "m.text",
+ body: "@欢欢 please reply",
+ formatted_body:
+ '@欢欢 please reply',
+ "m.mentions": { user_ids: ["@huanhuan:localhost"] },
+ },
+ userId: "@huanhuan:localhost",
+ displayName: "欢欢",
+ text: "@欢欢 please reply",
+ mentionRegexes: [],
+ });
+ expect(result.wasMentioned).toBe(true);
+ expect(result.hasExplicitMention).toBe(true);
+ });
+
it("ignores out-of-range hexadecimal HTML entities in visible labels", () => {
expect(() =>
resolveMentions({
diff --git a/extensions/matrix/src/matrix/monitor/mentions.ts b/extensions/matrix/src/matrix/monitor/mentions.ts
index dfba68d9cb0..201bb7094b9 100644
--- a/extensions/matrix/src/matrix/monitor/mentions.ts
+++ b/extensions/matrix/src/matrix/monitor/mentions.ts
@@ -81,6 +81,7 @@ function isVisibleMentionLabel(params: {
localpart ? extractVisibleMentionText(localpart) : null,
localpart ? extractVisibleMentionText(`@${localpart}`) : null,
params.displayName ? extractVisibleMentionText(params.displayName) : null,
+ params.displayName ? extractVisibleMentionText(`@${params.displayName}`) : null,
].filter((value): value is string => Boolean(value));
return candidates.includes(cleaned);
}
@@ -163,13 +164,13 @@ export function resolveMentions(params: {
mentionRegexes: params.mentionRegexes,
})
: false;
- // m.mentions.user_ids is the authoritative mention source per MSC3952.
- // Previously this also required a visible text or formatted_body mention,
- // which caused messages from non-OpenClaw clients that send proper
- // m.mentions metadata without an @-mention in the body to be silently
- // ignored when requireMention was enabled (#64785).
+ // Matrix clients can mention users through m.mentions metadata plus a visible
+ // Matrix URI label in formatted_body. Keep the visible-mention requirement so
+ // hidden metadata-only mentions do not trigger the handler.
const metadataBackedUserMention = Boolean(
- params.userId && mentionedUsers.has(params.userId),
+ params.userId &&
+ mentionedUsers.has(params.userId) &&
+ (mentionedInFormattedBody || textMentioned),
);
const metadataBackedRoomMention = Boolean(mentions?.room) && visibleRoomMention;
const explicitMention =