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.
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.
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
- 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.
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
- Could not allocate a unique task ID
- Memory path escapes the store
- Path escapes workspace
- Tasks directory escapes workspace
- Could not allocate a unique task ID
AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14).
Data as JSON: /api/errors/a7500c04d9a8480e.
Report an issue: GitHub.
Appendix: 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)