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

Task store escapes the workspace

Error message

Task store escapes the workspace

What it means

Raised by TaskStore._root() in s10_task_system/code.py:85 when the store directory, after resolve(), is not inside the resolved WORKDIR (Path.cwd() at import time). Task JSON files must live inside the workspace as a containment guarantee; if the configured directory resolves elsewhere (symlink, absolute path outside cwd), every store operation refuses to proceed. The check runs on all path access, optionally creating the directory first.

Source

Thrown at s10_task_system/code.py:85

class Task:
    id: str
    subject: str
    description: str
    status: str
    owner: str | None
    blockedBy: list[str]


class TaskStore:
    def __init__(self, directory: Path):
        self.directory = directory

    def _root(self, create: bool = False) -> Path:
        if create:
            self.directory.mkdir(parents=True, exist_ok=True)
        root = self.directory.resolve()
        if not root.is_relative_to(WORKDIR.resolve()):
            raise ValueError("Task store escapes the workspace")
        return root

    def _path(self, task_id: str, create_root: bool = False) -> Path:
        if not isinstance(task_id, str) or not TASK_ID_PATTERN.fullmatch(task_id):
            raise ValueError(f"Invalid task ID: {task_id!r}")
        root = self._root(create=create_root)
        path = (root / f"{task_id}.json").resolve()
        if not path.is_relative_to(root):
            raise ValueError(f"Invalid task ID: {task_id!r}")
        return path

    def exists(self, task_id: str) -> bool:
        return self._path(task_id).is_file()

    def create(self, subject: str, description: str = "",
               blocked_by: list[str] | None = None) -> Task:
        subject = subject.strip()
        if not subject:

View on GitHub (pinned to 985456f4ad)

Solutions

  1. Construct the store inside the workspace: TaskStore(WORKDIR / '.tasks') or any path that resolves under Path.cwd().
  2. In tests, run in a tmp cwd (monkeypatch.chdir(tmp_path)) and construct TaskStore(tmp_path / '.tasks') so both share the same root, or reload the module so WORKDIR is recomputed.
  3. Replace symlinks pointing outside the workspace with a real directory inside it; if sharing is required, place the store in the common parent and run the tool from there.
  4. Verify with .resolve().is_relative_to(Path.cwd().resolve()) before constructing the store.

Example fix

# before (fails when cwd is the project root)
store = TaskStore(Path('/home/user/tasks'))

# after
from s10_task_system.code import WORKDIR
store = TaskStore(WORKDIR / '.tasks')
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def store_dir_is_safe(directory: Path, workdir: Path) -> bool:
    return directory.resolve().is_relative_to(workdir.resolve())

Try / catch

try:
    store = TaskStore(directory)
    tasks = store.list()
except ValueError as e:
    if 'escapes the workspace' in str(e):
        raise SystemExit('task store must live inside the workspace')
    raise

Prevention

When it happens

Trigger: Constructing TaskStore(Path('/etc/tasks')) or any absolute directory outside the current working directory; passing a relative directory that traverses out of the workspace such as Path('../shared_tasks'); a symlinked .tasks directory pointing to another location; running the module with a different cwd than expected, since WORKDIR is frozen at import.

Common situations: Tests that create TaskStore in a tmp_path outside the (separately computed) workspace root; CLI tools invoked from a subdirectory so Path.cwd() differs from the project root; users trying to share one task store across multiple project checkouts via a symlink.

Related errors


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