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
- Fix the plugin's `path` in the monorepo manifest to a relative path that stays inside the repository.
- Remove leading '/' or '../' segments from the path.
- Use forward slashes and repo-relative paths so resolution works cross-platform.
- 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
- Use plain repo-relative paths (no leading '/', no '..') in monorepo manifests.
- Treat this error from third-party repos as a security signal — don't work around it.
- Test manifests with path.resolve in CI before publishing a plugin repo.
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
- output path must not be a symbolic link: ${resolved}
- Invalid skill path: ${relativePath}
- Invalid skill path: ${raw}
- Expected a trusted HTTPS bilibili.com video URL without cred
- HLTV parser returned an off-domain URL: ${url.toString()}
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/93f1821c4cc7952f.
Report an issue: GitHub.