shareAI-lab/learn-claude-code · error · ValueError

Memory path escapes the store: {filename}

Error message

Memory path escapes the store: {filename}

What it means

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.

Source

Thrown at s09_memory/code.py:99

        return {}, text
    return metadata, parts[2].lstrip()

def memory_slug(name: str) -> str:
    slug = re.sub(r"[^\w]+", "-", name.lower()).strip("-_")
    return slug or "memory"

def memory_path(filename: str, allow_index: bool = False) -> Path:
    if Path(filename).name != filename:
        raise ValueError(f"Invalid memory filename: {filename}")
    if filename == MEMORY_INDEX.name and not allow_index:
        raise ValueError("The memory index is not a memory record")

    root = MEMORY_DIR.resolve()
    if not root.is_relative_to(WORKDIR.resolve()):
        raise ValueError("Memory directory escapes the workspace")
    path = (root / filename).resolve()
    if not path.is_relative_to(root):
        raise ValueError(f"Memory path escapes the store: {filename}")
    return path

def _memory_slug(name: str) -> str:
    return memory_slug(name)

def _normalized_memory_text(value: str) -> str:
    return " ".join(value.lower().split())

def should_store_memory(candidate: dict, existing: list[dict]) -> bool:
    """Accept durable records that are not temporary or already stored."""
    if not isinstance(candidate, dict):
        return False
    if candidate.get("scope") != "persistent":
        return False
    if candidate.get("type") not in MEMORY_TYPES:
        return False

    name = str(candidate.get("name", "")).strip()

View on GitHub (pinned to 985456f4ad)

Solutions

  1. 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.
  2. Remove or relocate symlinks so that the memory store physically lives under WORKDIR; recreate it as a real directory and copy existing records in.
  3. 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.
  4. As a last resort for tests, monkeypatch MEMORY_DIR to a real temp directory under a temp WORKDIR so resolution stays consistent.

Example fix

# before: .memory is a symlink to /shared/memory
ln -s /shared/memory .memory

# after: real directory inside the workspace
rm .memory && mkdir .memory && cp /shared/memory/*.md .memory/
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def safe_memory_path(memory_dir: Path, filename: str) -> Path:
    root = memory_dir.resolve()
    if Path(filename).name != filename:
        raise ValueError('bad filename')
    candidate = (root / filename).resolve()
    if not candidate.is_relative_to(root):
        raise ValueError('escapes store')
    return candidate

Try / catch

try:
    path = memory_path(filename)
except ValueError as e:
    log.warning('rejected memory path %s: %s', filename, e)
    return None

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14). Data as JSON: /api/errors/0ec9692b220d2ab8. Report an issue: GitHub.