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

Memory directory escapes the workspace

Error message

Memory directory escapes the workspace

What it means

Raised by memory_path() in s09_memory/code.py as a startup/configuration sanity check: MEMORY_DIR.resolve() must lie inside WORKDIR.resolve(), or every memory operation aborts. It is an environment invariant, not user-input dependent — if it fires, the memory directory was mounted/placed outside the agent sandbox, and the module refuses rather than write outside the workspace. This fires before any filename logic, so all memory tools fail uniformly.

Source

Thrown at s09_memory/code.py:96

    except yaml.YAMLError:
        return {}, text
    if not isinstance(metadata, dict):
        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:

View on GitHub (pinned to 985456f4ad)

Solutions

  1. Place the memory store inside the workspace, e.g. WORKDIR / '.agent' / 'memory', and update the configuration
  2. If an outside location is truly required, extend WORKDIR to a common ancestor containing both — the module will not allow bypassing this
  3. Check for symlinks on MEMORY_DIR/WORKDIR and use real paths in configuration so resolve() is stable

Example fix

# before (env)
MEMORY_DIR=/var/lib/agent/memory   # outside WORKDIR
# after
MEMORY_DIR=<WORKDIR>/.agent/memory  # inside WORKDIR
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

wd = WORKDIR.resolve()
md = MEMORY_DIR.resolve()
assert md.is_relative_to(wd), (
    f"MEMORY_DIR {md} must live inside WORKDIR {wd}; "
    f"set it to {wd / '.agent' / 'memory'}"
)

Prevention

When it happens

Trigger: MEMORY_DIR configured (or defaulted) to something like ~/.memory or /var/data/memory while WORKDIR is the project directory; MEMORY_DIR or WORKDIR being a symlink whose target resolves outside the expected tree.

Common situations: Env var pointing the memory store at a shared location outside the workspace; project moved so a previously-inside relative path now resolves elsewhere; symlinked home/project directories changing resolve() outcomes across machines.

Related errors


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