can1357/oh-my-pi · error · ToolError
${scheme}:// path escapes its root: ${rawPath}
Error message
${scheme}:// path escapes its root: ${rawPath} What it means
As a final confinement check, `resolveUnderRoot` resolves the normalized path against the root and verifies the result still lies inside it (`resolved === rootPath` or starts with `rootPath + sep`). If the resolved path lands outside — e.g. via symlinks or root-edge tricks that earlier textual checks miss — this ToolError is thrown.
Source
Thrown at packages/coding-agent/src/eval/js/shared/helpers.ts:141
function resolveUnderRoot(scheme: string, root: string, rawRelative: string, rawPath: string): string {
let relative: string;
try {
relative = decodeURIComponent(rawRelative.replaceAll("\\", "/"));
} catch {
throw new ToolError(`Invalid URL encoding in ${scheme}:// path: ${rawPath}`);
}
const rootPath = path.resolve(root);
if (relative === "") return rootPath;
if (path.isAbsolute(relative)) {
throw new ToolError(`Absolute paths are not allowed in ${scheme}:// URLs: ${rawPath}`);
}
const normalized = path.normalize(relative);
if (normalized.startsWith("..") || normalized.includes("/../") || normalized.includes("/..")) {
throw new ToolError(`Path traversal (..) is not allowed in ${scheme}:// URLs: ${rawPath}`);
}
const resolved = path.resolve(rootPath, normalized);
if (resolved !== rootPath && !resolved.startsWith(`${rootPath}${path.sep}`)) {
throw new ToolError(`${scheme}:// path escapes its root: ${rawPath}`);
}
return resolved;
}
async function resolveRegularFile(
ctx: HelperContext,
rawPath: string,
): Promise<{ filePath: string; file: Bun.BunFile; size: number }> {
const filePath = resolveHelperPath(ctx, rawPath, "read");
const file = Bun.file(filePath);
const stat = await file.stat();
if (stat.isDirectory()) {
throw new ToolError(`Directory paths are not supported by read(): ${filePath}`);
}
return { filePath, file, size: stat.size };
}
function getDataSize(data: string | Blob | ArrayBuffer | ArrayBufferView): number {View on GitHub (pinned to 9690622007)
Solutions
- Use a plain file path relative to the root without edge segments (`.` or `..`).
- Ensure the localRoot passed by the host is a fully resolved, symlink-free directory (host-side fix: `path.realpathSync(root)`).
- Read the root directory itself via a dedicated listing helper if available rather than resolving the root edge path.
Example fix
// before
await read("local://.."); // resolves outside root
// after
await read("local://sub/dir/file.md"); Defensive patterns
Strategy: validation
Validate before calling
const root = path.resolve(mountedRoot);
const resolved = path.resolve(root, rel);
if (resolved !== root && !resolved.startsWith(root + path.sep)) throw new Error("escapes root"); Try / catch
try {
return await read(url);
} catch (err) {
if (String(err?.message).includes("escapes its root")) {
// root likely symlinked; ask host to re-mount with realpath or use plain path
}
throw err;
} Prevention
- Mount localRoots as realpath-resolved directories to avoid prefix mismatches.
- Avoid edge segments (`.`/`..`) in scheme URLs.
- Keep targets as direct descendants of the mounted root.
When it happens
Trigger: A `scheme://` relative path whose resolution escapes the mounted root despite passing the textual `..` checks — for instance `local://..` exactly at the root edge, or a relative path combined with a symlinked root resolved to a different prefix.
Common situations: Edge-case paths like `local://.` or `local://..` that survive string checks; roots mounted through symlinks so `path.resolve` produces a prefix differing from the configured root string.
Related errors
- Absolute paths are not allowed in ${scheme}:// URLs: ${rawPa
- Path traversal (..) is not allowed in ${scheme}:// URLs: ${r
- {scheme}:// path escapes its root: {path}
- #{scheme}:// path escapes its root: #{path}
- local:// URL escapes local root
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/e9ea5fbac75cbbc9.
Report an issue: GitHub.