{"record":{"id":"0f6c7f7c663ac644","repo":"paperclipai/paperclip","slug":"refusing-to-materialize-a-skill-root-that-is-itsel","errorCode":null,"errorMessage":"Refusing to materialize a skill root that is itself a symlink.","messagePattern":"Refusing to materialize a skill root that is itself a symlink\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/adapter-utils/src/server-utils.ts","lineNumber":3068,"sourceCode":"  source: string,\n  target: string,\n): Promise<MaterializedPaperclipSkillCopyResult> {\n  const sourceRoot = path.resolve(source);\n  const targetRoot = path.resolve(target);\n  const relativeTarget = path.relative(sourceRoot, targetRoot);\n  const relativeSource = path.relative(targetRoot, sourceRoot);\n  if (\n    !relativeTarget ||\n    (!relativeTarget.startsWith(\"..\") && !path.isAbsolute(relativeTarget)) ||\n    !relativeSource ||\n    (!relativeSource.startsWith(\"..\") && !path.isAbsolute(relativeSource))\n  ) {\n    throw new Error(\"Refusing to materialize a skill into itself, an ancestor, or one of its descendants.\");\n  }\n\n  const rootStat = await fs.lstat(sourceRoot);\n  if (rootStat.isSymbolicLink()) {\n    throw new Error(\"Refusing to materialize a skill root that is itself a symlink.\");\n  }\n  if (!rootStat.isDirectory()) {\n    throw new Error(\"Paperclip skills must be directories.\");\n  }\n\n  const result: MaterializedPaperclipSkillCopyResult = {\n    copiedFiles: 0,\n    skippedSymlinks: [],\n  };\n\n  const lockDir = `${targetRoot}.lock`;\n  const releaseLock = await acquireMaterializeLock(lockDir);\n  const tempRoot = `${targetRoot}.tmp-${process.pid}-${randomUUID()}`;\n\n  async function copyEntry(sourcePath: string, targetPath: string, relativePath: string): Promise<void> {\n    const stat = await fs.lstat(sourcePath);\n    if (stat.isSymbolicLink()) {\n      result.skippedSymlinks.push(relativePath || \".\");","sourceCodeStart":3050,"sourceCodeEnd":3086,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/packages/adapter-utils/src/server-utils.ts#L3050-L3086","documentation":"Thrown by materializePaperclipSkillCopy when fs.lstat(sourceRoot) reports the source root is itself a symbolic link. The copy walker descends into directories but explicitly skips symlinked entries, so a symlinked root would either copy nothing meaningful or follow outside the intended tree. Refusing a symlink root keeps the materialized copy self-contained and reproducible.","triggerScenarios":"Calling materializePaperclipSkillCopy with a source that is a symlink (ln -s real_skill link_skill and passing link_skill as source). fs.lstat follows the link metadata, not the target, so isSymbolicLink() is true.","commonSituations":"A skills home populated with symlinks into a maintainer-only catalog; a deploy that symlinks the skill dir into a shared volume; dev setups that symlink a repo subdirectory as the skill root.","solutions":["Pass the real directory the symlink resolves to: use fs.realpath(source) before calling, or resolve the link manually.","Reconfigure the skills catalog to expose real directories instead of symlinks at the root.","If the link is intentional for development, point materializePaperclipSkillCopy at the link target path directly."],"exampleFix":"// before\nawait materializePaperclipSkillCopy(source, target);\n// after\nconst realSource = await fs.realpath(source);\nawait materializePaperclipSkillCopy(realSource, target);","handlingStrategy":"validation","validationCode":"const stat = await fs.lstat(source);\nif (stat.isSymbolicLink()) {\n  // resolve to the real target or refuse\n  source = await fs.realpath(source);\n}\nawait materializePaperclipSkillCopy(source, target);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Run fs.realpath on skill source paths before materialization to defeat symlinks.","Document that skill catalog roots must be real directories, not symlinks.","Add a catalog-load check that lstat's each skill root and rejects symlinks at ingest time."],"tags":["filesystem","symlink","skill-materialization","paperclip"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}