diff --git a/docs/reference/file-formats.md b/docs/reference/file-formats.md index 9d1ae34d0..e7d5b6cbe 100644 --- a/docs/reference/file-formats.md +++ b/docs/reference/file-formats.md @@ -321,7 +321,9 @@ claudecode: # for claudecode-specific parameters - "WebFetch" disable-model-invocation: true # (optional) disable model invocation for this skill scheduled-task: true # (optional) emit to .claude/scheduled-tasks//SKILL.md instead of .claude/skills//SKILL.md - paths: # (optional) glob patterns (string or list) limiting auto-activation + # paths (optional) limits auto-activation to matching globs. Accepts a + # comma-separated string, e.g. paths: "src/**/*.ts,test/**/*.ts", or a list: + paths: - "src/**/*.ts" - "test/**/*.ts" codexcli: # for codexcli-specific parameters diff --git a/skills/rulesync/file-formats.md b/skills/rulesync/file-formats.md index 9d1ae34d0..e7d5b6cbe 100644 --- a/skills/rulesync/file-formats.md +++ b/skills/rulesync/file-formats.md @@ -321,7 +321,9 @@ claudecode: # for claudecode-specific parameters - "WebFetch" disable-model-invocation: true # (optional) disable model invocation for this skill scheduled-task: true # (optional) emit to .claude/scheduled-tasks//SKILL.md instead of .claude/skills//SKILL.md - paths: # (optional) glob patterns (string or list) limiting auto-activation + # paths (optional) limits auto-activation to matching globs. Accepts a + # comma-separated string, e.g. paths: "src/**/*.ts,test/**/*.ts", or a list: + paths: - "src/**/*.ts" - "test/**/*.ts" codexcli: # for codexcli-specific parameters diff --git a/src/features/skills/claudecode-skill.test.ts b/src/features/skills/claudecode-skill.test.ts index e20b30743..a0d26cf2e 100644 --- a/src/features/skills/claudecode-skill.test.ts +++ b/src/features/skills/claudecode-skill.test.ts @@ -415,6 +415,42 @@ describe("ClaudecodeSkill", () => { }); }); + it("should preserve an empty-string paths value through a round-trip", () => { + const skill = new ClaudecodeSkill({ + dirName: "empty-paths-string-skill", + frontmatter: { + name: "empty-paths-string-skill", + description: "Skill with an empty paths string", + paths: "", + }, + body: "Empty paths string body", + }); + + const rulesyncSkill = skill.toRulesyncSkill(); + expect(rulesyncSkill.getFrontmatter().claudecode).toEqual({ paths: "" }); + + const roundTripped = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill }); + expect(roundTripped.getFrontmatter().paths).toBe(""); + }); + + it("should preserve an empty-array paths value through a round-trip", () => { + const skill = new ClaudecodeSkill({ + dirName: "empty-paths-array-skill", + frontmatter: { + name: "empty-paths-array-skill", + description: "Skill with an empty paths list", + paths: [], + }, + body: "Empty paths array body", + }); + + const rulesyncSkill = skill.toRulesyncSkill(); + expect(rulesyncSkill.getFrontmatter().claudecode).toEqual({ paths: [] }); + + const roundTripped = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill }); + expect(roundTripped.getFrontmatter().paths).toEqual([]); + }); + it("should preserve other files during conversion", () => { const frontmatter: ClaudecodeSkillFrontmatter = { name: "test-skill", @@ -820,6 +856,52 @@ This skill uses a specific model.`; expect(skill.getFrontmatter().model).toBe("sonnet"); }); + it("should load skill with paths as a YAML list", async () => { + const skillDir = join(testDir, ".claude", "skills", "paths-list-skill"); + await ensureDir(skillDir); + + const content = `--- +name: paths-list-skill +description: Skill scoped to paths +paths: + - src/**/*.ts + - test/**/*.ts +--- + +This skill is scoped to matching files.`; + + await writeFileContent(join(skillDir, SKILL_FILE_NAME), content); + + const skill = await ClaudecodeSkill.fromDir({ + outputRoot: testDir, + dirName: "paths-list-skill", + }); + + expect(skill.getFrontmatter().paths).toEqual(["src/**/*.ts", "test/**/*.ts"]); + }); + + it("should load skill with paths as a comma-separated string", async () => { + const skillDir = join(testDir, ".claude", "skills", "paths-string-skill"); + await ensureDir(skillDir); + + const content = `--- +name: paths-string-skill +description: Skill scoped to paths +paths: "src/**/*.ts,test/**/*.ts" +--- + +This skill is scoped to matching files.`; + + await writeFileContent(join(skillDir, SKILL_FILE_NAME), content); + + const skill = await ClaudecodeSkill.fromDir({ + outputRoot: testDir, + dirName: "paths-string-skill", + }); + + expect(skill.getFrontmatter().paths).toBe("src/**/*.ts,test/**/*.ts"); + }); + it("should load skill with other files", async () => { const skillDir = join(testDir, ".claude", "skills", "multi-file-skill"); await ensureDir(skillDir); diff --git a/src/features/skills/claudecode-skill.ts b/src/features/skills/claudecode-skill.ts index b9d7ebc72..821243583 100644 --- a/src/features/skills/claudecode-skill.ts +++ b/src/features/skills/claudecode-skill.ts @@ -131,8 +131,8 @@ export class ClaudecodeSkill extends ToolSkill { ...(frontmatter["disable-model-invocation"] !== undefined && { "disable-model-invocation": frontmatter["disable-model-invocation"], }), - ...(frontmatter.paths !== undefined && { paths: frontmatter.paths }), ...(this.relativeDirPath === CLAUDE_SCHEDULED_TASKS_DIR_PATH && { "scheduled-task": true }), + ...(frontmatter.paths !== undefined && { paths: frontmatter.paths }), }; const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = { name: frontmatter.name,