paperclipai/paperclip · error · Error

Refusing to materialize a skill into itself, an ancestor, or

Error message

Refusing to materialize a skill into itself, an ancestor, or one of its descendants.

What it means

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.

Source

Thrown at packages/adapter-utils/src/server-utils.ts:3063

  await fs.rm(lockDir, { recursive: true, force: true }).catch(() => {});
  return true;
}

export async function materializePaperclipSkillCopy(
  source: string,
  target: string,
): Promise<MaterializedPaperclipSkillCopyResult> {
  const sourceRoot = path.resolve(source);
  const targetRoot = path.resolve(target);
  const relativeTarget = path.relative(sourceRoot, targetRoot);
  const relativeSource = path.relative(targetRoot, sourceRoot);
  if (
    !relativeTarget ||
    (!relativeTarget.startsWith("..") && !path.isAbsolute(relativeTarget)) ||
    !relativeSource ||
    (!relativeSource.startsWith("..") && !path.isAbsolute(relativeSource))
  ) {
    throw new Error("Refusing to materialize a skill into itself, an ancestor, or one of its descendants.");
  }

  const rootStat = await fs.lstat(sourceRoot);
  if (rootStat.isSymbolicLink()) {
    throw new Error("Refusing to materialize a skill root that is itself a symlink.");
  }
  if (!rootStat.isDirectory()) {
    throw new Error("Paperclip skills must be directories.");
  }

  const result: MaterializedPaperclipSkillCopyResult = {
    copiedFiles: 0,
    skippedSymlinks: [],
  };

  const lockDir = `${targetRoot}.lock`;
  const releaseLock = await acquireMaterializeLock(lockDir);
  const tempRoot = `${targetRoot}.tmp-${process.pid}-${randomUUID()}`;

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. 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>).
  2. Log path.resolve(source) and path.resolve(target) before the call to confirm they are not equal or nested.
  3. If you genuinely want to refresh a materialized copy, point target at a sibling directory, never back into the source.
  4. Check for trailing slashes or relative segments ('.', '..') that make two different-looking strings resolve to the same absolute path.

Example fix

// before
await materializePaperclipSkillCopy(
  "/app/skills/catalog/my-skill",
  "/app/skills/catalog/my-skill/copy",
);
// after
await materializePaperclipSkillCopy(
  "/app/skills/catalog/my-skill",
  "/app/skills/materialized/my-skill",
);
Defensive patterns

Strategy: validation

Validate before calling

function isSafeMaterializePair(source, target) {
  const a = path.resolve(source);
  const b = path.resolve(target);
  if (a === b) return false;
  const relTarget = path.relative(a, b);
  const relSource = path.relative(b, a);
  const outside = (r) => r.startsWith("..") || path.isAbsolute(r);
  return !!relTarget && outside(relTarget) && !!relSource && outside(relSource);
}
// before materializing:
if (!isSafeMaterializePair(source, target)) {
  throw new Error("source and target must be disjoint directory trees");
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/8585955570ac1f1d. Report an issue: GitHub.