{"record":{"id":"7c1c20ab06791bf6","repo":"different-ai/openwork","slug":"invalid-plugin-manifest","errorCode":"invalid_plugin_manifest","errorMessage":"${manifestPath} is not valid JSON","messagePattern":"(.+?) is not valid JSON","errorType":"validation","errorClass":"ApiError","httpStatus":400,"severity":"error","filePath":"apps/server/src/claude-plugin-bundle.ts","lineNumber":283,"sourceCode":"  return false;\n}\n\nexport async function resolveClaudePluginBundle(input: { url: string; ref?: string }): Promise<ClaudePluginBundle> {\n  const source = parseClaudePluginSource(input.url);\n  const { ref, dir, tree } = await resolveRefAndTree(source, input.ref?.trim() || undefined);\n  const root = locatePluginRoot(tree, dir);\n  const treeByPath = new Map(tree.map((entry) => [entry.path, entry]));\n  const warnings: string[] = [];\n\n  const manifestPath = `${root}.claude-plugin/plugin.json`;\n  const manifestText = await fetchGithubText(rawFileUrl(source, ref, manifestPath));\n  let manifest: Record<string, unknown>;\n  try {\n    const parsed: unknown = JSON.parse(manifestText);\n    if (!isRecord(parsed)) throw new Error(\"not an object\");\n    manifest = parsed;\n  } catch {\n    throw new ApiError(400, \"invalid_plugin_manifest\", `${manifestPath} is not valid JSON`);\n  }\n\n  const pluginName = readString(manifest.displayName) ?? readString(manifest.name);\n  if (!pluginName) {\n    throw new ApiError(400, \"invalid_plugin_manifest\", `${manifestPath} is missing a plugin name`);\n  }\n  const description = readString(manifest.description);\n  const version = readString(manifest.version);\n  if (manifest.hooks !== undefined) {\n    warnings.push(\"This plugin declares hooks, which OpenWork does not support yet. Hooks were skipped.\");\n  }\n\n  // --- Collect component file paths -----------------------------------------\n  const inTree = (path: string) => treeByPath.has(path);\n\n  const collectMarkdown = (declared: string[], defaultDir: string): string[] => {\n    const roots = declared.length > 0\n      ? declared.map((entry) => normalizeRelative(root, entry)).filter(Boolean)","sourceCodeStart":265,"sourceCodeEnd":301,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/apps/server/src/claude-plugin-bundle.ts#L265-L301","documentation":"After fetching the manifest text, resolveClaudePluginBundle parses it with JSON.parse and requires the result to be a plain object. Any parse failure (malformed JSON, trailing commas, HTML error page returned instead of JSON) or non-object result throws this 400 ApiError identifying the manifest path.","triggerScenarios":"The .claude-plugin/plugin.json in the repo is syntactically invalid JSON, contains comments/BOM, or the fetched text is not the manifest at all (e.g. a GitHub error page captured as text).","commonSituations":"Hand-edited manifest with a missing comma or trailing comma; manifest saved with comments (JSONC instead of JSON); wrong file committed with plugin.json name; encoding issues (BOM, smart quotes from editors).","solutions":["Validate the plugin.json locally with `node -e \"JSON.parse(require('fs').readFileSync('.claude-plugin/plugin.json'))\"` or a JSON linter.","Remove comments, trailing commas, BOM, and non-ASCII quotes; JSON does not allow JSONC syntax.","Confirm the file at that path/ref is the real manifest and not an HTML/error page.","Re-fetch after fixing and pushing the corrected manifest to the repo."],"exampleFix":"// before (.claude-plugin/plugin.json)\n{ \"name\": \"my-plugin\", }  // trailing comma\n// after\n{ \"name\": \"my-plugin\" }","handlingStrategy":"validation","validationCode":"import { readFileSync } from \"node:fs\";\nconst raw = readFileSync(\".claude-plugin/plugin.json\", \"utf8\").replace(/^\\uFEFF/, \"\");\nJSON.parse(raw); // throws locally before install if invalid","typeGuard":"function isJsonObject(v: unknown): v is Record<string, unknown> {\n  return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}","tryCatchPattern":"try {\n  await installPlugin(url);\n} catch (e) {\n  if (isApiError(e) && e.code === \"invalid_plugin_manifest\") {\n    // report the manifest path from e.message back to the plugin vendor\n  }\n}","preventionTips":["Lint plugin.json with a strict JSON parser in CI.","Never allow comments or trailing commas in manifests.","Strip BOM and ensure UTF-8 without smart quotes.","Fetch the manifest yourself and JSON.parse it as a pre-check."],"tags":["json","plugin-manifest","validation","plugin-install"],"backgroundTag":"invalid-json","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}