{"record":{"id":"62940573b38c3aea","repo":"different-ai/openwork","slug":"plugin-ambiguous","errorCode":"plugin_ambiguous","errorMessage":"Multiple plugins found (${candidates}). Add the plugin directory to the URL, e.g. /tree/main/<dir>.","messagePattern":"Multiple plugins found \\((.+?)\\)\\. Add the plugin directory to the URL, e\\.g\\. /tree/main/<dir>\\.","errorType":"error_code","errorClass":"ApiError","httpStatus":400,"severity":"error","filePath":"apps/server/src/claude-plugin-bundle.ts","lineNumber":225,"sourceCode":"    const normalized = dir.replace(/\\/+$/, \"\");\n    const expected = `${normalized}/.claude-plugin/plugin.json`;\n    if (!manifestPaths.includes(expected)) {\n      throw new ApiError(404, \"plugin_manifest_not_found\", `No .claude-plugin/plugin.json found under ${normalized}/`);\n    }\n    return `${normalized}/`;\n  }\n  if (manifestPaths.length === 0) {\n    throw new ApiError(404, \"plugin_manifest_not_found\", \"No .claude-plugin/plugin.json found in this repository\");\n  }\n  manifestPaths.sort((a, b) => a.split(\"/\").length - b.split(\"/\").length || a.localeCompare(b));\n  const shallowest = manifestPaths[0]!;\n  const root = shallowest.slice(0, shallowest.length - \".claude-plugin/plugin.json\".length);\n  const sameDepth = manifestPaths.filter((path) => path.split(\"/\").length === shallowest.split(\"/\").length);\n  if (sameDepth.length > 1) {\n    const candidates = sameDepth\n      .map((path) => path.slice(0, path.length - \"/.claude-plugin/plugin.json\".length))\n      .join(\", \");\n    throw new ApiError(400, \"plugin_ambiguous\", `Multiple plugins found (${candidates}). Add the plugin directory to the URL, e.g. /tree/main/<dir>.`);\n  }\n  return root;\n}\n\nfunction readString(value: unknown): string | null {\n  return typeof value === \"string\" && value.trim() ? value.trim() : null;\n}\n\nfunction readPathList(value: unknown): string[] {\n  if (typeof value === \"string\") return value.trim() ? [value.trim()] : [];\n  if (Array.isArray(value)) return value.flatMap((entry) => (typeof entry === \"string\" && entry.trim() ? [entry.trim()] : []));\n  return [];\n}\n\nfunction normalizeRelative(root: string, path: string): string {\n  const cleaned = path.replace(/^\\.\\//, \"\").replace(/^\\/+/, \"\");\n  if (cleaned.split(\"/\").some((part) => part === \"..\")) return \"\";\n  return `${root}${cleaned}`;","sourceCodeStart":207,"sourceCodeEnd":243,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/apps/server/src/claude-plugin-bundle.ts#L207-L243","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nGET /plugin/tree/main        // ambiguous: plugins/foo, plugins/bar\n// after\nGET /plugin/tree/main/plugins/foo","handlingStrategy":"validation","validationCode":"const tree = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/${ref}?recursive=1`).then(r => r.json());\nconst roots = (tree.tree ?? []).map((e: { path: string }) => e.path).filter((p: string) => p.endsWith(\"/.claude-plugin/plugin.json\"));\nif (roots.length > 1) throw new Error(`ambiguous: ${roots.join(\", \")} — include /<dir>`);","typeGuard":null,"tryCatchPattern":"try {\n  await installPlugin(url);\n} catch (e) {\n  if (isApiError(e) && e.code === \"plugin_ambiguous\") {\n    // parse candidates from e.message and retry with chosen /<dir>\n  }\n}","preventionTips":["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."],"tags":["plugin-install","ambiguity","github","url"],"backgroundTag":"ambiguous-resource-selector","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}