{"record":{"id":"d38b0fa2ff156c3f","repo":"n8n-io/n8n","slug":"duplicate-skill-source-directory-normalizedskil","errorCode":null,"errorMessage":"Duplicate skill source directory \"${normalizedSkill.sourceDirectory}\"","messagePattern":"Duplicate skill source directory \"(.+?)\"","errorType":"exception","errorClass":"InvalidRuntimeSkillError","httpStatus":null,"severity":"error","filePath":"packages/@n8n/agents/src/skills/registry.ts","lineNumber":196,"sourceCode":"\t\tconst validation = validateRuntimeSkill(normalizedSkill);\n\t\tif (!validation.ok) {\n\t\t\tthrow new InvalidRuntimeSkillError(formatSkillValidationErrors(validation.errors));\n\t\t}\n\n\t\tif (seenIds.has(normalizedSkill.id)) {\n\t\t\tthrow new InvalidRuntimeSkillError(`Duplicate skill id \"${normalizedSkill.id}\"`);\n\t\t}\n\t\tseenIds.add(normalizedSkill.id);\n\n\t\tconst normalizedName = normalizedSkill.name.toLowerCase();\n\t\tif (seenNames.has(normalizedName)) {\n\t\t\tthrow new InvalidRuntimeSkillError(`Duplicate skill name \"${normalizedSkill.name}\"`);\n\t\t}\n\t\tseenNames.add(normalizedName);\n\n\t\tif (normalizedSkill.sourceDirectory) {\n\t\t\tif (seenSourceDirectories.has(normalizedSkill.sourceDirectory)) {\n\t\t\t\tthrow new InvalidRuntimeSkillError(\n\t\t\t\t\t`Duplicate skill source directory \"${normalizedSkill.sourceDirectory}\"`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tseenSourceDirectories.add(normalizedSkill.sourceDirectory);\n\t\t}\n\n\t\treturn validation.skill;\n\t});\n}\n\nfunction toRegistryEntry(skill: RuntimeSkill): RuntimeSkillRegistryEntry {\n\treturn {\n\t\tid: skill.id,\n\t\tname: skill.name,\n\t\tdescription: skill.description,\n\t\thash: hashSkill(skill),\n\t\tlinkedFiles: normalizeLinkedFiles(skill.linkedFiles),\n\t\t...(skill.recommendedTools ? { recommendedTools: skill.recommendedTools } : {}),","sourceCodeStart":178,"sourceCodeEnd":214,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/agents/src/skills/registry.ts#L178-L214","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before — same collection loaded twice\nconst a = loadRuntimeSkillSourceFromDirectory('/srv/skills');\nconst b = loadRuntimeSkillSourceFromDirectory('/srv/skills'); // duplicate source dirs\n\n// after — load once, branch with exclude\nconst base = loadRuntimeSkillSourceFromDirectory('/srv/skills');\nconst a = base;\nconst b = filterRuntimeSkillSource(base, ['skill-to-hide']);","handlingStrategy":"validation","validationCode":"function assertUniqueSourceDirectories(skills: RuntimeSkill[]): void {\n  const seen = new Set<string>();\n  for (const s of skills) {\n    if (!s.sourceDirectory) continue;\n    if (seen.has(s.sourceDirectory)) {\n      throw new Error(`Duplicate skill source directory \"${s.sourceDirectory}\"`);\n    }\n    seen.add(s.sourceDirectory);\n  }\n}\n\nassertUniqueSourceDirectories(skills);","typeGuard":"function hasUniqueSourceDirs(skills: RuntimeSkill[]): boolean {\n  const dirs = skills.map((s) => s.sourceDirectory).filter(Boolean) as string[];\n  return new Set(dirs).size === dirs.length;\n}","tryCatchPattern":"try {\n  loadRuntimeSkillSourceFromDirectory(rootDir);\n} catch (err) {\n  if (err instanceof InvalidRuntimeSkillError && /Duplicate skill source directory/.test(err.message)) {\n    // structural: ensure each directory is loaded once, then rebuild\n    throw err;\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["skills","duplicates","filesystem","configuration"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}