shareAI-lab/learn-claude-code · error · ValueError

Path escapes workspace: {p}

Error message

Path escapes workspace: {p}

What it means

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.

Source

Thrown at agents/s02_tool_use.py:44

from anthropic import Anthropic
from dotenv import load_dotenv

load_dotenv(override=True)

if os.getenv("ANTHROPIC_BASE_URL"):
    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)

WORKDIR = Path.cwd()
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
MODEL = os.environ["MODEL_ID"]

SYSTEM = f"You are a coding agent at {WORKDIR}. Use tools to solve tasks. Act, don't explain."


def safe_path(p: str) -> Path:
    path = (WORKDIR / p).resolve()
    if not path.is_relative_to(WORKDIR):
        raise ValueError(f"Path escapes workspace: {p}")
    return path


def run_bash(command: str) -> str:
    dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
    if any(d in command for d in dangerous):
        return "Error: Dangerous command blocked"
    try:
        r = subprocess.run(command, shell=True, cwd=WORKDIR,
                           capture_output=True, text=True,
                           encoding="utf-8", errors="replace", timeout=120)
        out = (r.stdout + r.stderr).strip()
        return out[:50000] if out else "(no output)"
    except subprocess.TimeoutExpired:
        return "Error: Timeout (120s)"


def run_read(path: str, limit: int = None) -> str:

View on GitHub (pinned to 985456f4ad)

Solutions

  1. Call file tools with paths relative to the workspace root, e.g. "src/main.py" not "/home/user/proj/src/main.py"
  2. If the path is absolute but inside the workspace, strip the WORKDIR prefix before passing it (os.path.relpath(p, WORKDIR))
  3. Make the system prompt tell the model explicitly that tool paths must be relative to the stated workspace directory
  4. Launch the agent with cwd set to the actual workspace so WORKDIR matches where files live
  5. If symlinks inside the workspace legitimately point outside, replace the sandbox with an allowlist of permitted roots instead of widening the check blindly

Example fix

// before (tool call)
{"op": "read", "path": "/home/user/proj/src/main.py"}
// raises ValueError: Path escapes workspace

// after (tool call)
{"op": "read", "path": "src/main.py"}

// after (harness-side normalization before calling safe_path)
import os
rel = os.path.relpath(os.path.abspath(p), WORKDIR)
path = safe_path(rel)
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def is_in_workspace(p: str, workdir: Path) -> bool:
    try:
        return (workdir / p).resolve().is_relative_to(workdir)
    except (OSError, RuntimeError):
        return False

# before any file tool call:
assert is_in_workspace(user_path, WORKDIR), f"{user_path} escapes workspace"

Type guard

from pathlib import Path

def is_workspace_relative_path(p: str, workdir: Path) -> bool:
    """True iff p joins+resolves inside workdir (no escape via .., abs, or symlink)."""
    if not isinstance(p, str) or not p:
        return False
    try:
        return (workdir / p).resolve().is_relative_to(workdir)
    except (OSError, RuntimeError):
        return False

Try / catch

try:
    path = safe_path(p)
except ValueError as e:
    # feed the rejection back to the model as a tool error with a hint
    return f"Tool error: {e}. Pass a path relative to {WORKDIR} (no leading /, no ..)."

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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