@@ -33,7 +33,7 @@ pub use validator::{
3333 DefaultSkillValidator , SkillValidationError , SkillValidator , ValidationErrorKind ,
3434} ;
3535
36- use serde:: { Deserialize , Serialize } ;
36+ use serde:: { de , Deserialize , Deserializer , Serialize } ;
3737use std:: collections:: HashSet ;
3838use std:: path:: Path ;
3939
@@ -101,7 +101,11 @@ pub struct Skill {
101101 pub description : String ,
102102
103103 /// Allowed tools (Claude Code format: "Bash(pattern:*), read(*)")
104- #[ serde( default , rename = "allowed-tools" ) ]
104+ #[ serde(
105+ default ,
106+ rename = "allowed-tools" ,
107+ deserialize_with = "deserialize_allowed_tools"
108+ ) ]
105109 pub allowed_tools : Option < String > ,
106110
107111 /// Whether to disable model invocation
@@ -183,11 +187,25 @@ impl Skill {
183187 return permissions;
184188 } ;
185189
186- // Parse comma-separated tool permissions
187- for part in allowed. split ( ',' ) {
190+ // Parse Claude-style comma-separated permissions, plus legacy
191+ // whitespace-only tool lists such as "Read Write Edit Bash".
192+ let parts: Vec < & str > = if allowed. contains ( ',' ) {
193+ allowed. split ( ',' ) . collect ( )
194+ } else {
195+ allowed. split_whitespace ( ) . collect ( )
196+ } ;
197+ for part in parts {
188198 let part = part. trim ( ) ;
199+ if part. is_empty ( ) {
200+ continue ;
201+ }
189202 if let Some ( perm) = ToolPermission :: parse ( part) {
190203 permissions. insert ( perm) ;
204+ } else {
205+ permissions. insert ( ToolPermission {
206+ tool : part. to_string ( ) ,
207+ pattern : "*" . to_string ( ) ,
208+ } ) ;
191209 }
192210 }
193211
@@ -218,6 +236,34 @@ impl Skill {
218236 }
219237}
220238
239+ fn deserialize_allowed_tools < ' de , D > ( deserializer : D ) -> Result < Option < String > , D :: Error >
240+ where
241+ D : Deserializer < ' de > ,
242+ {
243+ let value = Option :: < serde_yaml:: Value > :: deserialize ( deserializer) ?;
244+ match value {
245+ None | Some ( serde_yaml:: Value :: Null ) => Ok ( None ) ,
246+ Some ( serde_yaml:: Value :: String ( s) ) => Ok ( Some ( s) ) ,
247+ Some ( serde_yaml:: Value :: Sequence ( items) ) => {
248+ let mut tools = Vec :: new ( ) ;
249+ for item in items {
250+ match item {
251+ serde_yaml:: Value :: String ( s) => tools. push ( s) ,
252+ other => {
253+ return Err ( de:: Error :: custom ( format ! (
254+ "allowed-tools list entries must be strings, got {other:?}"
255+ ) ) ) ;
256+ }
257+ }
258+ }
259+ Ok ( Some ( tools. join ( ", " ) ) )
260+ }
261+ Some ( other) => Err ( de:: Error :: custom ( format ! (
262+ "allowed-tools must be a string or a list of strings, got {other:?}"
263+ ) ) ) ,
264+ }
265+ }
266+
221267#[ cfg( test) ]
222268mod tests {
223269 use super :: * ;
@@ -270,6 +316,51 @@ You are a test assistant.
270316 assert_eq ! ( permissions. len( ) , 3 ) ;
271317 }
272318
319+ #[ test]
320+ fn test_parse_legacy_whitespace_allowed_tools ( ) {
321+ let skill = Skill {
322+ name : "test" . to_string ( ) ,
323+ description : "test" . to_string ( ) ,
324+ allowed_tools : Some ( "Read Write Edit Bash" . to_string ( ) ) ,
325+ disable_model_invocation : false ,
326+ kind : SkillKind :: Instruction ,
327+ content : String :: new ( ) ,
328+ tags : Vec :: new ( ) ,
329+ version : None ,
330+ } ;
331+
332+ let permissions = skill. parse_allowed_tools ( ) ;
333+ assert_eq ! ( permissions. len( ) , 4 ) ;
334+ assert ! ( permissions
335+ . iter( )
336+ . any( |perm| perm. tool == "Bash" && perm. pattern == "*" ) ) ;
337+ }
338+
339+ #[ test]
340+ fn test_parse_allowed_tools_yaml_list ( ) {
341+ let content = r#"---
342+ name: test-skill
343+ description: A test skill
344+ allowed-tools:
345+ - Read
346+ - Write
347+ - Bash(uv run skills analyze-ci:*)
348+ ---
349+ # Instructions
350+ "# ;
351+
352+ let skill = Skill :: parse ( content) . unwrap ( ) ;
353+ assert_eq ! (
354+ skill. allowed_tools. as_deref( ) ,
355+ Some ( "Read, Write, Bash(uv run skills analyze-ci:*)" )
356+ ) ;
357+ let permissions = skill. parse_allowed_tools ( ) ;
358+ assert_eq ! ( permissions. len( ) , 3 ) ;
359+ assert ! ( permissions
360+ . iter( )
361+ . any( |perm| perm. tool == "Read" && perm. pattern == "*" ) ) ;
362+ }
363+
273364 #[ test]
274365 fn test_is_tool_allowed ( ) {
275366 let skill = Skill {
0 commit comments