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

  1. Strip any leading slashes and `.`/`..` segments from the path portion before passing it, e.g. `path.delete_prefix('/').split('/').reject { |s| s == '..' || s == '.' }`
  2. Resolve the path yourself relative to the root declared in PI_EVAL_LOCAL_ROOTS and pass only the clean relative portion
  3. 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
  4. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/e4f11719cc76033d. Report an issue: GitHub.