{"record":{"id":"44ad3a0d20ff121f","repo":"oraios/serena","slug":"memory-name-cannot-be-absolute-or-contain-empty-pa","errorCode":null,"errorMessage":"Memory name cannot be absolute or contain empty path segments. Got: {name}","messagePattern":"Memory name cannot be absolute or contain empty path segments\\. Got: (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/serena/memories/memory_manager.py","lineNumber":184,"sourceCode":"        candidate = subdir / filename\n        base_norm = Path(os.path.normpath(base_dir))\n        if not Path(os.path.normpath(candidate)).is_relative_to(base_norm):\n            raise ValueError(f\"Memory name resolves outside the memories directory. Got: {'/'.join(parts)}\")\n        subdir.mkdir(parents=True, exist_ok=True)\n        return candidate\n\n    def get_memory_file_path(self, name: str) -> Path:\n        name = self._sanitize_name(name)\n        parts = name.split(\"/\")\n\n        if \"..\" in parts:\n            raise ValueError(f\"Memory name cannot contain '..' segments. Got: {name}\")\n\n        # Reject absolute names and empty path segments: pathlib discards the base directory when\n        # joined with an absolute path (e.g. \"/etc/cron.d/backdoor\" would reset to \"/etc/cron.d\"),\n        # letting a memory name escape the sandbox. A leading \"/\" produces an empty first segment.\n        if os.path.isabs(name) or \"\" in parts:\n            raise ValueError(f\"Memory name cannot be absolute or contain empty path segments. Got: {name}\")\n\n        if self._is_global(name):\n            if name == self.GLOBAL_TOPIC:\n                raise ValueError(\n                    f'Bare \"{self.GLOBAL_TOPIC}\" is not a valid memory name. Use \"{self.GLOBAL_TOPIC}/<name>\" to address a global memory.'\n                )\n            # Strip \"global/\" prefix and resolve against global dir\n            sub_name = name[len(self.GLOBAL_TOPIC) + 1 :]\n            return self._resolve_memory_path(self._global_memory_dir, sub_name.split(\"/\"))\n\n        # Project-local memory\n        assert self._project_memory_dir is not None, \"Project dir was not passed at initialization\"\n        return self._resolve_memory_path(self._project_memory_dir, parts)\n\n    def _check_write_access(self, name: str, is_tool_context: bool) -> None:\n        # in tool context, memories can be read-only\n        if is_tool_context and self._is_read_only_memory(name):\n            raise PermissionError(f\"Attempted to write to read_only memory: '{name}')\")","sourceCodeStart":166,"sourceCodeEnd":202,"githubUrl":"https://github.com/oraios/serena/blob/7fcbca7e62555ec2287ddb2f083caee805848ea6/src/serena/memories/memory_manager.py#L166-L202","documentation":"get_memory_file_path rejects absolute memory names and names with empty path segments. Because pathlib discards the base directory when joined with an absolute path (e.g. \"/etc/cron.d/backdoor\" would resolve to \"/etc/cron.d\"), an absolute or empty-segment name could let the memory name escape the sandbox, so a ValueError is raised.","triggerScenarios":"Calling get_memory_file_path (or any memory API) with a name starting with '/' (producing an empty first segment after split) or containing '//' / a trailing '/' — e.g. \"/etc/notes\", \"topic//sub\", or \"notes/\".","commonSituations":"Building memory names by concatenating path strings without normalization; LLM agents passing absolute filesystem paths as memory names; template strings with stray slashes; Windows backslashes are converted to '/' by _sanitize_name, so '\\\\server\\\\share' style inputs can surface here.","solutions":["Strip leading slashes and collapse duplicate slashes from the memory name before calling the API.","Pass a relative name like \"topic/sub/name\"; never an absolute path.","If you meant to read an actual filesystem path, use read_file with the absolute path, not the memory API.","Add a pre-call check: reject names where name != posixpath.normpath(name) or name.startswith('/')."],"exampleFix":"// before\nmanager.load_memory(\"/etc/notes\")\n// after\nmanager.load_memory(\"notes\")  # relative, no empty segments\n# or for real files:\nread_file(Path(\"/etc/notes\"))","handlingStrategy":"validation","validationCode":"import posixpath\nif name.startswith(\"/\") or name != posixpath.normpath(name):\n    raise ValueError(f\"memory name must be relative with no empty segments: {name!r}\")","typeGuard":"def is_relative_clean_path(name: str) -> bool:\n    import posixpath\n    parts = name.split(\"/\")\n    return not name.startswith(\"/\") and \"\" not in parts","tryCatchPattern":"try:\n    content = manager.load_memory(name)\nexcept ValueError as e:\n    if \"absolute or contain empty path segments\" in str(e):\n        name = \"/\".join(p for p in name.split(\"/\") if p).lstrip(\"/\")\n        content = manager.load_memory(name)\n    else:\n        raise","preventionTips":["Always store/derive memory names relative to the memories dir","Collapse duplicate slashes and strip leading/trailing slashes before calls","Use read_file for actual absolute filesystem paths"],"tags":["python","path-traversal","security","validation"],"backgroundTag":"absolute-path-memory-name-rejected","analyzedSha":"7fcbca7e62555ec2287ddb2f083caee805848ea6","analyzedAt":"2026-08-29T00:04:09.619Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}