{"record":{"id":"9c53536fae066f26","repo":"shareAI-lab/learn-claude-code","slug":"path-escapes-the-current-repository","errorCode":null,"errorMessage":"path escapes the current repository","messagePattern":"path escapes the current repository","errorType":"validation","errorClass":"GoalError","httpStatus":null,"severity":"error","filePath":"s17_goal_loop/code.py","lineNumber":753,"sourceCode":"                            f\"Evaluator: {decision.reason}\\n\"\n                            \"Continue working and surface the missing evidence.\"\n                        ),\n                    }\n                )\n                continue\n            self.trigger_hooks(\"Stop\", self.messages)\n            return SessionResult(\n                text=text,\n                status=decision.action,\n                reason=decision.reason,\n            )\n\n    def _safe_path(self, path: str) -> Path:\n        candidate = (self.workdir / path).resolve()\n        try:\n            candidate.relative_to(self.workdir)\n        except ValueError as error:\n            raise GoalError(\"path escapes the current repository\") from error\n        return candidate\n\n    def _run_tool(self, name: str, arguments: dict[str, Any]) -> str:\n        if name == \"bash\":\n            command = str(arguments[\"command\"])\n            result = subprocess.run(\n                command,\n                shell=True,\n                cwd=self.workdir,\n                capture_output=True,\n                text=True,\n                timeout=120,\n                check=False,\n            )\n            output = (result.stdout + result.stderr).strip()\n            output = output[-29950:]\n            return f\"exit_code={result.returncode}\\n{output}\"\n","sourceCodeStart":735,"sourceCodeEnd":771,"githubUrl":"https://github.com/shareAI-lab/learn-claude-code/blob/985456f4adea6f4df8fbad4112245dbd97444eae/s17_goal_loop/code.py#L735-L771","documentation":"AgentSession._safe_path resolved a tool-supplied relative path (workdir / path, then .resolve()) and found it does not lie inside self.workdir (s17_goal_loop/code.py:753). The resolution step follows symlinks and '../' segments, so this fires on sandbox escapes like '../../etc/passwd', absolute-path tricks after joining, or a symlink inside the repo pointing outside.","triggerScenarios":"A tool call (read/write/edit) from the model passes a path containing '..' that escapes the repo root, or a path that resolves through a symlink to a location outside workdir. For example read_file with '../../~/.ssh/config' while workdir is the repo.","commonSituations":"The model tries to read config or logs outside the project (e.g. ~/.gitconfig, /tmp artifacts); the repo contains a symlink (node_modules-style links, linked assets) to an external directory; generated code passes absolute paths which get joined and resolve outside root.","solutions":["If the access is legitimate, copy the needed file into the repo or run the session with a workdir that contains it","Rephrase the task so the model only references in-repo paths; add the constraint to the system/task prompt","Remove or relocate symlinks inside the repo that point outside the root","Catch GoalError around session runs and treat it as a tool-misuse signal: log and let the agent retry with a corrected path"],"exampleFix":"# before\n# model tool call: read_file ../../~/.gitconfig\n\n# after\n# copy the file into the repo first, then let the model read it\ncp ~/.gitconfig ./.gitconfig.reference\n# model tool call: read_file .gitconfig.reference","handlingStrategy":"try-catch","validationCode":"def resolves_inside(workdir: Path, relative: str) -> bool:\n    try:\n        (workdir / relative).resolve().relative_to(workdir.resolve())\n        return True\n    except ValueError:\n        return False\n\nif not resolves_inside(session.workdir, tool_args.get(\"path\", \"\")):\n    # reject or rewrite the path before dispatching the tool","typeGuard":"def is_safe_relative_path(workdir: Path, path: object) -> bool:\n    if not isinstance(path, str) or not path:\n        return False\n    try:\n        (workdir / path).resolve().relative_to(workdir.resolve())\n        return True\n    except ValueError:\n        return False","tryCatchPattern":"try:\n    await session.submit(query)\nexcept GoalError as error:\n    if \"escapes the current repository\" in str(error):\n        log.warning(\"tool attempted out-of-repo path; retrying with constrained prompt\")\n        continue  # let the agent see the error and choose an in-repo path\n    raise","preventionTips":["Tell the model in the system prompt that only in-repo paths are readable/writable","Audit the repo for symlinks pointing outside the root before running sessions","Treat this error as a tool-misuse signal to feed back to the agent, not a crash"],"tags":["goal-loop","security","path-traversal","sandbox"],"backgroundTag":null,"analyzedSha":"985456f4adea6f4df8fbad4112245dbd97444eae","analyzedAt":"2026-08-14T22:02:26.028Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}