diff --git a/src/github/utils/sanitizer.ts b/src/github/utils/sanitizer.ts index 83ee096ba..8f4d1870c 100644 --- a/src/github/utils/sanitizer.ts +++ b/src/github/utils/sanitizer.ts @@ -62,6 +62,12 @@ export function sanitizeContent(content: string): string { return content; } +export function sanitizeOutgoingCommentContent(content: string): string { + content = stripInvisibleCharacters(content); + content = redactGitHubTokens(content); + return content; +} + export function redactGitHubTokens(content: string): string { // GitHub Personal Access Tokens (classic): ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars) content = content.replace( diff --git a/src/mcp/github-comment-server.ts b/src/mcp/github-comment-server.ts index ef6728c94..bf777941e 100644 --- a/src/mcp/github-comment-server.ts +++ b/src/mcp/github-comment-server.ts @@ -6,7 +6,7 @@ import { z } from "zod"; import { GITHUB_API_URL } from "../github/api/config"; import { Octokit } from "@octokit/rest"; import { updateClaudeComment } from "../github/operations/comments/update-claude-comment"; -import { sanitizeContent } from "../github/utils/sanitizer"; +import { sanitizeOutgoingCommentContent } from "../github/utils/sanitizer"; // Get repository information from environment variables const REPO_OWNER = process.env.REPO_OWNER; @@ -55,7 +55,7 @@ server.tool( const isPullRequestReviewComment = eventName === "pull_request_review_comment"; - const sanitizedBody = sanitizeContent(body); + const sanitizedBody = sanitizeOutgoingCommentContent(body); const result = await updateClaudeComment(octokit, { owner, diff --git a/src/mcp/github-inline-comment-server.ts b/src/mcp/github-inline-comment-server.ts index 535124f32..435e62c6b 100644 --- a/src/mcp/github-inline-comment-server.ts +++ b/src/mcp/github-inline-comment-server.ts @@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { appendFileSync } from "fs"; import { z } from "zod"; import { createOctokit } from "../github/api/client"; -import { sanitizeContent } from "../github/utils/sanitizer"; +import { sanitizeOutgoingCommentContent } from "../github/utils/sanitizer"; // Get repository and PR information from environment variables const REPO_OWNER = process.env.REPO_OWNER; @@ -97,8 +97,8 @@ server.tool( const repo = REPO_NAME; const pull_number = parseInt(PR_NUMBER, 10); - // Sanitize the comment body to remove any potential GitHub tokens - const sanitizedBody = sanitizeContent(body); + // Preserve review suggestions exactly while still redacting accidental tokens. + const sanitizedBody = sanitizeOutgoingCommentContent(body); // Validate that either line or both startLine and line are provided if (!line && !startLine) { diff --git a/test/sanitizer.test.ts b/test/sanitizer.test.ts index a89353b78..1e6ee7f11 100644 --- a/test/sanitizer.test.ts +++ b/test/sanitizer.test.ts @@ -6,6 +6,7 @@ import { stripHiddenAttributes, normalizeHtmlEntities, sanitizeContent, + sanitizeOutgoingCommentContent, stripHtmlComments, redactGitHubTokens, } from "../src/github/utils/sanitizer"; @@ -243,6 +244,30 @@ describe("sanitizeContent", () => { }); }); +describe("sanitizeOutgoingCommentContent", () => { + it("preserves suggestion blocks with quoted attributes", () => { + const body = `**Drop the trailing full-stop.** + +\`\`\`suggestion + +\`\`\` +`; + + expect(sanitizeOutgoingCommentContent(body)).toBe(body); + }); + + it("redacts tokens without stripping visible suggestion text", () => { + const token = "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"; + const body = `\`\`\`suggestion + +\`\`\``; + + expect(sanitizeOutgoingCommentContent(body)).toBe(`\`\`\`suggestion + +\`\`\``); + }); +}); + describe("redactGitHubTokens", () => { it("should redact personal access tokens (ghp_)", () => { const token = "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";