@@ -119,6 +119,35 @@ pub(crate) fn detect_client(path: &Path) -> SkillClient {
119119 SkillClient :: Unknown
120120}
121121
122+ /// Return whether `path` is a skill owned by a Claude Code plugin.
123+ ///
124+ /// Claude plugins may expose either a root `SKILL.md` or skills below the
125+ /// plugin's `skills/` directory. Require the documented sibling manifest so a
126+ /// generic `skills/foo/SKILL.md` tree is not misclassified as Claude-owned.
127+ fn is_claude_plugin_skill ( path : & Path , config : & LintConfig ) -> bool {
128+ let Some ( parent) = path. parent ( ) else {
129+ return false ;
130+ } ;
131+
132+ let has_manifest = |root : & Path | {
133+ config
134+ . fs ( )
135+ . is_file ( & root. join ( ".claude-plugin" ) . join ( "plugin.json" ) )
136+ } ;
137+
138+ if path. file_name ( ) . and_then ( |name| name. to_str ( ) ) == Some ( "SKILL.md" ) && has_manifest ( parent) {
139+ return true ;
140+ }
141+
142+ for ancestor in parent. ancestors ( ) {
143+ if ancestor. file_name ( ) . and_then ( |name| name. to_str ( ) ) == Some ( "skills" ) {
144+ return ancestor. parent ( ) . is_some_and ( has_manifest) ;
145+ }
146+ }
147+
148+ false
149+ }
150+
122151/// Map a `tools = [...]` entry (or `--tools` value) to a [`SkillClient`].
123152/// Matching is case-insensitive to tolerate configs that use different casing.
124153fn skill_client_from_tool_str ( tool : & str ) -> Option < SkillClient > {
@@ -151,6 +180,9 @@ pub(crate) fn resolve_skill_client(path: &Path, config: &LintConfig) -> SkillCli
151180 if by_path != SkillClient :: Unknown {
152181 return by_path;
153182 }
183+ if is_claude_plugin_skill ( path, config) {
184+ return SkillClient :: ClaudeCode ;
185+ }
154186
155187 let mut from_tools = config
156188 . tools ( )
@@ -437,6 +469,7 @@ mod tests {
437469 use super :: * ;
438470 use crate :: config:: LintConfig ;
439471 use crate :: rules:: Validator ;
472+ use std:: fs;
440473
441474 fn make_skill ( frontmatter : & str , body : & str ) -> String {
442475 format ! ( "---\n {}\n ---\n {}" , frontmatter, body)
@@ -552,6 +585,39 @@ mod tests {
552585 ) ;
553586 }
554587
588+ #[ test]
589+ fn test_resolve_skill_client_claude_plugin_layouts ( ) {
590+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
591+ let plugin_root = temp. path ( ) . join ( "my-plugin" ) ;
592+ let manifest = plugin_root. join ( ".claude-plugin" ) . join ( "plugin.json" ) ;
593+ fs:: create_dir_all ( manifest. parent ( ) . unwrap ( ) ) . unwrap ( ) ;
594+ fs:: write ( & manifest, "{}" ) . unwrap ( ) ;
595+
596+ assert_eq ! (
597+ resolve_skill_client(
598+ & plugin_root. join( "skills/review/SKILL.md" ) ,
599+ & LintConfig :: default ( )
600+ ) ,
601+ SkillClient :: ClaudeCode
602+ ) ;
603+ assert_eq ! (
604+ resolve_skill_client( & plugin_root. join( "SKILL.md" ) , & LintConfig :: default ( ) ) ,
605+ SkillClient :: ClaudeCode
606+ ) ;
607+ }
608+
609+ #[ test]
610+ fn test_resolve_skill_client_generic_skills_without_plugin_manifest ( ) {
611+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
612+ assert_eq ! (
613+ resolve_skill_client(
614+ & temp. path( ) . join( "skills/review/SKILL.md" ) ,
615+ & LintConfig :: default ( )
616+ ) ,
617+ SkillClient :: Unknown
618+ ) ;
619+ }
620+
555621 // ===== Validation tests =====
556622
557623 #[ test]
0 commit comments