{"record":{"id":"967302e184729539","repo":"shareAI-lab/learn-claude-code","slug":"path-escapes-workspace-p-967302","errorCode":null,"errorMessage":"Path escapes workspace: {p}","messagePattern":"Path escapes workspace: (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"agents/s03_todo_write.py","lineNumber":96,"sourceCode":"        if not self.items:\n            return \"No todos.\"\n        lines = []\n        for item in self.items:\n            marker = {\"pending\": \"[ ]\", \"in_progress\": \"[>]\", \"completed\": \"[x]\"}[item[\"status\"]]\n            lines.append(f\"{marker} #{item['id']}: {item['text']}\")\n        done = sum(1 for t in self.items if t[\"status\"] == \"completed\")\n        lines.append(f\"\\n({done}/{len(self.items)} completed)\")\n        return \"\\n\".join(lines)\n\n\nTODO = TodoManager()\n\n\n# -- Tool implementations --\ndef safe_path(p: str) -> Path:\n    path = (WORKDIR / p).resolve()\n    if not path.is_relative_to(WORKDIR):\n        raise ValueError(f\"Path escapes workspace: {p}\")\n    return path\n\ndef run_bash(command: str) -> str:\n    dangerous = [\"rm -rf /\", \"sudo\", \"shutdown\", \"reboot\", \"> /dev/\"]\n    if any(d in command for d in dangerous):\n        return \"Error: Dangerous command blocked\"\n    try:\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\n                           capture_output=True, text=True, timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return \"Error: Timeout (120s)\"\n\ndef run_read(path: str, limit: int = None) -> str:\n    try:\n        lines = safe_path(path).read_text().splitlines()\n        if limit and limit < len(lines):","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/shareAI-lab/learn-claude-code/blob/985456f4adea6f4df8fbad4112245dbd97444eae/agents/s03_todo_write.py#L78-L114","documentation":"The same workspace sandbox guard as s02, duplicated at agents/s03_todo_write.py:96 in the todo-list agent script. Every file tool path is joined onto WORKDIR (Path.cwd() at launch), resolved, and must remain inside WORKDIR. Absolute paths win over the join in pathlib, and resolve() collapses `..` and follows symlinks, so both escape routes are caught.","triggerScenarios":"The model calls a file tool with \"/abs/path\", \"../../../outside.txt\", or a path through a symlink pointing outside the workspace. Also fires when the harness was launched from a cwd unrelated to the files the model was told (in its system prompt) it owns.","commonSituations":"Model copies an absolute path from bash tool output or from the system prompt's stated directory. Harness started from the wrong directory. Symlinked workspace roots resolving outside the literal cwd.","solutions":["Pass workspace-relative paths (\"notes.md\", \"src/app.py\") to file tools","Convert known-in-workspace absolute paths with os.path.relpath before calling","State in the system prompt / todo text that tool paths are relative to WORKDIR","Launch the agent with cwd = the workspace so WORKDIR is correct"],"exampleFix":"// before\n{\"op\": \"write\", \"path\": \"/tmp/errlookup-AdzUmp/TODO.md\"}\n// ValueError: Path escapes workspace\n\n// after\n{\"op\": \"write\", \"path\": \"TODO.md\"}","handlingStrategy":"validation","validationCode":"import os\nfrom pathlib import Path\n\ndef to_workspace_relative(p: str, workdir: Path) -> str | None:\n    \"\"\"Return a safe relative path, or None if it escapes.\"\"\"\n    try:\n        abs_p = (workdir / p).resolve()\n        return os.path.relpath(abs_p, workdir) if abs_p.is_relative_to(workdir) else None\n    except (OSError, ValueError, RuntimeError):\n        return None\n\nrel = to_workspace_relative(model_path, WORKDIR)\nassert rel and not rel.startswith(\"..\"), \"path escapes workspace\"","typeGuard":"def is_safe_tool_path(p: object, workdir) -> bool:\n    if not isinstance(p, str) or not p or p.startswith((\"/\", \"~\")):\n        return False\n    try:\n        return (workdir / p).resolve().is_relative_to(workdir)\n    except (OSError, RuntimeError):\n        return False","tryCatchPattern":"try:\n    path = safe_path(p)\nexcept ValueError:\n    return f\"Path rejected: {p}. Use a path relative to {WORKDIR}.\"","preventionTips":["Mention todo files with workspace-relative paths in plans and prompts","Normalize absolute paths to relative before file tool calls","Launch from the workspace root so relative and absolute views agree"],"tags":["security","path-traversal","sandbox","agent-tools","python"],"backgroundTag":null,"analyzedSha":"985456f4adea6f4df8fbad4112245dbd97444eae","analyzedAt":"2026-08-14T22:02:26.028Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}