can1357/oh-my-pi · error · RuntimeError
Unsafe #{scheme}:// path (absolute or traversal): #{path}
Error message
Unsafe #{scheme}:// path (absolute or traversal): #{path} What it means
`__omp_resolve_path` is the prelude's scheme:// path rewriter used by `read`/`write`: paths like `local://foo/bar.txt` are mapped onto an on-disk root injected via PI_EVAL_LOCAL_ROOTS. This error is the sanitizer's hard stop — it throws when the portion after `scheme://` looks like an absolute path (`/etc/passwd`) or contains a `..` segment, either of which could read or write outside the configured root.
Source
Thrown at packages/coding-agent/src/eval/rb/prelude.rb:48
def __omp_resolve_path(path)
return path unless path.is_a?(String)
m = path.match(%r{\A([a-z][a-z0-9+.\-]*)://(.*)\z}i)
return path unless m
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)View on GitHub (pinned to 9690622007)
Solutions
- Strip any leading slashes and `.`/`..` segments from the path portion before passing it, e.g. `path.delete_prefix('/').split('/').reject { |s| s == '..' || s == '.' }`
- Resolve the path yourself relative to the root declared in PI_EVAL_LOCAL_ROOTS and pass only the clean relative portion
- If the file genuinely lives outside the root, read/write it via a plain (non-scheme) path if permitted, or extend PI_EVAL_LOCAL_ROOTS to a root that contains it
- URL-encode deliberately if the segment is meant literally, but note `..` and leading `/` are rejected after decoding, so encoding cannot bypass this check
Example fix
// before
read("local://../../etc/passwd")
// after
safe = "../../etc/passwd".split('/').reject { |p| p == '..' || p == '.' || p.empty? }
read("local://#{safe.join('/')}") Defensive patterns
Strategy: validation
Validate before calling
def scheme_path_safe?(path)
return true unless path.is_a?(String)
m = path.match(/\A([a-z][a-z0-9+.\-]*):\/\/(.*)\z/i)
return true unless m
rel = m[2].tr("\\", "/").gsub(/%([0-9A-Fa-f]{2})/) { [Regexp.last_match(1)].pack("H2") }
!rel.start_with?("/") && !rel.split("/").include?("..")
end
# call: read(path) only if scheme_path_safe?(path) Prevention
- Never concatenate "scheme://" with user/agent-supplied absolute paths — normalize to a relative path first
- Reject or sanitize any segment equal to '..' and strip leading slashes before forming scheme paths
- Remember percent-encoding is decoded before checks — do not rely on %2e%2e to bypass validation
- Keep PI_EVAL_LOCAL_ROOTS pointing at a dedicated sandbox directory
When it happens
Trigger: Calling `read("local:///etc/passwd")` or `read("local://../../secrets.txt")` (or the equivalent via `write`), or any path where percent-decoding (after backslash-to-slash normalization) yields a leading `/` or a `..` segment, e.g. `read("local://%2e%2e/x")`.
Common situations: An LLM-generated or user-supplied path keeps its absolute form after being written as `scheme://` + path; string concatenation like `"local://" + user_path` where `user_path` starts with `/`; traversal attempts embedded in encoded segments (`%2e%2e`); joining with `File.join(root, "/abs")`-style inputs.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- Destination paths cannot contain parent traversal or NUL byt
- Shared-folder destination escapes its configured root
- Absolute paths are not allowed in ${scheme}:// URLs: ${rawPa
- Path traversal (..) is not allowed in ${scheme}:// URLs: ${r
- #{scheme}:// path escapes its root: #{path}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/e4f11719cc76033d.
Report an issue: GitHub.