Claude memory tool: path → customId normalization collides, silently overwriting unrelated memory files
Severity: Medium (silent data loss)
File: packages/tools/src/claude-memory.ts:52-58
Description
normalizePathToCustomId flattens both / and . to _:
private normalizePathToCustomId(path: string): string {
return path
.replace(/^\//, "") // strip leading slash
.replace(/\//g, "_") // / -> _
.replace(/\./g, "_") // . -> _
}The mapping is not injective. All of these produce the identical customId
memories_notes_txt:
| Path | customId |
|---|---|
/memories/notes.txt |
memories_notes_txt |
/memories/notes_txt |
memories_notes_txt |
/memories/notes/txt |
memories_notes_txt |
And /memories/project/a.md collides with /memories/project_a.md.
Since customId is the document identity for client.add(), create,
str_replace, insert, rename, and delete, a collision means:
createon the second path overwrites the first file's content with no warning — the tool reports success.viewon either path returns whichever document won last.deleteon either path destroys both.
Nested memory directories are the realistic trigger: Claude's memory tool
routinely creates paths like /memories/projects/foo.md, and any sibling
/memories/projects_foo.md (or a second nesting level that flattens the same
way) silently clobbers it.
Suggested fix
Make the encoding reversible — escape the separator before substituting it, e.g.
path.replace(/_/g, "__").replace(/[/.]/g, "_"), or drop the transformation
entirely and hash the path (customId = "mem_" + sha256(path).slice(0, 32)) if
the backend accepts arbitrary customIds. Either way, add a round-trip assertion
for the three colliding paths above.
Source: supermemoryai/supermemory