diff --git a/CHANGELOG.md b/CHANGELOG.md
index b60fc383104..d910007b667 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -58,6 +58,7 @@ Docs: https://docs.openclaw.ai
- Telegram/streaming: sanitize tool-progress draft preview backticks before shared compaction, so long backtick-heavy progress text still renders inside the safe code-formatted preview instead of collapsing to an ellipsis.
- Agents/Pi: suppress persistence for synthetic mid-turn overflow continuation prompts, so transcript-retry recovery does not write the "continue from transcript" prompt as a new user turn. Thanks @vincentkoc.
+- Agents/tools: strip reasoning text from visible rich presentation titles, blocks, buttons, and select labels before message-tool sends, so structured channel payloads cannot leak hidden planning. Thanks @vincentkoc.
- Telegram: keep reply-dispatch lazy provider runtime chunks behind stable dist names and delete `/reasoning stream` previews after final delivery so package updates and live reasoning drafts do not leave Telegram turns broken or noisy. Thanks @BunsDev.
- Exec approvals: detect `env -S` split-string command-carrier risks when `-S`/`-s` is combined with other env short options, so approval explanations do not miss split payloads hidden behind `env -iS...`. Thanks @vincentkoc.
- Google Meet: log the concrete agent-mode TTS provider, model, voice, output format, and sample rate after speech synthesis, so Meet logs show which voice backend spoke each reply.
diff --git a/src/agents/tools/message-tool.test.ts b/src/agents/tools/message-tool.test.ts
index 9c541a769a3..c8945029367 100644
--- a/src/agents/tools/message-tool.test.ts
+++ b/src/agents/tools/message-tool.test.ts
@@ -1135,6 +1135,57 @@ describe("message tool reasoning tag sanitization", () => {
expect(call?.params?.[field]).toBe(expected);
},
);
+
+ it("sanitizes visible presentation text before sending", async () => {
+ mockSendResult({ channel: "slack", to: "slack:C123" });
+
+ const call = await executeSend({
+ action: {
+ target: "slack:C123",
+ presentation: {
+ title: "internal titleDeploy ready",
+ blocks: [
+ { type: "text", text: "internal noteShip it" },
+ {
+ type: "buttons",
+ buttons: [
+ {
+ label: "button rationaleApprove",
+ value: "approve",
+ },
+ ],
+ },
+ {
+ type: "select",
+ placeholder: "selection rationalePick a lane",
+ options: [
+ {
+ label: "option rationaleMain",
+ value: "main",
+ },
+ ],
+ },
+ ],
+ },
+ },
+ });
+
+ expect(call?.params?.presentation).toEqual({
+ title: "Deploy ready",
+ blocks: [
+ { type: "text", text: "Ship it" },
+ {
+ type: "buttons",
+ buttons: [{ label: "Approve", value: "approve" }],
+ },
+ {
+ type: "select",
+ placeholder: "Pick a lane",
+ options: [{ label: "Main", value: "main" }],
+ },
+ ],
+ });
+ });
});
describe("message tool sandbox passthrough", () => {
diff --git a/src/agents/tools/message-tool.ts b/src/agents/tools/message-tool.ts
index c74e6480c98..5d8e72a8671 100644
--- a/src/agents/tools/message-tool.ts
+++ b/src/agents/tools/message-tool.ts
@@ -65,6 +65,55 @@ function stripFormattedReasoningMessage(text: string): string {
return lines.slice(index).join("\n").trim();
}
+function sanitizePresentationTextFields(value: unknown): unknown {
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
+ return value;
+ }
+ const presentation = { ...(value as Record) };
+ if (typeof presentation.title === "string") {
+ presentation.title = stripFormattedReasoningMessage(presentation.title);
+ }
+ if (Array.isArray(presentation.blocks)) {
+ presentation.blocks = presentation.blocks.map((block) => {
+ if (!block || typeof block !== "object" || Array.isArray(block)) {
+ return block;
+ }
+ const sanitizedBlock = { ...(block as Record) };
+ for (const field of ["text", "placeholder"]) {
+ if (typeof sanitizedBlock[field] === "string") {
+ sanitizedBlock[field] = stripFormattedReasoningMessage(sanitizedBlock[field]);
+ }
+ }
+ if (Array.isArray(sanitizedBlock.buttons)) {
+ sanitizedBlock.buttons = sanitizedBlock.buttons.map((button) => {
+ if (!button || typeof button !== "object" || Array.isArray(button)) {
+ return button;
+ }
+ const sanitizedButton = { ...(button as Record) };
+ if (typeof sanitizedButton.label === "string") {
+ sanitizedButton.label = stripFormattedReasoningMessage(sanitizedButton.label);
+ }
+ return sanitizedButton;
+ });
+ }
+ if (Array.isArray(sanitizedBlock.options)) {
+ sanitizedBlock.options = sanitizedBlock.options.map((option) => {
+ if (!option || typeof option !== "object" || Array.isArray(option)) {
+ return option;
+ }
+ const sanitizedOption = { ...(option as Record) };
+ if (typeof sanitizedOption.label === "string") {
+ sanitizedOption.label = stripFormattedReasoningMessage(sanitizedOption.label);
+ }
+ return sanitizedOption;
+ });
+ }
+ return sanitizedBlock;
+ });
+ }
+ return presentation;
+}
+
function buildRoutingSchema() {
return {
channel: Type.Optional(Type.String()),
@@ -715,6 +764,7 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
params[field] = stripFormattedReasoningMessage(params[field]);
}
}
+ params.presentation = sanitizePresentationTextFields(params.presentation);
const action = readStringParam(params, "action", {
required: true,