{"record":{"id":"fb830c99a8ae60e7","repo":"stablyai/orca","slug":"packaged-plugin-contains-a-symlink-relative-roo","errorCode":null,"errorMessage":"packaged plugin contains a symlink: ${relative(root, entryPath)}","messagePattern":"packaged plugin contains a symlink: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"config/scripts/verify-packaged-plugin-resources.cjs","lineNumber":33,"sourceCode":"  const files = []\n  let entriesVisited = 0\n  let totalBytes = 0\n  const visit = (directory) => {\n    const entries = readdirSync(directory, { withFileTypes: true }).sort((left, right) =>\n      left.name < right.name ? -1 : left.name > right.name ? 1 : 0\n    )\n    for (const entry of entries) {\n      if (directory === root && entry.name === '.git') {\n        continue\n      }\n      const entryPath = join(directory, entry.name)\n      const metadata = lstatSync(entryPath)\n      entriesVisited += 1\n      if (entriesVisited > MAX_PLUGIN_FILES) {\n        throw new Error(`plugin exceeds the ${MAX_PLUGIN_FILES}-entry limit`)\n      }\n      if (metadata.isSymbolicLink()) {\n        throw new Error(`packaged plugin contains a symlink: ${relative(root, entryPath)}`)\n      }\n      if (metadata.isDirectory()) {\n        visit(entryPath)\n      } else if (metadata.isFile()) {\n        totalBytes += metadata.size\n        if (totalBytes > MAX_PLUGIN_TOTAL_BYTES) {\n          throw new Error(`plugin exceeds the ${MAX_PLUGIN_TOTAL_BYTES}-byte limit`)\n        }\n        files.push({ path: entryPath, size: metadata.size })\n      } else {\n        throw new Error(`packaged plugin contains an unsupported entry: ${entryPath}`)\n      }\n    }\n  }\n  visit(root)\n  const hash = createHash('sha256').update('orca-plugin-tree-v1\\0')\n  for (const file of files) {\n    const relativePath = relative(root, file.path).replaceAll('\\\\', '/')","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/config/scripts/verify-packaged-plugin-resources.cjs#L15-L51","documentation":"Thrown by the packaged plugin resource verifier when lstatSync detects a symbolicic link during the recursive tree walk. Packaged plugins must contain only real files and directories — symlinks break content-addressable hashing (they produce platform-dependent behavior), cause issues on Windows where symlinks may not be supported, and can escape the plugin root. The check uses lstatSync (not statSync) specifically to detect the link itself rather than its target.","triggerScenarios":"hashPackagedPluginTree(root) encounters an entry where metadata.isSymbolicLink() returns true. Common in pnpm/yarn-workspace monorepos where node_modules use symlinks for package linking, or when a developer creates convenience symlinks within a plugin directory that get packaged.","commonSituations":"The plugin was developed in a pnpm workspace where dependencies are symlinked; a developer created a symlink for convenience (e.g., linking to a shared config); the packaging step dereferences most symlinks but missed some; cross-platform builds where symlinks created on Linux are packaged into a Windows-incompatible layout.","solutions":["Identify the symlink from the error message (it includes the relative path): ls -la <pluginRoot>/<relativePath>.","Replace the symlink with a real copy of the target file or directory.","Ensure the packaging step resolves all symlinks before creating the plugin archive: cp -rL or rsync -L.","If using pnpm, run the packaging step after pnpm install --shamefully-hoist or use a bundler that produces a self-contained output."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Before packaging, scan for symlinks in the plugin directory.\nconst { execSync } = require('node:child_process')\n\nfunction preCheckPluginSymlinks(pluginRoot) {\n  try {\n    const output = execSync(`find ${JSON.stringify(pluginRoot)} -type l`, {\n      encoding: 'utf8'\n    }).trim()\n    return { ok: output.length === 0, symlinks: output.split('\\n').filter(Boolean) }\n  } catch {\n    return { ok: true, symlinks: [] }\n  }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Resolve all symlinks during the packaging copy step: use cp -rL or rsync -L instead of plain cp -r.","If using pnpm, verify the packaged output doesn't contain the symlink-based node_modules layout; use --shamefully-hoist or a bundler.","Add a pre-packaging symlink scan to the CI pipeline that fails on any symlink found."],"tags":["plugin-packaging","symlink","ci-gate","cross-platform","resource-verification"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}