Skip to content

Commit 414b8ae

Browse files
feat(skills): add support for disable model invocation
1 parent 2b19292 commit 414b8ae

4 files changed

Lines changed: 92 additions & 10 deletions

File tree

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,37 @@ git clone https://github.com/anthropics/skills.git _temp
462462
mv _temp/skills/* . ; rm -r -force _temp
463463
```
464464

465+
#### User-Invocable Skills
466+
467+
Skills can be made invocable as commands from the commands palette (Ctrl+P). Add `user-invocable: true` to the skill's YAML frontmatter:
468+
469+
```yaml
470+
---
471+
name: my-skill
472+
description: A skill that can be invoked as a command.
473+
user-invocable: true
474+
---
475+
```
476+
477+
User-invocable skills appear in the commands palette with a `user:` or `project:` prefix:
478+
- Skills from global directories show as `user:skill-name`
479+
- Skills from project directories show as `project:skill-name`
480+
481+
When invoked, the skill's instructions are loaded into the conversation context.
482+
483+
To prevent the model from auto-triggering a skill (while still allowing user invocation), add `disable-model-invocation: true`:
484+
485+
```yaml
486+
---
487+
name: my-skill
488+
description: Only invocable by users, not the model.
489+
user-invocable: true
490+
disable-model-invocation: true
491+
---
492+
```
493+
494+
Skills with `disable-model-invocation` won't appear in the model's available skills list but can still be invoked manually by users.
495+
465496
### Desktop notifications
466497

467498
Crush sends desktop notifications when a tool call requires permission and when

internal/skills/builtin/crush-config/SKILL.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,37 @@ The `$schema` property enables IDE autocomplete but is optional.
156156
157157
Other options: `context_paths`, `progress`, `disable_notifications`, `disable_auto_summarize`, `disable_metrics`, `disable_provider_auto_update`, `disable_default_providers`, `data_directory`, `initialize_as`.
158158

159+
## User-Invocable Skills
160+
161+
Skills can be made invocable as commands from the commands palette. Add `user-invocable: true` to the skill's YAML frontmatter:
162+
163+
```yaml
164+
---
165+
name: my-skill
166+
description: A skill that can be invoked as a command.
167+
user-invocable: true
168+
---
169+
```
170+
171+
User-invocable skills appear in the commands palette with a prefix:
172+
- Skills from global directories: `user:skill-name`
173+
- Skills from project directories: `project:skill-name`
174+
175+
When invoked, the skill's instructions are loaded into the conversation context.
176+
177+
To prevent the model from auto-triggering a skill (while still allowing user invocation), add `disable-model-invocation: true`:
178+
179+
```yaml
180+
---
181+
name: my-skill
182+
description: Only invocable by users, not the model.
183+
user-invocable: true
184+
disable-model-invocation: true
185+
---
186+
```
187+
188+
Skills with `disable-model-invocation` won't appear in the model's available skills list but can still be invoked manually by users.
189+
159190
## Hooks
160191

161192
Hooks are user-defined shell commands that fire on agent events. Currently only `PreToolUse` is supported, which runs before a tool is executed.

internal/skills/skills.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,17 @@ var (
3434

3535
// Skill represents a parsed SKILL.md file.
3636
type Skill struct {
37-
Name string `yaml:"name" json:"name"`
38-
Description string `yaml:"description" json:"description"`
39-
UserInvocable bool `yaml:"user-invocable" json:"user_invocable"`
40-
License string `yaml:"license,omitempty" json:"license,omitempty"`
41-
Compatibility string `yaml:"compatibility,omitempty" json:"compatibility,omitempty"`
42-
Metadata map[string]string `yaml:"metadata,omitempty" json:"metadata,omitempty"`
43-
Instructions string `yaml:"-" json:"instructions"`
44-
Path string `yaml:"-" json:"path"`
45-
SkillFilePath string `yaml:"-" json:"skill_file_path"`
46-
Builtin bool `yaml:"-" json:"builtin"`
37+
Name string `yaml:"name" json:"name"`
38+
Description string `yaml:"description" json:"description"`
39+
UserInvocable bool `yaml:"user-invocable" json:"user_invocable"`
40+
DisableModelInvocation bool `yaml:"disable-model-invocation" json:"disable_model_invocation"`
41+
License string `yaml:"license,omitempty" json:"license,omitempty"`
42+
Compatibility string `yaml:"compatibility,omitempty" json:"compatibility,omitempty"`
43+
Metadata map[string]string `yaml:"metadata,omitempty" json:"metadata,omitempty"`
44+
Instructions string `yaml:"-" json:"instructions"`
45+
Path string `yaml:"-" json:"path"`
46+
SkillFilePath string `yaml:"-" json:"skill_file_path"`
47+
Builtin bool `yaml:"-" json:"builtin"`
4748
}
4849

4950
// DiscoveryState represents the outcome of discovering a single skill file.
@@ -259,6 +260,7 @@ func DiscoverWithStates(paths []string) ([]*Skill, []*SkillState) {
259260
}
260261

261262
// ToPromptXML generates XML for injection into the system prompt.
263+
// Skills with DisableModelInvocation set to true are excluded.
262264
func ToPromptXML(skills []*Skill) string {
263265
if len(skills) == 0 {
264266
return ""
@@ -267,6 +269,10 @@ func ToPromptXML(skills []*Skill) string {
267269
var sb strings.Builder
268270
sb.WriteString("<available_skills>\n")
269271
for _, s := range skills {
272+
// Skip skills that have disable-model-invocation set
273+
if s.DisableModelInvocation {
274+
continue
275+
}
270276
sb.WriteString(" <skill>\n")
271277
fmt.Fprintf(&sb, " <name>%s</name>\n", escape(s.Name))
272278
fmt.Fprintf(&sb, " <description>%s</description>\n", escape(s.Description))

internal/skills/skills_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,20 @@ func TestToPromptXML(t *testing.T) {
330330
require.Contains(t, xml, "&amp;") // XML escaping
331331
}
332332

333+
func TestToPromptXMLDisableModelInvocation(t *testing.T) {
334+
t.Parallel()
335+
336+
skills := []*Skill{
337+
{Name: "visible-skill", Description: "This one appears.", SkillFilePath: "/skills/visible/SKILL.md"},
338+
{Name: "hidden-skill", Description: "This one is hidden.", SkillFilePath: "/skills/hidden/SKILL.md", DisableModelInvocation: true},
339+
}
340+
341+
xml := ToPromptXML(skills)
342+
343+
require.Contains(t, xml, "<name>visible-skill</name>")
344+
require.NotContains(t, xml, "<name>hidden-skill</name>")
345+
}
346+
333347
func TestToPromptXMLEmpty(t *testing.T) {
334348
t.Parallel()
335349
require.Empty(t, ToPromptXML(nil))

0 commit comments

Comments
 (0)