@@ -399,6 +399,7 @@ impl SkillHubClient {
399399
400400 /// Remove an installed skill
401401 pub fn remove ( & self , name : & str ) -> Result < bool > {
402+ validate_skill_name ( name) ?;
402403 let mut removed = false ;
403404 let skill_dir = self . config . managed_root . join ( name) ;
404405 if skill_dir. exists ( ) {
@@ -654,4 +655,37 @@ mod tests {
654655 "partial install directory should be removed on failure"
655656 ) ;
656657 }
658+
659+ #[ test]
660+ fn remove_rejects_path_traversal_name ( ) {
661+ let cache_root = TempDir :: new ( ) . unwrap ( ) ;
662+ let managed_root = TempDir :: new ( ) . unwrap ( ) ;
663+ let config = SkillHubClientConfig :: for_mofaclaw ( )
664+ . with_managed_root ( managed_root. path ( ) )
665+ . with_cache_root ( cache_root. path ( ) ) ;
666+ let client = SkillHubClient :: new ( config) . unwrap ( ) ;
667+
668+ let err = client. remove ( "../outside" ) . unwrap_err ( ) ;
669+ assert ! (
670+ err. to_string( ) . contains( "invalid skill name" ) ,
671+ "unexpected error: {err}"
672+ ) ;
673+ assert ! (
674+ !managed_root. path( ) . join( "outside" ) . exists( ) ,
675+ "path traversal should not remove files outside managed root"
676+ ) ;
677+ }
678+
679+ #[ test]
680+ fn remove_returns_false_for_nonexistent_skill ( ) {
681+ let cache_root = TempDir :: new ( ) . unwrap ( ) ;
682+ let managed_root = TempDir :: new ( ) . unwrap ( ) ;
683+ let config = SkillHubClientConfig :: for_mofaclaw ( )
684+ . with_managed_root ( managed_root. path ( ) )
685+ . with_cache_root ( cache_root. path ( ) ) ;
686+ let client = SkillHubClient :: new ( config) . unwrap ( ) ;
687+
688+ let removed = client. remove ( "no-such-skill" ) . unwrap ( ) ;
689+ assert ! ( !removed, "expected false when skill does not exist" ) ;
690+ }
657691}
0 commit comments