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

  1. Identify the two load paths producing the same sourceDirectory and load each skill collection from a single canonical root.
  2. De-duplicate the mounted/copied folders so each skill lives under exactly one source path.
  3. 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

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


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/d38b0fa2ff156c3f. Report an issue: GitHub.