Skip to content

Commit 8ce082f

Browse files
authored
Merge pull request #1256 from jornetsimon/feat/support-disable-model-invocation-in-skills
feat(skills): support disable-model-invocation in Claude Code skills
2 parents 147b53a + ee2effc commit 8ce082f

File tree

5 files changed

+152
-0
lines changed

5 files changed

+152
-0
lines changed

docs/reference/file-formats.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ claudecode: # for claudecode-specific parameters
163163
- "Read"
164164
- "Write"
165165
- "Grep"
166+
disable-model-invocation: true # (optional) disable model invocation for this skill
166167
codexcli: # for codexcli-specific parameters
167168
short-description: A brief user-facing description
168169
---

skills/rulesync/file-formats.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ claudecode: # for claudecode-specific parameters
163163
- "Read"
164164
- "Write"
165165
- "Grep"
166+
disable-model-invocation: true # (optional) disable model invocation for this skill
166167
codexcli: # for codexcli-specific parameters
167168
short-description: A brief user-facing description
168169
---

src/features/skills/claudecode-skill.test.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,48 @@ describe("ClaudecodeSkill", () => {
304304
});
305305
});
306306

307+
it("should convert to RulesyncSkill with disable-model-invocation true", () => {
308+
const frontmatter: ClaudecodeSkillFrontmatter = {
309+
name: "no-invoke-skill",
310+
description: "Skill with disabled invocation",
311+
"disable-model-invocation": true,
312+
};
313+
314+
const skill = new ClaudecodeSkill({
315+
dirName: "no-invoke-skill",
316+
frontmatter,
317+
body: "No invoke body",
318+
});
319+
320+
const rulesyncSkill = skill.toRulesyncSkill();
321+
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
322+
323+
expect(rulesyncFrontmatter.claudecode).toEqual({
324+
"disable-model-invocation": true,
325+
});
326+
});
327+
328+
it("should convert to RulesyncSkill with disable-model-invocation false", () => {
329+
const frontmatter: ClaudecodeSkillFrontmatter = {
330+
name: "invoke-skill",
331+
description: "Skill with explicit false",
332+
"disable-model-invocation": false,
333+
};
334+
335+
const skill = new ClaudecodeSkill({
336+
dirName: "invoke-skill",
337+
frontmatter,
338+
body: "Invoke body",
339+
});
340+
341+
const rulesyncSkill = skill.toRulesyncSkill();
342+
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
343+
344+
expect(rulesyncFrontmatter.claudecode).toEqual({
345+
"disable-model-invocation": false,
346+
});
347+
});
348+
307349
it("should preserve other files during conversion", () => {
308350
const frontmatter: ClaudecodeSkillFrontmatter = {
309351
name: "test-skill",
@@ -409,6 +451,40 @@ describe("ClaudecodeSkill", () => {
409451
expect(fm["allowed-tools"]).toEqual(["Bash"]);
410452
});
411453

454+
it("should convert from RulesyncSkill with disable-model-invocation", () => {
455+
const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
456+
name: "no-invoke-skill",
457+
description: "Skill with disabled invocation",
458+
claudecode: { "disable-model-invocation": true },
459+
};
460+
461+
const rulesyncSkill = new RulesyncSkill({
462+
dirName: "no-invoke-skill",
463+
frontmatter: rulesyncFrontmatter,
464+
body: "No invoke body",
465+
});
466+
467+
const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });
468+
expect(claudecodeSkill.getFrontmatter()["disable-model-invocation"]).toBe(true);
469+
});
470+
471+
it("should convert from RulesyncSkill with disable-model-invocation false", () => {
472+
const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
473+
name: "invoke-skill",
474+
description: "Skill with explicit false",
475+
claudecode: { "disable-model-invocation": false },
476+
};
477+
478+
const rulesyncSkill = new RulesyncSkill({
479+
dirName: "invoke-skill",
480+
frontmatter: rulesyncFrontmatter,
481+
body: "Invoke body",
482+
});
483+
484+
const claudecodeSkill = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });
485+
expect(claudecodeSkill.getFrontmatter()["disable-model-invocation"]).toBe(false);
486+
});
487+
412488
it("should set correct relativeDirPath", () => {
413489
const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
414490
name: "test-skill",
@@ -754,6 +830,33 @@ Global skill content.`;
754830
expect(result.success).toBe(true);
755831
});
756832

833+
it("should validate frontmatter with disable-model-invocation true", () => {
834+
const result = ClaudecodeSkillFrontmatterSchema.safeParse({
835+
name: "test-skill",
836+
description: "Test",
837+
"disable-model-invocation": true,
838+
});
839+
expect(result.success).toBe(true);
840+
});
841+
842+
it("should validate frontmatter with disable-model-invocation false", () => {
843+
const result = ClaudecodeSkillFrontmatterSchema.safeParse({
844+
name: "test-skill",
845+
description: "Test",
846+
"disable-model-invocation": false,
847+
});
848+
expect(result.success).toBe(true);
849+
});
850+
851+
it("should reject non-boolean disable-model-invocation value", () => {
852+
const result = ClaudecodeSkillFrontmatterSchema.safeParse({
853+
name: "test-skill",
854+
description: "Test",
855+
"disable-model-invocation": "yes",
856+
});
857+
expect(result.success).toBe(false);
858+
});
859+
757860
it("should reject non-string model value", () => {
758861
const result = ClaudecodeSkillFrontmatterSchema.safeParse({
759862
name: "test-skill",
@@ -765,6 +868,44 @@ Global skill content.`;
765868
});
766869

