{"record":{"id":"a7500c04d9a8480e","repo":"shareAI-lab/learn-claude-code","slug":"task-store-escapes-the-workspace","errorCode":null,"errorMessage":"Task store escapes the workspace","messagePattern":"Task store escapes the workspace","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"s10_task_system/code.py","lineNumber":85,"sourceCode":"class Task:\n    id: str\n    subject: str\n    description: str\n    status: str\n    owner: str | None\n    blockedBy: list[str]\n\n\nclass TaskStore:\n    def __init__(self, directory: Path):\n        self.directory = directory\n\n    def _root(self, create: bool = False) -> Path:\n        if create:\n            self.directory.mkdir(parents=True, exist_ok=True)\n        root = self.directory.resolve()\n        if not root.is_relative_to(WORKDIR.resolve()):\n            raise ValueError(\"Task store escapes the workspace\")\n        return root\n\n    def _path(self, task_id: str, create_root: bool = False) -> Path:\n        if not isinstance(task_id, str) or not TASK_ID_PATTERN.fullmatch(task_id):\n            raise ValueError(f\"Invalid task ID: {task_id!r}\")\n        root = self._root(create=create_root)\n        path = (root / f\"{task_id}.json\").resolve()\n        if not path.is_relative_to(root):\n            raise ValueError(f\"Invalid task ID: {task_id!r}\")\n        return path\n\n    def exists(self, task_id: str) -> bool:\n        return self._path(task_id).is_file()\n\n    def create(self, subject: str, description: str = \"\",\n               blocked_by: list[str] | None = None) -> Task:\n        subject = subject.strip()\n        if not subject:","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/shareAI-lab/learn-claude-code/blob/985456f4adea6f4df8fbad4112245dbd97444eae/s10_task_system/code.py#L67-L103","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Construct the store inside the workspace: TaskStore(WORKDIR / '.tasks') or any path that resolves under Path.cwd().","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.","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.","Verify with .resolve().is_relative_to(Path.cwd().resolve()) before constructing the store."],"exampleFix":"# before (fails when cwd is the project root)\nstore = TaskStore(Path('/home/user/tasks'))\n\n# after\nfrom s10_task_system.code import WORKDIR\nstore = TaskStore(WORKDIR / '.tasks')","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef store_dir_is_safe(directory: Path, workdir: Path) -> bool:\n    return directory.resolve().is_relative_to(workdir.resolve())","typeGuard":null,"tryCatchPattern":"try:\n    store = TaskStore(directory)\n    tasks = store.list()\nexcept ValueError as e:\n    if 'escapes the workspace' in str(e):\n        raise SystemExit('task store must live inside the workspace')\n    raise","preventionTips":["Always construct the store as WORKDIR / '.tasks'.","In tests, chdir into a tmp workspace before importing the module so WORKDIR matches the store.","Never symlink the store outside the workspace; share stores by running from a common parent."],"tags":["filesystem","path-traversal","tasks","configuration"],"backgroundTag":null,"analyzedSha":"985456f4adea6f4df8fbad4112245dbd97444eae","analyzedAt":"2026-08-14T22:02:26.028Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}