shareAI-lab/learn-claude-code · error · ValueError
Tasks directory escapes workspace
Error message
Tasks directory escapes workspace
What it means
list_tasks() re-validates at call time that the resolved TASKS_ROOT still sits inside the resolved WORKDIR. Unlike _task_path's per-ID check, this guards the whole directory: if the tasks root escapes the workspace (symlink, relocated WORKDIR, retargeted constant), listing is refused so no task file outside the workspace is ever read or parsed.
Source
Thrown at s15_integrated_harness/code.py:267
def load_task(task_id: str) -> Task:
with task_lock:
data = json.loads(_task_path(task_id).read_text(encoding="utf-8"))
task = Task(**data)
if task.id != task_id:
raise ValueError(f"Task file ID does not match {task_id}")
if task.status not in {"pending", "in_progress", "completed"}:
raise ValueError(f"Invalid task status: {task.status}")
return task
def list_tasks() -> list[Task]:
with task_lock:
if not TASKS_DIR.exists():
return []
if not TASKS_ROOT.is_relative_to(WORKDIR.resolve()):
raise ValueError("Tasks directory escapes workspace")
return [load_task(path.stem)
for path in sorted(TASKS_DIR.glob("task_*.json"))]
def get_task_json(task_id: str) -> str:
return json.dumps(asdict(load_task(task_id)), indent=2)
def can_start(task_id: str) -> bool:
# Dependencies are intentionally simple: every blocker must exist and be
# completed before the task can be claimed.
task = load_task(task_id)
for dep_id in task.blockedBy:
try:
dep_path = _task_path(dep_id)
except ValueError:
return False
if not dep_path.exists():View on GitHub (pinned to 985456f4ad)
Solutions
- Make the tasks directory a real directory inside the workspace (remove escaping symlinks).
- Avoid reassigning WORKDIR/TASKS_DIR globals at runtime; set them once at startup from a consistent base.
- Verify with `readlink -f` that both WORKDIR and the tasks dir resolve inside the intended workspace.
Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
def store_inside_workspace(tasks_root: Path, workdir: Path) -> bool:
wr, tr = workdir.resolve(), tasks_root.resolve()
return tr.is_relative_to(wr) Try / catch
try:
tasks = list_tasks()
except ValueError as e:
if "escapes workspace" in str(e):
raise SystemExit("tasks directory misconfigured; fix symlinks/config")
raise Prevention
- Keep the tasks directory a real directory inside WORKDIR.
- Don't symlink it to shared storage; use a copy or export tool instead.
- Verify configuration at startup with an is_relative_to check.
When it happens
Trigger: TASKS_DIR/TASKS_ROOT resolves outside WORKDIR at the moment list_tasks() runs — e.g. the directory is a symlink to /tmp or another checkout, WORKDIR was reassigned after import, or WORKDIR itself is a symlink whose target moved.
Common situations: Users symlinking the tasks dir for sharing between checkouts; the memory runtime reassigning runtime.WORKDIR-related globals; running the harness under a launcher that chdirs or rewrites WORKDIR.
Related errors
- Memory path escapes the store: {filename}
- Worktree path escapes directory: {name!r}
- Path escapes workspace: {path}
- Mailbox path escapes directory: {agent!r}
- Path escapes workspace: {p}
AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14).
Data as JSON: /api/errors/fb45291c7bf69597.
Report an issue: GitHub.