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 })
    nil

View on GitHub (pinned to 9690622007)

Solutions

  1. Remove or repoint the symlink inside the root that resolves outside it, then retry the read/write
  2. 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
  3. 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

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


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