From a5a5dc85c6d21c8b18f3af6b6e117a73ed22497f Mon Sep 17 00:00:00 2001 From: Aditya kumar singh <143548997+Adityakk9031@users.noreply.github.com> Date: Thu, 17 Sep 2026 16:27:11 +0530 Subject: [PATCH 1/2] fix(tools): prevent customId collisions in claude memory tool (#1547) --- packages/tools/src/claude-memory.test.ts | 39 ++++++++++++++++++++++ packages/tools/src/claude-memory.ts | 23 ++++++++++--- packages/tools/src/tool-operations.test.ts | 4 ++- 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/packages/tools/src/claude-memory.test.ts b/packages/tools/src/claude-memory.test.ts index 7337441a..f51247ce 100644 --- a/packages/tools/src/claude-memory.test.ts +++ b/packages/tools/src/claude-memory.test.ts @@ -210,3 +210,42 @@ describe("ClaudeMemoryTool str_replace replacement literalness", () => { expect(stored).toContain(`price is ${dollarSequence} today`) }) }) + +describe("ClaudeMemoryTool path normalization collision resistance", () => { + it("produces distinct customIds for paths that previously collided", () => { + const tool = new ClaudeMemoryTool("test-api-key") + const paths = [ + "/memories/notes.txt", + "/memories/notes_txt", + "/memories/notes/txt", + "/memories/project/a.md", + "/memories/project_a.md", + ] + + const ids = paths.map((path) => tool.normalizePathToCustomId(path)) + const uniqueIds = new Set(ids) + + expect(uniqueIds.size).toBe(paths.length) + }) + + it("resolves documents stored under legacy customId format", async () => { + // Mock a document saved with legacy normalization (memories_notes_txt) + mockDocuments([ + { + id: "legacy-doc", + customId: "memories_notes_txt", + filePath: "/memories/notes.txt", + content: "legacy content", + }, + ]) + + const tool = new ClaudeMemoryTool("test-api-key") + const result = await tool.handleCommand({ + command: "view", + path: "/memories/notes.txt", + }) + + expect(result.success).toBe(true) + expect(result.content).toContain("legacy content") + }) +}) diff --git a/packages/tools/src/claude-memory.ts b/packages/tools/src/claude-memory.ts index 867c4d82..a419fee9 100644 --- a/packages/tools/src/claude-memory.ts +++ b/packages/tools/src/claude-memory.ts @@ -57,13 +57,22 @@ export class ClaudeMemoryTool { /** * Normalize file path to be used as customId - * Converts /memories/file.txt -> memories_file_txt + * Reversibly encodes path components to prevent collisions between paths like + * `/memories/notes.txt`, `/memories/notes_txt`, and `/memories/notes/txt`. */ - private normalizePathToCustomId(path: string): string { + normalizePathToCustomId(path: string): string { return path .replace(/^\//, "") // Remove leading slash - .replace(/\//g, "_") // Replace / with _ - .replace(/\./g, "_") // Replace . with _ + .replace(/_/g, "__") // Escape underscores: _ -> __ + .replace(/\//g, "_s_") // Encode slashes: / -> _s_ + .replace(/\./g, "_d_") // Encode dots: . -> _d_ + } + + /** + * Legacy normalization used in older versions (/ and . both flattened to _) + */ + private legacyNormalizePathToCustomId(path: string): string { + return path.replace(/^\//, "").replace(/\//g, "_").replace(/\./g, "_") } constructor(apiKey: string, config?: ClaudeMemoryConfig) { @@ -650,8 +659,12 @@ export class ClaudeMemoryTool { }) for (const document of response.memories) { + const isMatchingCustomId = + document.customId === normalizedId || + document.customId === this.legacyNormalizePathToCustomId(filePath) + if ( - document.customId === normalizedId && + isMatchingCustomId && this.getDocumentFilePath(document) === filePath && this.isDocumentInConfiguredScope(document) ) { diff --git a/packages/tools/src/tool-operations.test.ts b/packages/tools/src/tool-operations.test.ts index 8efef91d..0439c5ca 100644 --- a/packages/tools/src/tool-operations.test.ts +++ b/packages/tools/src/tool-operations.test.ts @@ -344,7 +344,9 @@ describe("ClaudeMemoryTool", () => { expect(result.success).toBe(true) expect(clientAdd).toHaveBeenCalledWith( - expect.objectContaining({ customId: "memories_renamed_txt" }), + expect.objectContaining({ + customId: tool.normalizePathToCustomId("/memories/renamed.txt"), + }), ) expect(documentsDeleteBulk).toHaveBeenCalledWith({ ids: [DOCUMENT_ID] }) }) From 9d2358bf0e7ab13a1102958488c25fc42c1146db Mon Sep 17 00:00:00 2001 From: Aditya kumar singh <143548997+Adityakk9031@users.noreply.github.com> Date: Thu, 17 Sep 2026 16:29:40 +0530 Subject: [PATCH 2/2] fix(tools): handle legacy customIds during document verification --- packages/tools/src/claude-memory.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/tools/src/claude-memory.ts b/packages/tools/src/claude-memory.ts index a419fee9..1e2fb995 100644 --- a/packages/tools/src/claude-memory.ts +++ b/packages/tools/src/claude-memory.ts @@ -694,8 +694,12 @@ export class ClaudeMemoryTool { hasUnverifiedCandidate = true continue } + const isMatchingCustomId = + document.customId === normalizedId || + document.customId === this.legacyNormalizePathToCustomId(filePath) + if ( - document.customId !== normalizedId || + !isMatchingCustomId || this.getDocumentFilePath(document) !== filePath || !this.hasExactContainerTags(document.containerTags) ) {