{"record":{"id":"0ec9692b220d2ab8","repo":"shareAI-lab/learn-claude-code","slug":"memory-path-escapes-the-store-filename","errorCode":null,"errorMessage":"Memory path escapes the store: {filename}","messagePattern":"Memory path escapes the store: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"s09_memory/code.py","lineNumber":99,"sourceCode":"        return {}, text\n    return metadata, parts[2].lstrip()\n\ndef memory_slug(name: str) -> str:\n    slug = re.sub(r\"[^\\w]+\", \"-\", name.lower()).strip(\"-_\")\n    return slug or \"memory\"\n\ndef memory_path(filename: str, allow_index: bool = False) -> Path:\n    if Path(filename).name != filename:\n        raise ValueError(f\"Invalid memory filename: {filename}\")\n    if filename == MEMORY_INDEX.name and not allow_index:\n        raise ValueError(\"The memory index is not a memory record\")\n\n    root = MEMORY_DIR.resolve()\n    if not root.is_relative_to(WORKDIR.resolve()):\n        raise ValueError(\"Memory directory escapes the workspace\")\n    path = (root / filename).resolve()\n    if not path.is_relative_to(root):\n        raise ValueError(f\"Memory path escapes the store: {filename}\")\n    return path\n\ndef _memory_slug(name: str) -> str:\n    return memory_slug(name)\n\ndef _normalized_memory_text(value: str) -> str:\n    return \" \".join(value.lower().split())\n\ndef should_store_memory(candidate: dict, existing: list[dict]) -> bool:\n    \"\"\"Accept durable records that are not temporary or already stored.\"\"\"\n    if not isinstance(candidate, dict):\n        return False\n    if candidate.get(\"scope\") != \"persistent\":\n        return False\n    if candidate.get(\"type\") not in MEMORY_TYPES:\n        return False\n\n    name = str(candidate.get(\"name\", \"\")).strip()","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/shareAI-lab/learn-claude-code/blob/985456f4adea6f4df8fbad4112245dbd97444eae/s09_memory/code.py#L81-L117","documentation":"Raised by memory_path() in s09_memory/code.py:99 when the resolved absolute path of root/filename no longer sits inside the resolved MEMORY_DIR root. This is a path-traversal guard: after joining the store root with the requested filename, resolve() follows symlinks and normalizes '..', and if the result falls outside the store the function refuses to return it. It is the last line of defense after the earlier check that rejects any filename containing a path separator.","triggerScenarios":"Calling memory_path() (directly or via write_memory_file/read_memory helpers) with a filename that survives the Path(filename).name check only through symlink tricks, or when MEMORY_DIR itself contains a symlink that points outside the workspace. On case-insensitive filesystems a mismatched-case store directory can also make is_relative_to fail after resolution. The earlier separator check already blocks plain '../' names, so this branch fires mainly on symlinked store layouts.","commonSituations":"A user symlinks .memory (or a parent of it) to another location to share memory across workspaces; the workspace was moved or copied with symlinks preserved; running on macOS/Windows where /tmp or the home dir resolves through /var or a junction, changing the resolved prefix between the root check and the join.","solutions":["Verify MEMORY_DIR.resolve() and the returned path on the same resolved filesystem: print both paths when the error occurs and confirm the store directory is a real directory inside the workspace, not a symlink pointing outside.","Remove or relocate symlinks so that the memory store physically lives under WORKDIR; recreate it as a real directory and copy existing records in.","If sharing a store across workspaces is the actual goal, make WORKDIR the common parent that contains the store, rather than symlinking the store out of the workspace.","As a last resort for tests, monkeypatch MEMORY_DIR to a real temp directory under a temp WORKDIR so resolution stays consistent."],"exampleFix":"# before: .memory is a symlink to /shared/memory\nln -s /shared/memory .memory\n\n# after: real directory inside the workspace\nrm .memory && mkdir .memory && cp /shared/memory/*.md .memory/","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef safe_memory_path(memory_dir: Path, filename: str) -> Path:\n    root = memory_dir.resolve()\n    if Path(filename).name != filename:\n        raise ValueError('bad filename')\n    candidate = (root / filename).resolve()\n    if not candidate.is_relative_to(root):\n        raise ValueError('escapes store')\n    return candidate","typeGuard":null,"tryCatchPattern":"try:\n    path = memory_path(filename)\nexcept ValueError as e:\n    log.warning('rejected memory path %s: %s', filename, e)\n    return None","preventionTips":["Keep the memory store a real directory under WORKDIR; avoid symlinking it elsewhere.","Never construct memory filenames from user input containing '/' or '..'.","Test path helpers on the same filesystem/OS you deploy on; resolution differs across platforms."],"tags":["filesystem","path-traversal","security","symlink"],"backgroundTag":null,"analyzedSha":"985456f4adea6f4df8fbad4112245dbd97444eae","analyzedAt":"2026-08-14T22:02:26.028Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}