767870
describe("round-trip conversion", () => {
871+
it("should preserve disable-model-invocation through round-trip", () => {
872+
const originalFrontmatter: ClaudecodeSkillFrontmatter = {
873+
name: "round-trip-skill",
874+
description: "Round trip test",
875+
"disable-model-invocation": true,
876+
};
877+
878+
const original = new ClaudecodeSkill({
879+
dirName: "round-trip-skill",
880+
frontmatter: originalFrontmatter,
881+
body: "Round trip body",
882+
});
883+
884+
const rulesyncSkill = original.toRulesyncSkill();
885+
const restored = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });
886+
887+
expect(restored.getFrontmatter()["disable-model-invocation"]).toBe(true);
888+
});
889+
890+
it("should preserve disable-model-invocation false through round-trip", () => {
891+
const originalFrontmatter: ClaudecodeSkillFrontmatter = {
892+
name: "round-trip-skill",
893+
description: "Round trip test",
894+
"disable-model-invocation": false,
895+
};
896+
897+
const original = new ClaudecodeSkill({
898+
dirName: "round-trip-skill",
899+
frontmatter: originalFrontmatter,
900+
body: "Round trip body",
901+
});
902+
903+
const rulesyncSkill = original.toRulesyncSkill();
904+
const restored = ClaudecodeSkill.fromRulesyncSkill({ rulesyncSkill });
905+
906+
expect(restored.getFrontmatter()["disable-model-invocation"]).toBe(false);
907+
});
908+
768909
it("should preserve model through ClaudecodeSkill -> RulesyncSkill -> ClaudecodeSkill", () => {
769910
const originalFrontmatter: ClaudecodeSkillFrontmatter = {
770911
name: "round-trip-skill",

src/features/skills/claudecode-skill.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const ClaudecodeSkillFrontmatterSchema = z.looseObject({
2020
description: z.string(),
2121
"allowed-tools": z.optional(z.array(z.string())),
2222
model: z.optional(z.string()),
23+
"disable-model-invocation": z.optional(z.boolean()),
2324
});
2425

2526
export type ClaudecodeSkillFrontmatter = z.infer<typeof ClaudecodeSkillFrontmatterSchema>;
@@ -120,6 +121,9 @@ export class ClaudecodeSkill extends ToolSkill {
120121
const claudecodeSection = {
121122
...(frontmatter["allowed-tools"] && { "allowed-tools": frontmatter["allowed-tools"] }),
122123
...(frontmatter.model && { model: frontmatter.model }),
124+
...(frontmatter["disable-model-invocation"] !== undefined && {
125+
"disable-model-invocation": frontmatter["disable-model-invocation"],
126+
}),
123127
};
124128
const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
125129
name: frontmatter.name,
@@ -156,6 +160,9 @@ export class ClaudecodeSkill extends ToolSkill {
156160
...(rulesyncFrontmatter.claudecode?.model && {
157161
model: rulesyncFrontmatter.claudecode.model,
158162
}),
163+
...(rulesyncFrontmatter.claudecode?.["disable-model-invocation"] !== undefined && {
164+
"disable-model-invocation": rulesyncFrontmatter.claudecode["disable-model-invocation"],
165+
}),
159166
};
160167

161168
const settablePaths = ClaudecodeSkill.getSettablePaths({ global });

src/features/skills/rulesync-skill.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const RulesyncSkillFrontmatterSchemaInternal = z.looseObject({
1818
z.looseObject({
1919
"allowed-tools": z.optional(z.array(z.string())),
2020
model: z.optional(z.string()),
21+
"disable-model-invocation": z.optional(z.boolean()),
2122
}),
2223
),
2324
codexcli: z.optional(
@@ -50,6 +51,7 @@ export type RulesyncSkillFrontmatterInput = {
5051
claudecode?: {
5152
"allowed-tools"?: string[];
5253
model?: string;
54+
"disable-model-invocation"?: boolean;
5355
};
5456
codexcli?: {
5557
"short-description"?: string;

0 commit comments

Comments
 (0)