jackwener/OpenCLI · error · PluginError

Plugin path "${subPath}" escapes repo root.

Error message

Plugin path "${subPath}" escapes repo root.

What it means

resolveRepoContainedPath resolves a subPath against a repo root and refuses results that resolve outside the repo root (path traversal). opencli throws this as a security guard so a malicious or misconfigured monorepo manifest cannot point a plugin symlink at arbitrary filesystem locations.

Source

Thrown at src/plugin.ts:274

  if (!source || source.kind === 'local') {
    throw new Error(`Unable to determine remote source for plugin at ${dir}`);
  }
  return source.url;
}

function pathExistsSync(p: string): boolean {
  try {
    fs.lstatSync(p);
    return true;
  } catch {
    return false;
  }
}

function resolveRepoContainedPath(repoRoot: string, subPath: string): string {
  const resolved = path.resolve(repoRoot, subPath);
  if (!resolved.startsWith(repoRoot + path.sep) && resolved !== repoRoot) {
    throw new PluginError(`Plugin path "${subPath}" escapes repo root.`);
  }
  return resolved;
}

function removePathSync(p: string): void {
  try {
    const stat = fs.lstatSync(p);
    if (stat.isSymbolicLink()) {
      fs.unlinkSync(p);
      return;
    }
    fs.rmSync(p, { recursive: true, force: true });
  } catch {}
}

interface TransactionHandle {
  finalize(): void;
  rollback(): void;

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Fix the plugin's `path` in the monorepo manifest to a relative path that stays inside the repository.
  2. Remove leading '/' or '../' segments from the path.
  3. Use forward slashes and repo-relative paths so resolution works cross-platform.
  4. If a third-party repo triggers this intentionally, do not install it — this is a security rejection.

Example fix

// before (opencli-plugin.json)
{ "plugins": { "evil": { "path": "../../outside" } } }
// after
{ "plugins": { "good": { "path": "packages/good-plugin" } } }
Defensive patterns

Strategy: validation

Validate before calling

const path = require('node:path');
function pathStaysInside(repoRoot, subPath) {
  const resolved = path.resolve(repoRoot, subPath);
  return resolved === repoRoot || resolved.startsWith(repoRoot + path.sep);
}

Try / catch

try {
  installPlugin('github:user/monorepo/sub');
} catch (err) {
  if (err instanceof PluginError && err.message.includes('escapes repo root')) {
    // reject manifest / fix plugin path — do not bypass
  } else throw err;
}

Prevention

When it happens

Trigger: Called from installMonorepo, publishMonorepoPlugins, and updatePlugin with a manifest entry whose `path` is absolute, contains '../' segments that climb above the repo root, or resolves to the repo root itself via symlinks — e.g. plugins entry { path: "../../etc" } in opencli-plugin.json.

Common situations: A monorepo manifest with a mistyped plugin path (leading slash or extra ../); a malicious third-party plugin repo attempting path escape; Windows path separator mismatches; repo moved so relative paths no longer line up.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/93f1821c4cc7952f. Report an issue: GitHub.