{"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":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/adapter-utils/src/server-utils.ts","lineNumber":4405,"sourceCode":"): 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(\n      \"Refusing to materialize a skill into itself, an ancestor, or one of its descendants.\",\n    );\n  }\n\n  const rootStat = await fs.lstat(sourceRoot);\n  if (rootStat.isSymbolicLink()) {\n    throw new Error(\n      \"Refusing to materialize a skill root that is itself a symlink.\",\n    );\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(\n    sourcePath: string,","sourceCodeStart":4387,"sourceCodeEnd":4423,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/packages/adapter-utils/src/server-utils.ts#L4387-L4423","documentation":"Safety guard in the Paperclip skill materialization copy helper: before copying, the routine resolves source and target roots and computes relative paths between them; when the resolved source root itself is a symlink (or source/target nest inside each other), it refuses to copy. This prevents aliasing hazards where a symlinked skill root would make 'copying' clobber the original location or escape the intended destination tree. It is a fail-fast input validation, not a runtime fault.","triggerScenarios":"Thrown at packages/adapter-utils/src/server-utils.ts:3161 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":["Use a real directory (not a symlink) as the skill root."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}