can1357/oh-my-pi · error · RuntimeError
#{scheme}:// path escapes its root: #{path}
Error message
#{scheme}:// path escapes its root: #{path} What it means
After building the absolute candidate `File.absolute_path(File.join(root_path, relative))`, `__omp_resolve_path` verifies the result is still inside the configured scheme root (equal to it or under it with a separator). This is a final containment check that catches symlinks or normalization quirks the earlier lexical `..` check missed; if the resolved path escapes the root, the helper refuses to proceed for `read`/`write`.
Source
Thrown at packages/coding-agent/src/eval/rb/prelude.rb:52
scheme = m[1].downcase
roots =
begin
raw = ENV["PI_EVAL_LOCAL_ROOTS"]
raw && !raw.empty? ? JSON.parse(raw) : {}
rescue StandardError
{}
end
root = roots.is_a?(Hash) ? roots[scheme] : nil
raise "Protocol paths are not supported by this helper: #{path}" if root.nil? || root.to_s.empty?
relative = __omp_url_decode(m[2].tr("\\", "/"))
root_path = File.absolute_path(root.to_s)
return root_path if relative.empty?
if relative.start_with?("/") || relative.split("/").include?("..")
raise "Unsafe #{scheme}:// path (absolute or traversal): #{path}"
end
resolved = File.absolute_path(File.join(root_path, relative))
unless resolved == root_path || resolved.start_with?(root_path + File::SEPARATOR)
raise "#{scheme}:// path escapes its root: #{path}"
end
resolved
end
# -------------------------------------------------------------------------
# Display + status
# -------------------------------------------------------------------------
def display(value)
__omp_present(value, "display")
nil
end
# Emit a base64 image as a display output. `mime_type` is "image/png" (default)
# or "image/jpeg"; the host surfaces it as an inspectable image block.
def display_image(base64, mime_type: "image/png")
__omp_emit_display({ mime_type.to_s => base64.to_s })
nilView on GitHub (pinned to 9690622007)
Solutions
- Remove or repoint the symlink inside the root that resolves outside it, then retry the read/write
- Check PI_EVAL_LOCAL_ROOTS for the scheme and confirm the root itself is an absolute, canonical directory (`File.realpath`); fix the configured value if it is relative or unusual
- If the target is legitimately outside the root, copy/link it into the root or use an allowed plain filesystem path instead
Defensive patterns
Strategy: try-catch
Try / catch
begin
data = read("local://link/out.txt")
rescue RuntimeError => e
raise unless e.message.include?("escapes its root")
# inspect the symlink inside the root: File.realpath("#{root}/link")
fallback = read_absolute_if_allowed(real_target)
end Prevention
- Audit the scheme root for symlinks pointing outside it (find root -type l)
- Configure PI_EVAL_LOCAL_ROOTS with canonical absolute paths (File.realpath output)
- Treat any occurrence as evidence the sandbox root is not self-contained
When it happens
Trigger: A `scheme://` path whose lexical segments are clean but whose resolution lands outside the root — most commonly a symlink inside the root pointing to an outside target (e.g. root contains `link -> /etc` and you call `read("local://link/passwd")`), or a root path with trailing-slash/normalization edge cases such that `File.absolute_path(root)` and the joined result diverge.
Common situations: Eval sandboxes where the local root contains symlinks created by setup scripts; roots given with relative paths or `~` expansions that normalize differently on the two sides of the comparison; writing through a symlink placed in the output directory.
Related errors
- ${scheme}:// path escapes its root: ${rawPath}
- {scheme}:// path escapes its root: {path}
- Unsafe #{scheme}:// path (absolute or traversal): #{path}
- local:// URL escapes local root
- Path traversal is not allowed
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/e468722bc6c9dfa8.
Report an issue: GitHub.