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
- 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.
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
- 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.
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
- Refusing to materialize a skill root that is itself a symlin
- Paperclip skills must be directories.
- Could not locate local Paperclip skills directory. Expected
- Export output path ${root} exists and is not a directory.
- Export output directory ${root} already contains files. Re-r
AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12).
Data as JSON: /api/errors/8585955570ac1f1d.
Report an issue: GitHub.