different-ai/openwork · error · ApiError
plugin_ambiguous
plugin_ambiguous
Error message
Multiple plugins found (${candidates}). Add the plugin directory to the URL, e.g. /tree/main/<dir>. What it means
When no dir is given and locatePluginRoot finds multiple .claude-plugin/plugin.json files at the same shallowest depth, the plugin is ambiguous — it cannot pick one automatically. It throws this 400 ApiError listing the candidate directories and instructs the caller to add the plugin directory to the URL.
Source
Thrown at apps/server/src/claude-plugin-bundle.ts:225
const normalized = dir.replace(/\/+$/, "");
const expected = `${normalized}/.claude-plugin/plugin.json`;
if (!manifestPaths.includes(expected)) {
throw new ApiError(404, "plugin_manifest_not_found", `No .claude-plugin/plugin.json found under ${normalized}/`);
}
return `${normalized}/`;
}
if (manifestPaths.length === 0) {
throw new ApiError(404, "plugin_manifest_not_found", "No .claude-plugin/plugin.json found in this repository");
}
manifestPaths.sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b));
const shallowest = manifestPaths[0]!;
const root = shallowest.slice(0, shallowest.length - ".claude-plugin/plugin.json".length);
const sameDepth = manifestPaths.filter((path) => path.split("/").length === shallowest.split("/").length);
if (sameDepth.length > 1) {
const candidates = sameDepth
.map((path) => path.slice(0, path.length - "/.claude-plugin/plugin.json".length))
.join(", ");
throw new ApiError(400, "plugin_ambiguous", `Multiple plugins found (${candidates}). Add the plugin directory to the URL, e.g. /tree/main/<dir>.`);
}
return root;
}
function readString(value: unknown): string | null {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
function readPathList(value: unknown): string[] {
if (typeof value === "string") return value.trim() ? [value.trim()] : [];
if (Array.isArray(value)) return value.flatMap((entry) => (typeof entry === "string" && entry.trim() ? [entry.trim()] : []));
return [];
}
function normalizeRelative(root: string, path: string): string {
const cleaned = path.replace(/^\.\//, "").replace(/^\/+/, "");
if (cleaned.split("/").some((part) => part === "..")) return "";
return `${root}${cleaned}`;View on GitHub (pinned to 2b7df46e8a)
Solutions
- Append the desired plugin directory to the URL as the message suggests: /tree/main/<dir>.
- Enumerate the candidates listed in the message and pick the intended one.
- If you meant to install all plugins, install each with its own URL.
- For stable automation, always include the <dir> segment in monorepos.
Example fix
// before GET /plugin/tree/main // ambiguous: plugins/foo, plugins/bar // after GET /plugin/tree/main/plugins/foo
Defensive patterns
Strategy: validation
Validate before calling
const tree = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/${ref}?recursive=1`).then(r => r.json());
const roots = (tree.tree ?? []).map((e: { path: string }) => e.path).filter((p: string) => p.endsWith("/.claude-plugin/plugin.json"));
if (roots.length > 1) throw new Error(`ambiguous: ${roots.join(", ")} — include /<dir>`); Try / catch
try {
await installPlugin(url);
} catch (e) {
if (isApiError(e) && e.code === "plugin_ambiguous") {
// parse candidates from e.message and retry with chosen /<dir>
}
} Prevention
- Always include the plugin directory in URLs for monorepos.
- Parse the candidate list from the error message to select the right dir.
- Script installs should enumerate plugins and install each explicitly.
- Re-test monorepo URLs after upstream adds new plugins.
When it happens
Trigger: Requesting /plugin/tree/<ref> against a monorepo containing two or more sibling plugin roots (e.g. plugins/a/.claude-plugin/plugin.json and plugins/b/.claude-plugin/plugin.json) without specifying which plugin to install.
Common situations: Monorepos bundling several plugins; installing from an org's plugins collection repo; a repo that recently gained a second plugin so previously working URLs became ambiguous.
Related errors
- plugin_ref_not_found
- plugin_manifest_not_found
- OpenWork-managed OAuth requires a remote MCP URL.
- invalid_plugin_manifest
- plugin_empty
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/62940573b38c3aea.
Report an issue: GitHub.