{"record":{"id":"354a3aa4a2e7cd2a","repo":"moeru-ai/airi","slug":"extension-packages-cannot-contain-symbolic-links","errorCode":null,"errorMessage":"Extension packages cannot contain symbolic links: ${relativePath}","messagePattern":"Extension packages cannot contain symbolic links: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts","lineNumber":183,"sourceCode":"  let entryCount = 0\n  let totalBytes = 0\n  while (pendingDirectories.length > 0) {\n    const directory = pendingDirectories.pop()\n    if (!directory) {\n      continue\n    }\n    const entries = await opendir(directory)\n    for await (const entry of entries) {\n      entryCount += 1\n      if (entryCount > extensionPackageLimits.entries) {\n        throw new Error(`Extension package exceeds the ${extensionPackageLimits.entries} entry limit.`)\n      }\n      const path = join(directory, entry.name)\n      const stats = await lstat(path)\n      const relativePath = relative(sourceRealPath, path)\n\n      if (stats.isSymbolicLink()) {\n        throw new Error(`Extension packages cannot contain symbolic links: ${relativePath}`)\n      }\n      if (stats.isDirectory()) {\n        directories.push(relativePath)\n        pendingDirectories.push(path)\n        continue\n      }\n      if (!stats.isFile()) {\n        throw new Error(`Extension packages can contain only files and directories: ${relativePath}`)\n      }\n      totalBytes += stats.size\n      if (totalBytes > extensionPackageLimits.totalBytes) {\n        throw new Error('Extension package exceeds the 512 MiB size limit.')\n      }\n      files.push({ path, relativePath, size: stats.size })\n    }\n  }\n\n  directories.sort()","sourceCodeStart":165,"sourceCodeEnd":201,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts#L165-L201","documentation":"Every entry is checked with lstat; if any entry (at any depth) is a symbolic link the walk aborts. Symbolic links are forbidden because they can point outside the package root, breaking the containment guarantee for path traversal, size accounting, and staged copying.","triggerScenarios":"Calling inspectExtensionDirectory on a folder that contains any symlink — internal ones (entry -> ../shared) or absolute ones (entry -> /etc) — discovered during the recursive walk.","commonSituations":"macOS/Linux developers committing symlinks into the package (shared assets, node_modules .bin links); unpacking an archive containing symlinks; build tools creating .bin symlink directories inside the output.","solutions":["Replace symlinks with real copies of the target files/directories (cp -rL or rsync -L).","Import a flattened/resolved copy of the folder rather than the original tree.","Remove .bin or tool-generated symlink directories from the package.","If the link target is needed, vendor the actual files into the extension folder."],"exampleFix":"// before\nawait staged('my-ext/') // contains my-ext/assets -> ../shared/assets (symlink) -> throws\n// after: dereference links into real files\n// cp -rL my-ext my-ext-flat\nawait staged('my-ext-flat/')","handlingStrategy":"validation","validationCode":"import { readdir, lstat } from 'node:fs/promises'\nimport { join } from 'node:path'\nasync function hasSymlinks(dir: string): Promise<boolean> {\n  for (const entry of await readdir(dir, { withFileTypes: true })) {\n    if (entry.isSymbolicLink()) return true\n    if (entry.isDirectory() && await hasSymlinks(join(dir, entry.name))) return true\n  }\n  return false\n}","typeGuard":null,"tryCatchPattern":"try {\n  await staged(folder)\n} catch (error) {\n  if (error instanceof Error && error.message.includes('symbolic links')) {\n    showUserError(`Replace symlink with a real copy: ${error.message}`)\n    return\n  }\n  throw error\n}","preventionTips":["Package extensions with dereferenced copies (`cp -rL`, `rsync -L`) so no links ship.","Add a packaging step that fails CI if the bundle contains symlinks.","Exclude tool-generated symlink directories like node_modules/.bin from packages."],"tags":["filesystem","security","symlink","extensions"],"backgroundTag":"path-traversal-blocked","analyzedSha":"438a067dde47aa0bdb46c2323d1fe293dc805218","analyzedAt":"2026-09-17T01:14:42.644Z","contentChangedAt":"2026-09-17T01:14:42.644Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}