shareAI-lab/learn-claude-code · error · ValueError
Tasks directory escapes workspace
Error message
Tasks directory escapes workspace
What it means
list_tasks() re-checks at call time that TASKS_ROOT (the resolved .tasks directory, computed at import) is still inside WORKDIR.resolve(). This guards against the workspace being swapped, moved, or re-symlinked after startup so the listing never enumerates a foreign directory.
Source
Thrown at s13_agent_teams/code.py:192
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(task_id: str) -> str:
"""Return full task details as JSON."""
task = load_task(task_id)
return json.dumps(asdict(task), indent=2)
def can_start(task_id: str) -> bool:
"""Check if all blockedBy dependencies are completed.
Missing dependencies are treated as blocked."""
task = load_task(task_id)
for dep_id in task.blockedBy:
try:
dep_path = _task_path(dep_id)
except ValueError:View on GitHub (pinned to 985456f4ad)
Solutions
- Avoid rotating/re-pointing the workspace symlink while the agent process runs; restart after rotations.
- Set WORKDIR to the fully resolved real path at startup.
- In tests, reload the module or recompute TASKS_ROOT after changing WORKDIR.
Example fix
# before
WORKDIR = Path('/srv/app/current') # 'current' is a rotating symlink
# after
WORKDIR = Path('/srv/app/current').resolve() # pinned to the real release dir Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
def tasks_root_inside_workdir() -> bool:
return TASKS_ROOT.is_relative_to(WORKDIR.resolve()) Try / catch
try:
tasks = list_tasks()
except ValueError as exc:
if 'escapes workspace' in str(exc):
abort_run('workspace moved or re-symlinked; restart the agent')
raise Prevention
- Pin WORKDIR with .resolve() at startup.
- Restart processes after rotating workspace symlinks (deploys).
- Don't monkeypatch WORKDIR in tests without recomputing module roots.
When it happens
Trigger: WORKDIR is a symlink that is retargeted between import and the list_tasks() call; the workspace directory is moved and a symlink left behind; tests monkeypatching WORKDIR without recomputing TASKS_ROOT.
Common situations: Deployments where the workspace path goes through a 'current' symlink that is rotated during the process's lifetime; test fixtures that mutate module globals inconsistently.
Related errors
- Mailbox path escapes directory: {agent!r}
- Memory path escapes the store: {filename}
- Worktree path escapes directory: {name!r}
- Tasks directory escapes workspace
- Mailbox path escapes directory: {agent!r}
AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14).
Data as JSON: /api/errors/1ca657db739dc792.
Report an issue: GitHub.