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

  1. Avoid rotating/re-pointing the workspace symlink while the agent process runs; restart after rotations.
  2. Set WORKDIR to the fully resolved real path at startup.
  3. 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

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


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