Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions packages/loopover-mcp/lib/redact-local-path.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ export function redactLocalPath(value) {
// Both `/g` patterns are rebuilt per call so no `lastIndex` state carries between invocations.
// Delimiter-anchored roots (`~/`, `~\`, `C:\`, `C:/`, `/`) whose interior segments may contain
// spaces, e.g. `/Users/Alice Smith/project` — the anchoring prefix is preserved, only the path swaps.
const pathSegment = "[^\\\\/\\s\"'`,;)]+(?:\\s+[^\\\\/\\s\"'`,;)]+)*(?=[\\\\/])";
const pathTail = "[^\\\\/\\s\"'`,;)]+";
const rootedPath = new RegExp(`(^|[\\s"'\\\`=])((?:~[\\\\/]|[A-Za-z]:[\\\\/]|/)(?:${pathSegment}[\\\\/])*${pathTail})`, "g");
const pathSegment = "[^\\\\/\\s\"'`,;)\\]]+(?:\\s+[^\\\\/\\s\"'`,;)\\]]+)*(?=[\\\\/])";
const pathTail = "[^\\\\/\\s\"'`,;)\\]]+";
// Prefix delimiters a real path can immediately follow in pasted stack-trace/validation-output text.
// `(` is the Node.js stack-frame shape (`at fn (/abs/path:10:5)`); `[` and `:` cover the same "no space
// before the path" shape in bracketed log lines and colon-joined messages (e.g. `path:/abs/path`).
const rootedPath = new RegExp(`(^|[\\s"'\\\`=(\\[:])((?:~[\\\\/]|[A-Za-z]:[\\\\/]|/)(?:${pathSegment}[\\\\/])*${pathTail})`, "g");
return text
.replace(rootedPath, (_, prefix) => `${prefix}<local-path>`)
// Home/Windows roots that appear mid-token with no leading delimiter (so the anchored pass skips
Expand Down
18 changes: 18 additions & 0 deletions test/unit/redact-local-path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,24 @@ describe("redactLocalPath (heuristic: detect an unknown path in free text)", ()
expect(redacted).not.toContain("~/private");
});

// #6258: a Node.js stack-frame path (`at fn (/abs/path:line:col)`) previously leaked in full because
// `(` was missing from the prefix delimiter class -- only a space-prefixed path redacted correctly.
it("redacts a parenthesis-prefixed path (the Node.js stack-frame shape)", () => {
// The trailing `:10:5` line/column suffix has no path separator before the closing `)`, so it's
// swallowed into the same redacted match -- still a full, leak-free redaction either way.
expect(redactLocalPath("at Object.<anonymous> (/Users/alice/secretproject/file.js:10:5)")).toBe(
"at Object.<anonymous> (<local-path>)",
);
});

it("redacts a bracket-prefixed path, preserving the closing bracket", () => {
expect(redactLocalPath("[/Users/alice/secretproject/file.js]")).toBe("[<local-path>]");
});

it("redacts a colon-prefixed path with no space (e.g. `label:/abs/path`)", () => {
expect(redactLocalPath("error:/Users/alice/secretproject/file.js")).toBe("error:<local-path>");
});

it("leaves text with no local path untouched, including a bare slash", () => {
expect(redactLocalPath("just some text, version 1.2.3")).toBe("just some text, version 1.2.3");
expect(redactLocalPath("pass --flag / or | here")).toBe("pass --flag / or | here");
Expand Down