can1357/oh-my-pi · error · Error
git-subdir path "${source.path}" escapes the cloned reposito
Error message
git-subdir path "${source.path}" escapes the cloned repository What it means
Thrown by resolveObjectSource for a git-subdir plugin source: after cloning the repository into a temp directory, the resolver resolves source.path against the clone root and rejects it if the result is not contained within the clone. Before throwing, it cleans up the temp clone (fs.rm recursive/force) so no partial clone is left behind. This is a containment guard against path traversal — a catalog entry must not be able to pull a plugin directory from outside the repository it declares.
Source
Thrown at packages/coding-agent/src/extensibility/plugins/marketplace/source-resolver.ts:127
}
case "git-subdir": {
// { source: "git-subdir", url: "owner/repo" | "https://...", path: "plugins/foo" }
const url =
source.url.includes("://") || source.url.startsWith("git@")
? source.url
: `https://github.com/${source.url}.git`;
const cloneDir = path.join(context.tmpDir, `plugin-repo-${crypto.randomUUID()}`);
await vcs.clone(url, cloneDir, {
refName: source.ref,
sha: source.sha,
timeoutMs: GIT_CLONE_TIMEOUT_MS,
});
const subdirPath = path.resolve(cloneDir, source.path);
if (!pathIsWithin(cloneDir, subdirPath)) {
await fs.rm(cloneDir, { recursive: true, force: true });
throw new Error(`git-subdir path "${source.path}" escapes the cloned repository`);
}
try {
await verifyDirExists(subdirPath, `git-subdir path "${source.path}" does not exist in cloned repository`);
} catch (err) {
await fs.rm(cloneDir, { recursive: true, force: true });
throw err;
}
return { dir: subdirPath, tempCloneRoot: cloneDir };
}
case "npm":
throw new Error("npm plugin sources are not yet supported. Use git-based sources instead.");
default:
throw new Error(`Unknown plugin source type: "${(source as { source: string }).source}"`);
}
}
View on GitHub (pinned to 9690622007)
Solutions
- Edit the catalog entry so source.path is a repository-relative directory that stays inside the clone, e.g. { "source": "git-subdir", "url": "owner/repo", "path": "plugins/foo" } — remove ".." segments and any leading "/".
- Confirm the subdirectory actually exists at the ref/sha being cloned; fix ref/sha if the layout changed between versions.
- Check the cloned repo for symlinks whose targets live outside the repository and replace or remove them.
- If the plugin genuinely lives in a different repository, point the source at that repo directly (source "url" or "github") instead of trying to escape via path.
Example fix
// before (marketplace.json entry)
{ "source": "git-subdir", "url": "owner/monorepo", "path": "../shared-plugin" }
// after
{ "source": "git-subdir", "url": "owner/monorepo", "path": "packages/shared-plugin" } Defensive patterns
Strategy: validation
Validate before calling
import * as path from "node:path";
function gitSubdirPathIsSafe(p: string): boolean {
return typeof p === "string" && p.length > 0 && !path.isAbsolute(p)
&& p.split(/[\\/]/).every(seg => seg !== "..");
}
// run before calling resolvePluginSource:
// if (source.source === "git-subdir" && !gitSubdirPathIsSafe(source.path)) reject(entry); Type guard
function isGitSubdirSource(source: unknown): source is { source: "git-subdir"; url: string; path: string; ref?: string; sha?: string } {
return typeof source === "object" && source !== null
&& (source as { source?: unknown }).source === "git-subdir"
&& typeof (source as { url?: unknown }).url === "string"
&& typeof (source as { path?: unknown }).path === "string";
} Try / catch
try {
const { dir, tempCloneRoot } = await resolvePluginSource(entry, context);
} catch (err) {
if (err instanceof Error && err.message.includes("escapes the cloned repository")) {
// fix or reject the catalog entry; the temp clone was already cleaned up by the resolver
} else throw err;
} Prevention
- Write git-subdir paths as clone-relative with no ".." segments and no leading "/"
- Pin ref/sha and confirm the subdir exists at that revision before publishing the catalog
- Validate catalog entries with a schema rejecting traversal segments in git-subdir paths
- Avoid symlinks in plugin repos that point outside the repository
When it happens
Trigger: resolvePluginSource is called with an object source of the form { source: "git-subdir", url: ..., path: ... } and path.resolve(cloneDir, source.path) lands outside the freshly cloned repository: (1) path contains ".." segments that climb out of the clone, e.g. "../../other-plugin"; (2) path is an absolute path like "/opt/plugins/foo" (path.resolve discards cloneDir entirely); (3) the ref/sha that was cloned moved or a symlink inside the repo redirects outside (symlinked path components resolving elsewhere).
Common situations: A marketplace catalog entry copied from another repo with a path written relative to a different layout; typos like "../plugins/foo" in the git-subdir path; a compromised or malicious marketplace publishing traversal paths to exfiltrate/load arbitrary directories; using an absolute path because the author misunderstood that path is clone-relative.
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
- Plugin "${entry.name}" lspServers path escapes the plugin di
- Plugin "${entry.name}" dapAdapters path escapes the plugin d
- Plugin source "${source}" resolves outside marketplace root
- Destination paths cannot contain parent traversal or NUL byt
- Shared-folder destination escapes its configured root
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/4b702e76360f7186.
Report an issue: GitHub.