can1357/oh-my-pi · error · Error

Plugin "${entry.name}" lspServers path escapes the plugin di

Error message

Plugin "${entry.name}" lspServers path escapes the plugin directory

What it means

When a plugin's lspServers field is a string, it is treated as a path to an LSP config file resolved relative to the installed plugin directory (cachePath). #writeEmbeddedLspConfig validates with pathIsWithin that the resolved path stays inside the plugin directory, blocking path traversal out of the plugin sandbox. A malicious or buggy plugin manifest referencing ../../secrets/lsp.json is rejected.

Source

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

			}
		}
		await this.#registerRuntimePlugin(scope, packageName, cachePath, version, wasDisabled ? false : undefined);

		this.#clearCache();

		logger.debug("Plugin installed", { pluginId, version, cachePath });
		return installedEntry;
	}

	async #writeEmbeddedLspConfig(entry: MarketplacePluginEntry, cachePath: string): Promise<void> {
		const lspServers = entry.lspServers;
		if (!lspServers) return;

		const targetPath = path.join(cachePath, ".lsp.json");
		if (typeof lspServers === "string") {
			const sourcePath = path.resolve(cachePath, lspServers);
			if (!pathIsWithin(cachePath, sourcePath)) {
				throw new Error(`Plugin "${entry.name}" lspServers path escapes the plugin directory`);
			}
			const content = await Bun.file(sourcePath).text();
			await Bun.write(targetPath, content);
			return;
		}

		await Bun.write(targetPath, `${JSON.stringify({ servers: lspServers }, null, 2)}\n`);
	}

	async #writeEmbeddedDapConfig(entry: MarketplacePluginEntry, cachePath: string): Promise<void> {
		const dapAdapters = entry.dapAdapters;
		if (!dapAdapters) return;

		if (typeof dapAdapters === "string") {
			const sourcePath = path.resolve(cachePath, dapAdapters);
			if (!pathIsWithin(cachePath, sourcePath)) {
				throw new Error(`Plugin "${entry.name}" dapAdapters path escapes the plugin directory`);
			}

View on GitHub (pinned to 9690622007)

Solutions

  1. Fix the plugin's lspServers field to point at a file inside the plugin directory (relative path within it)
  2. Inline the LSP server config directly in the plugin entry instead of referencing an external file
  3. If sharing config across plugins, duplicate the file into each plugin directory or restructure the marketplace so each plugin owns its config
  4. Contact the plugin maintainer to republish with a contained path

Example fix

// before (catalog entry)
"lspServers": "../shared/lsp.json"
// after
"lspServers": "./lsp.json"  // file placed inside this plugin's directory
Defensive patterns

Strategy: validation

Validate before calling

import * as path from "node:path";
function lspPathIsContained(cachePath: string, lspServers: string | object | undefined): boolean {
  if (typeof lspServers !== "string") return true;
  const resolved = path.resolve(cachePath, lspServers);
  return resolved === cachePath || resolved.startsWith(cachePath + path.sep);
}
if (!lspPathIsContained(cachePath, entry.lspServers)) throw new Error("lspServers escapes plugin dir");

Type guard

function isContainedLspServers(v: unknown, cachePath: string): v is string {
  return typeof v === "string" && (path.resolve(cachePath, v) === cachePath || path.resolve(cachePath, v).startsWith(cachePath + path.sep));
}

Try / catch

try {
  await manager.installPlugin(name, marketplace);
} catch (err) {
  if (err instanceof Error && err.message.includes("lspServers path escapes")) {
    logger.error("Refusing plugin: insecure lspServers path", { name, marketplace });
  } else throw err;
}

Prevention

When it happens

Trigger: Installing (installPlugin) a marketplace plugin whose catalog entry sets lspServers to a string path that, after path.resolve against cachePath, escapes the plugin directory (e.g. "../shared/lsp.json", "/etc/lsp.json", or a symlinked path outside).

Common situations: Plugin authors using ../ to share one LSP config across sibling plugins in a monorepo-style marketplace; absolute paths in manifests that were valid on the author's machine; symlinks inside the plugin dir pointing outward.

Related errors


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