{"record":{"id":"d04b278eab8450cf","repo":"paperclipai/paperclip","slug":"plugin-ui-directory-not-found","errorCode":null,"errorMessage":"Plugin UI directory not found","messagePattern":"Plugin UI directory not found","errorType":"http","errorClass":null,"httpStatus":404,"severity":"error","filePath":"server/src/routes/plugin-ui-static.ts","lineNumber":420,"sourceCode":"      }\n    } catch {\n      // Config lookup failure is non-fatal — fall through to static serving\n    }\n\n    // Step 3: Resolve the plugin's UI directory\n    const uiDir = resolvePluginUiDir(\n      options.localPluginDir,\n      plugin.packageName,\n      manifest.entrypoints.ui,\n      plugin.packagePath,\n    );\n\n    if (!uiDir) {\n      log.warn(\n        { pluginId: plugin.id, pluginKey: plugin.pluginKey, packageName: plugin.packageName },\n        \"plugin-ui-static: UI directory not found on disk\",\n      );\n      res.status(404).json({ error: \"Plugin UI directory not found\" });\n      return;\n    }\n\n    // Step 4: Resolve the requested file path and prevent traversal (including symlinks)\n    const resolvedFilePath = path.resolve(uiDir, rawFilePath);\n\n    // Step 5: Check that the file exists and is a regular file\n    let fileStat: fs.Stats;\n    try {\n      fileStat = fs.statSync(resolvedFilePath);\n    } catch {\n      res.status(404).json({ error: \"File not found\" });\n      return;\n    }\n\n    // Security: resolve symlinks via realpathSync and verify containment.\n    // This prevents symlink-based traversal that string-based startsWith misses.\n    let realFilePath: string;","sourceCodeStart":402,"sourceCodeEnd":438,"githubUrl":"https://github.com/paperclipai/paperclip/blob/120ae5428fa29bee300bcf806491cd4d965fbb7c/server/src/routes/plugin-ui-static.ts#L402-L438","documentation":"Returned as HTTP 404 by GET /_plugins/:pluginId/ui/* (server/src/routes/plugin-ui-static.ts:420) when resolvePluginUiDir() cannot find the plugin's UI directory on disk. The resolver tries the persisted packagePath, then <localPluginDir>/node_modules/<packageName>, then <localPluginDir>/<packageName>, and finally <packageRoot>/<entrypoints.ui>; if none exists the route logs 'UI directory not found on disk' and 404s even though the plugin is 'ready' and declares a UI.","triggerScenarios":"Plugin marked ready and manifest declares entrypoints.ui='./dist/ui/', but the installed package has no dist/ui directory (build step skipped, .npmignore excluded it), the package was deleted from ~/.paperclip/plugins/node_modules, packagePath points to a moved/deleted local directory, or the server was started with a localPluginDir option different from the directory the plugin was installed into.","commonSituations":"Plugin published without building the UI bundle first; local-path installs after the source directory was moved or the worktree deleted; running multiple server instances with different plugin dirs; partial/corrupted npm installs during plugin upgrade.","solutions":["Verify the UI directory physically exists: ls <localPluginDir>/node_modules/<packageName>/dist/ui (or the packagePath location)","If missing, rebuild the plugin (npm run build producing dist/ui) and reinstall it so the registry re-points at a valid package","Confirm the server's localPluginDir option matches the directory used at install time (default ~/.paperclip/plugins/)","For local-path installs, make sure the plugin's packagePath still exists on this machine and contains the entrypoints.ui directory"],"exampleFix":"# before: package.json of the plugin publishes without building\n\"files\": [\"src\"]\n\n# after: include built UI output and build before publish\n\"scripts\": { \"prepublishOnly\": \"npm run build\" },\n\"files\": [\"dist\"]","handlingStrategy":"validation","validationCode":"import fs from \"node:fs\";\nimport path from \"node:path\";\n// Run after install, before marking the plugin ready\nconst uiDir = path.join(packageRoot, manifest.entrypoints.ui);\nif (!fs.existsSync(uiDir)) {\n  throw new Error(`UI bundle missing: ${uiDir} — run the plugin build before install`);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Build the plugin UI (dist/ui) before install/publish and include it in the package files list","Start the server with the same localPluginDir used by the installer","For local-path installs, keep the source directory in place or update the plugin's packagePath"],"tags":["plugins","plugin-ui","filesystem","http-404","build-output","installation"],"backgroundTag":"missing-build-output","analyzedSha":"120ae5428fa29bee300bcf806491cd4d965fbb7c","analyzedAt":"2026-08-18T22:49:45.177Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}