n8n-io/n8n · error · InvalidRuntimeSkillError
Duplicate skill source directory "${normalizedSkill.sourceDi
Error message
Duplicate skill source directory "${normalizedSkill.sourceDirectory}" What it means
Thrown by normalizeRuntimeSkills when two skills share the same `sourceDirectory` — the path of the skill folder relative to the load root. Each skill must live in its own directory; two skills resolving to the same source directory indicates a structural problem (e.g. the same folder loaded twice, or two SKILL.md files detected under one logical path). Wrapped as InvalidRuntimeSkillError.
Source
Thrown at packages/@n8n/agents/src/skills/registry.ts:196
const validation = validateRuntimeSkill(normalizedSkill);
if (!validation.ok) {
throw new InvalidRuntimeSkillError(formatSkillValidationErrors(validation.errors));
}
if (seenIds.has(normalizedSkill.id)) {
throw new InvalidRuntimeSkillError(`Duplicate skill id "${normalizedSkill.id}"`);
}
seenIds.add(normalizedSkill.id);
const normalizedName = normalizedSkill.name.toLowerCase();
if (seenNames.has(normalizedName)) {
throw new InvalidRuntimeSkillError(`Duplicate skill name "${normalizedSkill.name}"`);
}
seenNames.add(normalizedName);
if (normalizedSkill.sourceDirectory) {
if (seenSourceDirectories.has(normalizedSkill.sourceDirectory)) {
throw new InvalidRuntimeSkillError(
`Duplicate skill source directory "${normalizedSkill.sourceDirectory}"`,
);
}
seenSourceDirectories.add(normalizedSkill.sourceDirectory);
}
return validation.skill;
});
}
function toRegistryEntry(skill: RuntimeSkill): RuntimeSkillRegistryEntry {
return {
id: skill.id,
name: skill.name,
description: skill.description,
hash: hashSkill(skill),
linkedFiles: normalizeLinkedFiles(skill.linkedFiles),
...(skill.recommendedTools ? { recommendedTools: skill.recommendedTools } : {}),View on GitHub (pinned to 5ac6606e81)
Solutions
- Identify the two load paths producing the same sourceDirectory and load each skill collection from a single canonical root.
- De-duplicate the mounted/copied folders so each skill lives under exactly one source path.
- If you genuinely need the same skill in two contexts, use filterRuntimeSkillSource(exclude) instead of loading the directory twice.
Example fix
// before — same collection loaded twice
const a = loadRuntimeSkillSourceFromDirectory('/srv/skills');
const b = loadRuntimeSkillSourceFromDirectory('/srv/skills'); // duplicate source dirs
// after — load once, branch with exclude
const base = loadRuntimeSkillSourceFromDirectory('/srv/skills');
const a = base;
const b = filterRuntimeSkillSource(base, ['skill-to-hide']); Defensive patterns
Strategy: validation
Validate before calling
function assertUniqueSourceDirectories(skills: RuntimeSkill[]): void {
const seen = new Set<string>();
for (const s of skills) {
if (!s.sourceDirectory) continue;
if (seen.has(s.sourceDirectory)) {
throw new Error(`Duplicate skill source directory "${s.sourceDirectory}"`);
}
seen.add(s.sourceDirectory);
}
}
assertUniqueSourceDirectories(skills); Type guard
function hasUniqueSourceDirs(skills: RuntimeSkill[]): boolean {
const dirs = skills.map((s) => s.sourceDirectory).filter(Boolean) as string[];
return new Set(dirs).size === dirs.length;
} Try / catch
try {
loadRuntimeSkillSourceFromDirectory(rootDir);
} catch (err) {
if (err instanceof InvalidRuntimeSkillError && /Duplicate skill source directory/.test(err.message)) {
// structural: ensure each directory is loaded once, then rebuild
throw err;
}
throw err;
} Prevention
- Load each skill collection from a single canonical root directory.
- Use filterRuntimeSkillSource(exclude) to branch one loaded source rather than loading twice.
- In CI, assert no two resolved skill sourceDirectories are equal before publishing.
When it happens
Trigger: Loading the same root directory twice via loadRuntimeSkillSourceFromDirectory; a skill folder reachable through two relative paths that normalize to the same posix path; mounting the same skill collection under two roots that get concatenated; a build step that copies skills into a staging dir that is also scanned.
Common situations: A monorepo that mounts a shared skills folder into multiple packages and loads both; CI that symlinks (note: symlinks are separately rejected by error 107); misconfigured skill root globbing that overlaps; a deploy script duplicating skills into a bundle directory.
Related errors
- Duplicate skill id "${normalizedSkill.id}"
- Duplicate skill name "${normalizedSkill.name}"
- Tool name "${reservedTool.name}" is reserved for runtime ski
- Invalid skill at ${sourceDirectory}: ${formatSkillValidation
- Invalid skill at ${sourceDirectory}: ${errors.join('; ')}
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/d38b0fa2ff156c3f.
Report an issue: GitHub.