Skip to content
Open
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
10 changes: 8 additions & 2 deletions src/anthropic/lib/tools/_beta_builtin_memory_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,10 @@ def __init__(self, base_path: str = "./memory"):

def _validate_path(self, path: str) -> Path:
"""Validate and resolve memory paths"""
if not path.startswith("/memories"):
# The separator matters: without it "/memoriesX" passes the prefix check and
# then slices down to "X", so a path that names something outside the store
# is silently re-pointed at <root>/X.
if path != "/memories" and not path.startswith("/memories/"):
raise ToolError(f"Path must start with /memories, got: {path}")

relative_path = path[len("/memories") :].lstrip("/")
Expand Down Expand Up @@ -681,7 +684,10 @@ async def _ensure_memory_root(self) -> None:

async def _validate_path(self, path: str) -> AsyncPath:
"""Validate and resolve memory paths"""
if not path.startswith("/memories"):
# The separator matters: without it "/memoriesX" passes the prefix check and
# then slices down to "X", so a path that names something outside the store
# is silently re-pointed at <root>/X.
if path != "/memories" and not path.startswith("/memories/"):
raise ToolError(f"Path must start with /memories, got: {path}")

relative_path = path[len("/memories") :].lstrip("/")
Expand Down
42 changes: 42 additions & 0 deletions tests/lib/tools/memory_tools/test_filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,27 @@ def test_delete_not_allow_deleting_memories_directory(
with pytest.raises(ToolError, match="Cannot delete the /memories directory itself"):
sync_local_filesystem_tool.delete(BetaMemoryTool20250818DeleteCommand(command="delete", path="/memories"))

@pytest.mark.parametrize("path", ["/memoriesX", "/memoriesXY/z.md", "/memoriessub", "/memories."])
def test_paths_sharing_the_prefix_without_a_separator_are_rejected(
self, sync_local_filesystem_tool: BetaLocalFilesystemMemoryTool, temp_directory: str, path: str
) -> None:
"""Paths that share the prefix without a separator name nothing inside the
store. Unchecked, they slice down to a relative path and land on a real
entry: delete "/memoriessub" removes <root>/sub and reports success."""
sync_local_filesystem_tool.create(
BetaMemoryTool20250818CreateCommand(command="create", file_text="keep me", path="/memories/sub/a.txt")
)

with pytest.raises(ToolError, match="Path must start with /memories"):
sync_local_filesystem_tool.delete(BetaMemoryTool20250818DeleteCommand(command="delete", path=path))

with pytest.raises(ToolError, match="Path must start with /memories"):
sync_local_filesystem_tool.create(
BetaMemoryTool20250818CreateCommand(command="create", file_text="written", path=path)
)

assert get_directory_snapshot(temp_directory) == {"memories/sub/a.txt": "keep me"}

def test_rename(self, sync_local_filesystem_tool: BetaLocalFilesystemMemoryTool) -> None:
sync_local_filesystem_tool.create(
BetaMemoryTool20250818CreateCommand(
Expand Down Expand Up @@ -988,6 +1009,27 @@ async def test_delete_not_allow_deleting_memories_directory(
BetaMemoryTool20250818DeleteCommand(command="delete", path="/memories")
)

@pytest.mark.parametrize("path", ["/memoriesX", "/memoriesXY/z.md", "/memoriessub", "/memories."])
async def test_paths_sharing_the_prefix_without_a_separator_are_rejected(
self, async_local_filesystem_tool: BetaAsyncLocalFilesystemMemoryTool, temp_directory: str, path: str
) -> None:
"""Paths that share the prefix without a separator name nothing inside the
store. Unchecked, they slice down to a relative path and land on a real
entry: delete "/memoriessub" removes <root>/sub and reports success."""
await async_local_filesystem_tool.create(
BetaMemoryTool20250818CreateCommand(command="create", file_text="keep me", path="/memories/sub/a.txt")
)

with pytest.raises(ToolError, match="Path must start with /memories"):
await async_local_filesystem_tool.delete(BetaMemoryTool20250818DeleteCommand(command="delete", path=path))

with pytest.raises(ToolError, match="Path must start with /memories"):
await async_local_filesystem_tool.create(
BetaMemoryTool20250818CreateCommand(command="create", file_text="written", path=path)
)

assert get_directory_snapshot(temp_directory) == {"memories/sub/a.txt": "keep me"}

async def test_rename(self, async_local_filesystem_tool: BetaAsyncLocalFilesystemMemoryTool) -> None:
await async_local_filesystem_tool.create(
BetaMemoryTool20250818CreateCommand(
Expand Down