@@ -92,6 +92,8 @@ enum FieldType {
9292 Bool ,
9393 Object ,
9494 StringArray ,
95+ HookArray ,
96+ RulesImport ,
9597 Number ,
9698}
9799
@@ -102,6 +104,8 @@ impl FieldType {
102104 Self :: Bool => "a boolean" ,
103105 Self :: Object => "an object" ,
104106 Self :: StringArray => "an array of strings" ,
107+ Self :: RulesImport => "a string or an array of strings" ,
108+ Self :: HookArray => "an array of strings or hook objects" ,
105109 Self :: Number => "a number" ,
106110 }
107111 }
@@ -114,6 +118,13 @@ impl FieldType {
114118 Self :: StringArray => value
115119 . as_array ( )
116120 . is_some_and ( |arr| arr. iter ( ) . all ( |v| v. as_str ( ) . is_some ( ) ) ) ,
121+ Self :: HookArray => true ,
122+ Self :: RulesImport => {
123+ value. as_str ( ) . is_some ( )
124+ || value
125+ . as_array ( )
126+ . is_some_and ( |arr| arr. iter ( ) . all ( |v| v. as_str ( ) . is_some ( ) ) )
127+ }
117128 Self :: Number => value. as_i64 ( ) . is_some ( ) ,
118129 }
119130 }
@@ -201,6 +212,10 @@ const TOP_LEVEL_FIELDS: &[FieldSpec] = &[
201212 name : "provider" ,
202213 expected : FieldType :: Object ,
203214 } ,
215+ FieldSpec {
216+ name : "rulesImport" ,
217+ expected : FieldType :: RulesImport ,
218+ } ,
204219 FieldSpec {
205220 name : "subagentModel" ,
206221 expected : FieldType :: String ,
@@ -210,15 +225,15 @@ const TOP_LEVEL_FIELDS: &[FieldSpec] = &[
210225const HOOKS_FIELDS : & [ FieldSpec ] = & [
211226 FieldSpec {
212227 name : "PreToolUse" ,
213- expected : FieldType :: StringArray ,
228+ expected : FieldType :: HookArray ,
214229 } ,
215230 FieldSpec {
216231 name : "PostToolUse" ,
217- expected : FieldType :: StringArray ,
232+ expected : FieldType :: HookArray ,
218233 } ,
219234 FieldSpec {
220235 name : "PostToolUseFailure" ,
221- expected : FieldType :: StringArray ,
236+ expected : FieldType :: HookArray ,
222237 } ,
223238] ;
224239
@@ -410,9 +425,8 @@ fn validate_object_keys(
410425 } else if DEPRECATED_FIELDS . iter ( ) . any ( |d| d. name == key) {
411426 // Deprecated key — handled separately, not an unknown-key error.
412427 } else {
413- // Unknown key.
414428 let suggestion = suggest_field ( key, & known_names) ;
415- result. errors . push ( ConfigDiagnostic {
429+ result. warnings . push ( ConfigDiagnostic {
416430 path : path_display. to_string ( ) ,
417431 field : field_path,
418432 line : find_key_line ( source, key) ,
@@ -591,10 +605,11 @@ mod tests {
591605 let result = validate_config_file ( object, source, & test_path ( ) ) ;
592606
593607 // then
594- assert_eq ! ( result. errors. len( ) , 1 ) ;
595- assert_eq ! ( result. errors[ 0 ] . field, "unknownField" ) ;
608+ assert ! ( result. errors. is_empty( ) ) ;
609+ assert_eq ! ( result. warnings. len( ) , 1 ) ;
610+ assert_eq ! ( result. warnings[ 0 ] . field, "unknownField" ) ;
596611 assert ! ( matches!(
597- result. errors [ 0 ] . kind,
612+ result. warnings [ 0 ] . kind,
598613 DiagnosticKind :: UnknownKey { .. }
599614 ) ) ;
600615 }
@@ -674,9 +689,10 @@ mod tests {
674689 let result = validate_config_file ( object, source, & test_path ( ) ) ;
675690
676691 // then
677- assert_eq ! ( result. errors. len( ) , 1 ) ;
678- assert_eq ! ( result. errors[ 0 ] . line, Some ( 3 ) ) ;
679- assert_eq ! ( result. errors[ 0 ] . field, "badKey" ) ;
692+ assert ! ( result. errors. is_empty( ) ) ;
693+ assert_eq ! ( result. warnings. len( ) , 1 ) ;
694+ assert_eq ! ( result. warnings[ 0 ] . line, Some ( 3 ) ) ;
695+ assert_eq ! ( result. warnings[ 0 ] . field, "badKey" ) ;
680696 }
681697
682698 #[ test]
@@ -697,16 +713,72 @@ mod tests {
697713 #[ test]
698714 fn validates_nested_hooks_keys ( ) {
699715 // given
700- let source = r#"{"hooks": {"PreToolUse": [" cmd"], "BadHook": ["x"]}}"# ;
716+ let source = r#"{"hooks": {"PreToolUse": [{"hooks":[{"type":"command","command":" cmd"}]} ], "BadHook": ["x"]}}"# ;
701717 let parsed = JsonValue :: parse ( source) . expect ( "valid json" ) ;
702718 let object = parsed. as_object ( ) . expect ( "object" ) ;
703719
704720 // when
705721 let result = validate_config_file ( object, source, & test_path ( ) ) ;
706722
707723 // then
724+ assert ! ( result. errors. is_empty( ) ) ;
725+ assert_eq ! (
726+ result. warnings. len( ) ,
727+ 1 ,
728+ "expected only the unknown key warning, got {:?}" ,
729+ result. warnings
730+ ) ;
731+ assert_eq ! ( result. warnings[ 0 ] . field, "hooks.BadHook" ) ;
732+ }
733+
734+ #[ test]
735+ fn validates_object_style_hook_entries ( ) {
736+ let source = r#"{"hooks":{"PreToolUse":["legacy",{"matcher":"Bash","hooks":[{"type":"command","command":"echo ok"}]}]}}"# ;
737+ let parsed = JsonValue :: parse ( source) . expect ( "valid json" ) ;
738+ let object = parsed. as_object ( ) . expect ( "object" ) ;
739+
740+ let result = validate_config_file ( object, source, & test_path ( ) ) ;
741+
742+ assert ! ( result. errors. is_empty( ) , "{:?}" , result. errors) ;
743+ }
744+
745+ #[ test]
746+ fn allows_wrong_hook_entry_types_for_partial_runtime_validation_441 ( ) {
747+ let source = r#"{"hooks":{"PreToolUse":[42]}}"# ;
748+ let parsed = JsonValue :: parse ( source) . expect ( "valid json" ) ;
749+ let object = parsed. as_object ( ) . expect ( "object" ) ;
750+
751+ let result = validate_config_file ( object, source, & test_path ( ) ) ;
752+
753+ assert ! ( result. errors. is_empty( ) , "{:?}" , result. errors) ;
754+ }
755+
756+ #[ test]
757+ fn validates_rules_import_string_and_array_forms ( ) {
758+ for source in [
759+ r#"{"rulesImport":"auto"}"# ,
760+ r#"{"rulesImport":"none"}"# ,
761+ r#"{"rulesImport":["cursor","copilot"]}"# ,
762+ ] {
763+ let parsed = JsonValue :: parse ( source) . expect ( "valid json" ) ;
764+ let object = parsed. as_object ( ) . expect ( "object" ) ;
765+
766+ let result = validate_config_file ( object, source, & test_path ( ) ) ;
767+
768+ assert ! ( result. errors. is_empty( ) , "{source}: {:?}" , result. errors) ;
769+ }
770+ }
771+
772+ #[ test]
773+ fn rejects_rules_import_wrong_type ( ) {
774+ let source = r#"{"rulesImport":42}"# ;
775+ let parsed = JsonValue :: parse ( source) . expect ( "valid json" ) ;
776+ let object = parsed. as_object ( ) . expect ( "object" ) ;
777+
778+ let result = validate_config_file ( object, source, & test_path ( ) ) ;
779+
708780 assert_eq ! ( result. errors. len( ) , 1 ) ;
709- assert_eq ! ( result. errors[ 0 ] . field, "hooks.BadHook " ) ;
781+ assert_eq ! ( result. errors[ 0 ] . field, "rulesImport " ) ;
710782 }
711783
712784 #[ test]
@@ -720,8 +792,9 @@ mod tests {
720792 let result = validate_config_file ( object, source, & test_path ( ) ) ;
721793
722794 // then
723- assert_eq ! ( result. errors. len( ) , 1 ) ;
724- assert_eq ! ( result. errors[ 0 ] . field, "permissions.denyAll" ) ;
795+ assert ! ( result. errors. is_empty( ) ) ;
796+ assert_eq ! ( result. warnings. len( ) , 1 ) ;
797+ assert_eq ! ( result. warnings[ 0 ] . field, "permissions.denyAll" ) ;
725798 }
726799
727800 #[ test]
@@ -735,8 +808,9 @@ mod tests {
735808 let result = validate_config_file ( object, source, & test_path ( ) ) ;
736809
737810 // then
738- assert_eq ! ( result. errors. len( ) , 1 ) ;
739- assert_eq ! ( result. errors[ 0 ] . field, "sandbox.containerMode" ) ;
811+ assert ! ( result. errors. is_empty( ) ) ;
812+ assert_eq ! ( result. warnings. len( ) , 1 ) ;
813+ assert_eq ! ( result. warnings[ 0 ] . field, "sandbox.containerMode" ) ;
740814 }
741815
742816 #[ test]
@@ -750,8 +824,9 @@ mod tests {
750824 let result = validate_config_file ( object, source, & test_path ( ) ) ;
751825
752826 // then
753- assert_eq ! ( result. errors. len( ) , 1 ) ;
754- assert_eq ! ( result. errors[ 0 ] . field, "plugins.autoUpdate" ) ;
827+ assert ! ( result. errors. is_empty( ) ) ;
828+ assert_eq ! ( result. warnings. len( ) , 1 ) ;
829+ assert_eq ! ( result. warnings[ 0 ] . field, "plugins.autoUpdate" ) ;
755830 }
756831
757832 #[ test]
@@ -765,16 +840,17 @@ mod tests {
765840 let result = validate_config_file ( object, source, & test_path ( ) ) ;
766841
767842 // then
768- assert_eq ! ( result. errors. len( ) , 1 ) ;
769- assert_eq ! ( result. errors[ 0 ] . field, "oauth.secret" ) ;
843+ assert ! ( result. errors. is_empty( ) ) ;
844+ assert_eq ! ( result. warnings. len( ) , 1 ) ;
845+ assert_eq ! ( result. warnings[ 0 ] . field, "oauth.secret" ) ;
770846 }
771847
772848 #[ test]
773849 fn valid_config_produces_no_diagnostics ( ) {
774850 // given
775851 let source = r#"{
776852 "model": "opus",
777- "hooks": {"PreToolUse": [" guard"]},
853+ "hooks": {"PreToolUse": [{"hooks":[{"type":"command","command":" guard"}]} ]},
778854 "permissions": {"defaultMode": "plan", "allow": ["Read"]},
779855 "mcpServers": {},
780856 "sandbox": {"enabled": false}
@@ -801,8 +877,9 @@ mod tests {
801877 let result = validate_config_file ( object, source, & test_path ( ) ) ;
802878
803879 // then
804- assert_eq ! ( result. errors. len( ) , 1 ) ;
805- match & result. errors [ 0 ] . kind {
880+ assert ! ( result. errors. is_empty( ) ) ;
881+ assert_eq ! ( result. warnings. len( ) , 1 ) ;
882+ match & result. warnings [ 0 ] . kind {
806883 DiagnosticKind :: UnknownKey {
807884 suggestion : Some ( s) ,
808885 } => assert_eq ! ( s, "model" ) ,
@@ -813,7 +890,7 @@ mod tests {
813890 #[ test]
814891 fn format_diagnostics_includes_all_entries ( ) {
815892 // given
816- let source = r#"{"permissionMode ": "plan" , "badKey": 1}"# ;
893+ let source = r#"{"model ": 42 , "badKey": 1}"# ;
817894 let parsed = JsonValue :: parse ( source) . expect ( "valid json" ) ;
818895 let object = parsed. as_object ( ) . expect ( "object" ) ;
819896 let result = validate_config_file ( object, source, & test_path ( ) ) ;
@@ -825,7 +902,7 @@ mod tests {
825902 assert ! ( output. contains( "warning:" ) ) ;
826903 assert ! ( output. contains( "error:" ) ) ;
827904 assert ! ( output. contains( "badKey" ) ) ;
828- assert ! ( output. contains( "permissionMode " ) ) ;
905+ assert ! ( output. contains( "model " ) ) ;
829906 }
830907
831908 #[ test]
0 commit comments