can1357/oh-my-pi · error · Error

Marketplace plugin package path escapes node_modules: ${JSON

Error message

Marketplace plugin package path escapes node_modules: ${JSON.stringify(packageName)}

What it means

MarketplaceManager.#runtimePackagePath() computes the symlink path for a plugin's package inside the scope's node_modules directory. It first validates the package name with assertRuntimePackageName(), then double-checks via path.relative() that the resolved link path stays inside node_modules. If the name would escape (e.g. contains "..", absolute paths, or is empty), it throws this path-traversal defense error.

Source

Thrown at packages/coding-agent/src/extensibility/plugins/marketplace/manager.ts:811

	}

	async #resolvePluginPackageName(installPath: string, fallbackName: string): Promise<string> {
		try {
			const pkg: { name?: unknown } = await Bun.file(path.join(installPath, "package.json")).json();
			const name = typeof pkg.name === "string" && pkg.name.length > 0 ? pkg.name : fallbackName;
			return assertRuntimePackageName(name);
		} catch (err) {
			if (isEnoent(err)) return assertRuntimePackageName(fallbackName);
			throw err;
		}
	}

	#runtimePackagePath(scope: "user" | "project", packageName: string): string {
		const nodeModules = path.resolve(this.#nodeModulesPath(scope));
		const linkPath = path.resolve(nodeModules, assertRuntimePackageName(packageName));
		const relative = path.relative(nodeModules, linkPath);
		if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) {
			throw new Error(`Marketplace plugin package path escapes node_modules: ${JSON.stringify(packageName)}`);
		}
		return linkPath;
	}

	async #resolveInstalledPackageNames(
		entries: readonly InstalledPluginEntry[],
		fallbackName: string,
	): Promise<Set<string>> {
		const packageNames = new Set<string>();
		for (const entry of entries) {
			packageNames.add(await this.#resolvePluginPackageName(entry.installPath, fallbackName));
		}
		return packageNames;
	}

	async #registerRuntimePlugin(
		scope: "user" | "project",
		packageName: string,

View on GitHub (pinned to 9690622007)

Solutions

  1. Inspect the installed-plugins registry / marketplace catalog entry and fix the packageName field to a bare npm-style name
  2. Remove and reinstall the plugin so the registry is rewritten with a valid name
  3. Never hand-edit registry JSON — use the marketplace CLI commands to mutate state
  4. If a third-party marketplace produced this, report/remove that marketplace entry

Example fix

// before (registry entry)
{"packageName": "../../evil"}
// after
{"packageName": "my-plugin-pkg"}
Defensive patterns

Strategy: validation

Validate before calling

import * as path from 'node:path';
function isSafePackageName(name) {
  return typeof name === 'string' && /^[a-z0-9@][a-z0-9._\-/]*$/i.test(name) &&
    !name.includes('..') && !path.isAbsolute(name);
}

Type guard

const isSafePkgName = (v) => typeof v === 'string' && v.length > 0 && !v.includes('..') && !path.isAbsolute(v);

Try / catch

try {
  const linkPath = manager.resolveRuntimePackagePath(scope, pkg);
} catch (err) {
  if (err.message.includes('escapes node_modules')) {
    console.error(`Refusing unsafe package name ${pkg} — registry entry may be corrupted`);
  } else throw err;
}

Prevention

When it happens

Trigger: A marketplace catalog or installed entry contains a malicious or corrupt packageName such as "../../evil", an absolute path like "/etc", or an empty string; a manually edited installed-plugins.json with a tampered package field.

Common situations: Hand-edited or corrupted registry files; a malicious/misconfigured marketplace publishing a plugin entry with traversal in its package name; symlink-resolution oddities after moving project directories.

Related errors


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