{"record":{"id":"8585955570ac1f1d","repo":"paperclipai/paperclip","slug":"refusing-to-materialize-a-skill-into-itself-an-an","errorCode":null,"errorMessage":"Refusing to materialize a skill into itself, an ancestor, or one of its descendants.","messagePattern":"Refusing to materialize a skill into itself, an ancestor, or one of its descendants\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/adapter-utils/src/server-utils.ts","lineNumber":3063,"sourceCode":"  await fs.rm(lockDir, { recursive: true, force: true }).catch(() => {});\n  return true;\n}\n\nexport async function materializePaperclipSkillCopy(\n  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()}`;","sourceCodeStart":3045,"sourceCodeEnd":3081,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/packages/adapter-utils/src/server-utils.ts#L3045-L3081","documentation":"Thrown by materializePaperclipSkillCopy when the source and target skill directories overlap. The function computes path.relative both ways and rejects any case where one path is equal to, nested inside, or an ancestor of the other, because copying would recurse into itself or clobber the source. This is a safety guard against destructive or infinite copy loops, not a filesystem limitation.","triggerScenarios":"Calling materializePaperclipSkillCopy(source, target) where path.resolve(source) === path.resolve(target), or target lives under source (e.g. source=/skills/foo, target=/skills/foo/dist), or source lives under target. Also fires when relativeTarget or relativeSource is the empty string (identical paths).","commonSituations":"Misconfigured skill materialization config where the target materialize dir resolves to the same tree as the catalog source; relative paths passed from different cwd contexts that resolve to overlapping locations; a default target that was accidentally set inside the skills catalog directory.","solutions":["Ensure source and target resolve to disjoint directory trees: pick a target outside the source subtree (e.g. a separate materialize root like /var/paperclip/materialized/<skill>).","Log path.resolve(source) and path.resolve(target) before the call to confirm they are not equal or nested.","If you genuinely want to refresh a materialized copy, point target at a sibling directory, never back into the source.","Check for trailing slashes or relative segments ('.', '..') that make two different-looking strings resolve to the same absolute path."],"exampleFix":"// before\nawait materializePaperclipSkillCopy(\n  \"/app/skills/catalog/my-skill\",\n  \"/app/skills/catalog/my-skill/copy\",\n);\n// after\nawait materializePaperclipSkillCopy(\n  \"/app/skills/catalog/my-skill\",\n  \"/app/skills/materialized/my-skill\",\n);","handlingStrategy":"validation","validationCode":"function isSafeMaterializePair(source, target) {\n  const a = path.resolve(source);\n  const b = path.resolve(target);\n  if (a === b) return false;\n  const relTarget = path.relative(a, b);\n  const relSource = path.relative(b, a);\n  const outside = (r) => r.startsWith(\"..\") || path.isAbsolute(r);\n  return !!relTarget && outside(relTarget) && !!relSource && outside(relSource);\n}\n// before materializing:\nif (!isSafeMaterializePair(source, target)) {\n  throw new Error(\"source and target must be disjoint directory trees\");\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always resolve source and target to absolute paths and assert neither is nested under the other before calling materializePaperclipSkillCopy.","Keep the materialize target root physically separate from the skills catalog source root.","Log both resolved paths in debug builds to catch overlap early."],"tags":["filesystem","skill-materialization","path-safety","paperclip"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}