@@ -66,6 +66,8 @@ func (s Skill) DisplayName() string {
6666 return "[plugins] " + name
6767 case "root" :
6868 return "[root] " + name
69+ case "hidden-dir" , "hidden-dir-namespaced" :
70+ return "[hidden-dir] " + name
6971 default :
7072 return name
7173 }
@@ -82,6 +84,23 @@ func (s Skill) InstallName() string {
8284 return s .Name
8385}
8486
87+ // IsHiddenDirConvention returns true if the skill was discovered in a hidden
88+ // (dot-prefixed) directory such as .claude/skills/ or .agents/skills/.
89+ func (s Skill ) IsHiddenDirConvention () bool {
90+ return s .Convention == "hidden-dir" || s .Convention == "hidden-dir-namespaced"
91+ }
92+
93+ // HasHiddenDirSkills returns true if any of the given skills were discovered
94+ // in hidden directories.
95+ func HasHiddenDirSkills (skills []Skill ) bool {
96+ for _ , s := range skills {
97+ if s .IsHiddenDirConvention () {
98+ return true
99+ }
100+ }
101+ return false
102+ }
103+
85104// ResolvedRef contains the resolved git reference and its SHA.
86105type ResolvedRef struct {
87106 Ref string // fully qualified ref (refs/heads/*, refs/tags/*) or commit SHA
@@ -393,8 +412,87 @@ func matchSkillConventions(entry treeEntry) *skillMatch {
393412 return nil
394413}
395414
396- // DiscoverSkills finds all skills in a repository at the given commit SHA.
415+ // matchHiddenDirConventions checks if a blob path matches a skill convention
416+ // under a hidden (dot-prefixed) root directory. These patterns mirror the
417+ // standard skills/ conventions but rooted under .{host}/skills/:
418+ //
419+ // - .{host}/skills/*/SKILL.md -> "hidden-dir"
420+ // - .{host}/skills/{scope}/*/SKILL.md -> "hidden-dir-namespaced"
421+ func matchHiddenDirConventions (entry treeEntry ) * skillMatch {
422+ if path .Base (entry .Path ) != "SKILL.md" {
423+ return nil
424+ }
425+
426+ // .{host}/skills/*
427+ // .{host}/skills/{scope}/*
428+ dir := path .Dir (entry .Path )
429+ skillName := path .Base (dir )
430+
431+ if ! validateName (skillName ) {
432+ return nil
433+ }
434+
435+ // .{host}/skills
436+ // .{host}/skills/{scope}
437+ parentDir := path .Dir (dir )
438+
439+ // .{host}/skills/*/SKILL.md
440+ if path .Base (parentDir ) == "skills" {
441+ hiddenRoot := path .Dir (parentDir )
442+ if path .Dir (hiddenRoot ) == "." && strings .HasPrefix (hiddenRoot , "." ) {
443+ return & skillMatch {entry : entry , name : skillName , skillDir : dir , convention : "hidden-dir" }
444+ }
445+ }
446+
447+ // .{host}/skills/{scope}/*/SKILL.md
448+ grandparentDir := path .Dir (parentDir )
449+ if path .Base (grandparentDir ) == "skills" {
450+ hiddenRoot := path .Dir (grandparentDir )
451+ if path .Dir (hiddenRoot ) == "." && strings .HasPrefix (hiddenRoot , "." ) {
452+ namespace := path .Base (parentDir )
453+ if ! validateName (namespace ) {
454+ return nil
455+ }
456+ return & skillMatch {entry : entry , name : skillName , namespace : namespace , skillDir : dir , convention : "hidden-dir-namespaced" }
457+ }
458+ }
459+
460+ return nil
461+ }
462+
463+ // DiscoverOptions controls optional discovery behaviors.
464+ type DiscoverOptions struct {
465+ }
466+
467+ // DiscoverSkills finds all non-hidden-dir skills in a repository at the given
468+ // commit SHA. Hidden-dir skills are excluded; use DiscoverSkillsWithOptions to
469+ // retrieve all skills including those in hidden directories.
397470func DiscoverSkills (client * api.Client , host , owner , repo , commitSHA string ) ([]Skill , error ) {
471+ all , err := DiscoverSkillsWithOptions (client , host , owner , repo , commitSHA , DiscoverOptions {})
472+ if err != nil {
473+ return nil , err
474+ }
475+ var skills []Skill
476+ for _ , s := range all {
477+ if ! s .IsHiddenDirConvention () {
478+ skills = append (skills , s )
479+ }
480+ }
481+ if len (skills ) == 0 {
482+ return nil , fmt .Errorf (
483+ "no skills found in %s/%s\n " +
484+ " Expected skills in skills/*/SKILL.md, skills/{scope}/*/SKILL.md,\n " +
485+ " */SKILL.md, or plugins/*/skills/*/SKILL.md\n " +
486+ " This repository may be a curated list rather than a skills publisher" ,
487+ owner , repo ,
488+ )
489+ }
490+ return skills , nil
491+ }
492+
493+ // DiscoverSkillsWithOptions finds all skills in a repository at the given
494+ // commit SHA, with configurable discovery behavior.
495+ func DiscoverSkillsWithOptions (client * api.Client , host , owner , repo , commitSHA string , opts DiscoverOptions ) ([]Skill , error ) {
398496 apiPath := fmt .Sprintf ("repos/%s/%s/git/trees/%s?recursive=true" , url .PathEscape (owner ), url .PathEscape (repo ), url .PathEscape (commitSHA ))
399497 var tree treeResponse
400498 if err := client .REST (host , "GET" , apiPath , nil , & tree ); err != nil {
@@ -419,6 +517,9 @@ func DiscoverSkills(client *api.Client, host, owner, repo, commitSHA string) ([]
419517 continue
420518 }
421519 m := matchSkillConventions (entry )
520+ if m == nil {
521+ m = matchHiddenDirConventions (entry )
522+ }
422523 if m == nil {
423524 continue
424525 }
@@ -703,9 +804,35 @@ func FetchBlob(client *api.Client, host, owner, repo, sha string) (string, error
703804 return string (decoded ), nil
704805}
705806
706- // DiscoverLocalSkills finds skills in a local directory using the same
707- // conventions as remote discovery.
807+ // DiscoverLocalSkills finds non-hidden-dir skills in a local directory using
808+ // the same conventions as remote discovery. Hidden-dir skills are excluded; use
809+ // DiscoverLocalSkillsWithOptions to retrieve all skills including those in
810+ // hidden directories.
708811func DiscoverLocalSkills (dir string ) ([]Skill , error ) {
812+ all , err := DiscoverLocalSkillsWithOptions (dir , DiscoverOptions {})
813+ if err != nil {
814+ return nil , err
815+ }
816+ var skills []Skill
817+ for _ , s := range all {
818+ if ! s .IsHiddenDirConvention () {
819+ skills = append (skills , s )
820+ }
821+ }
822+ if len (skills ) == 0 {
823+ return nil , fmt .Errorf (
824+ "no skills found in %s\n " +
825+ " Expected SKILL.md in the directory, or skills in skills/*/SKILL.md,\n " +
826+ " skills/{scope}/*/SKILL.md, */SKILL.md, or plugins/*/skills/*/SKILL.md" ,
827+ dir ,
828+ )
829+ }
830+ return skills , nil
831+ }
832+
833+ // DiscoverLocalSkillsWithOptions finds skills in a local directory using the
834+ // same conventions as remote discovery, with configurable discovery behavior.
835+ func DiscoverLocalSkillsWithOptions (dir string , opts DiscoverOptions ) ([]Skill , error ) {
709836 absDir , err := filepath .Abs (dir )
710837 if err != nil {
711838 return nil , fmt .Errorf ("could not resolve path: %w" , err )
@@ -751,6 +878,9 @@ func DiscoverLocalSkills(dir string) ([]Skill, error) {
751878
752879 entry := treeEntry {Path : relPath , Type : "blob" }
753880 m := matchSkillConventions (entry )
881+ if m == nil {
882+ m = matchHiddenDirConventions (entry )
883+ }
754884 if m == nil {
755885 return nil
756886 }
0 commit comments