From 0c4d60f8c8431b98ced8bb7ebc9dfe13c8da7aad Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 24 Dec 2025 22:32:37 +0100 Subject: [PATCH] Skip over exact duplicate skills --- packages/coding-agent/src/core/skills.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/src/core/skills.ts b/packages/coding-agent/src/core/skills.ts index 681bea72..262b1e2b 100644 --- a/packages/coding-agent/src/core/skills.ts +++ b/packages/coding-agent/src/core/skills.ts @@ -1,4 +1,4 @@ -import { existsSync, readdirSync, readFileSync, statSync } from "fs"; +import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "fs"; import { minimatch } from "minimatch"; import { homedir } from "os"; import { basename, dirname, join, resolve } from "path"; @@ -357,6 +357,7 @@ export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult { const resolvedAgentDir = agentDir ?? getAgentDir(); const skillMap = new Map(); + const realPathSet = new Set(); const allWarnings: SkillWarning[] = []; const collisionWarnings: SkillWarning[] = []; @@ -383,6 +384,20 @@ export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult { if (!matchesIncludePatterns(skill.name)) { continue; } + + // Resolve symlinks to detect duplicate files + let realPath: string; + try { + realPath = realpathSync(skill.filePath); + } catch { + realPath = skill.filePath; + } + + // Skip silently if we've already loaded this exact file (via symlink) + if (realPathSet.has(realPath)) { + continue; + } + const existing = skillMap.get(skill.name); if (existing) { collisionWarnings.push({ @@ -391,6 +406,7 @@ export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult { }); } else { skillMap.set(skill.name, skill); + realPathSet.add(realPath); } } }