{"record":{"id":"a754ae80ac2888da","repo":"shareAI-lab/learn-claude-code","slug":"path-escapes-workspace-p","errorCode":null,"errorMessage":"Path escapes workspace: {p}","messagePattern":"Path escapes workspace: (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"agents/s02_tool_use.py","lineNumber":44,"sourceCode":"from anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\n\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n    os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\nSYSTEM = f\"You are a coding agent at {WORKDIR}. Use tools to solve tasks. Act, don't explain.\"\n\n\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\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,\n                           encoding=\"utf-8\", errors=\"replace\", 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\n\ndef run_read(path: str, limit: int = None) -> str:","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/shareAI-lab/learn-claude-code/blob/985456f4adea6f4df8fbad4112245dbd97444eae/agents/s02_tool_use.py#L26-L62","documentation":"Raised by safe_path() in agents/s02_tool_use.py:44, the workspace sandbox guard for every file tool in this agent script. It resolves the model-supplied path against WORKDIR (the process cwd at launch) with Path.resolve(), then rejects it unless the result is still inside WORKDIR. In pathlib, `WORKDIR / p` with an absolute `p` yields that absolute path, so any absolute path outside the workspace, or any `../` traversal that resolves outside it, trips this check.","triggerScenarios":"The LLM calls read_file/write_file/edit_file with an absolute path like \"/etc/passwd\", a path with leading slashes, a \"../../secrets.env\" traversal, or a symlink inside the workspace whose target lives outside (resolve() follows symlinks). It can also fire if the agent process was launched from a different directory than the intended workspace, making WORKDIR = Path.cwd() point somewhere the model's files are not under.","commonSituations":"Models habitually emit absolute paths copied from tool output or the system prompt (the prompt embeds the absolute WORKDIR). Broken setups where the harness is started from $HOME or / while the task files live in a project dir. Workspaces reached through symlinked paths (macOS /tmp -> /private/tmp style) can also resolve outside the literal WORKDIR.","solutions":["Call file tools with paths relative to the workspace root, e.g. \"src/main.py\" not \"/home/user/proj/src/main.py\"","If the path is absolute but inside the workspace, strip the WORKDIR prefix before passing it (os.path.relpath(p, WORKDIR))","Make the system prompt tell the model explicitly that tool paths must be relative to the stated workspace directory","Launch the agent with cwd set to the actual workspace so WORKDIR matches where files live","If symlinks inside the workspace legitimately point outside, replace the sandbox with an allowlist of permitted roots instead of widening the check blindly"],"exampleFix":"// before (tool call)\n{\"op\": \"read\", \"path\": \"/home/user/proj/src/main.py\"}\n// raises ValueError: Path escapes workspace\n\n// after (tool call)\n{\"op\": \"read\", \"path\": \"src/main.py\"}\n\n// after (harness-side normalization before calling safe_path)\nimport os\nrel = os.path.relpath(os.path.abspath(p), WORKDIR)\npath = safe_path(rel)","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef is_in_workspace(p: str, workdir: Path) -> bool:\n    try:\n        return (workdir / p).resolve().is_relative_to(workdir)\n    except (OSError, RuntimeError):\n        return False\n\n# before any file tool call:\nassert is_in_workspace(user_path, WORKDIR), f\"{user_path} escapes workspace\"","typeGuard":"from pathlib import Path\n\ndef is_workspace_relative_path(p: str, workdir: Path) -> bool:\n    \"\"\"True iff p joins+resolves inside workdir (no escape via .., abs, or symlink).\"\"\"\n    if not isinstance(p, str) or not p:\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 as e:\n    # feed the rejection back to the model as a tool error with a hint\n    return f\"Tool error: {e}. Pass a path relative to {WORKDIR} (no leading /, no ..).\"","preventionTips":["State in the system prompt that all tool paths must be relative to WORKDIR","Normalize model paths at the tool boundary: os.path.relpath against WORKDIR when the absolute path is already known to be inside it","Launch the harness with cwd set to the workspace so WORKDIR is the intended root","Never widen safe_path for absolute paths; convert the input instead"],"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"}