@@ -111,6 +111,13 @@ pub struct Entry {
111111 // notifications, window titles, or bells without a controlling terminal.
112112 #[ serde( default , rename = "terminalSequence" ) ]
113113 pub terminal_sequence : Option < String > ,
114+ // Present in Stop and SubagentStop hook input payloads (v2.1.145+). Claude Code includes
115+ // currently-running background task descriptors and session-scoped cron jobs so hooks can
116+ // inspect or block on them before the session exits.
117+ #[ serde( default , rename = "background_tasks" ) ]
118+ pub background_tasks : Option < Value > ,
119+ #[ serde( default , rename = "session_crons" ) ]
120+ pub session_crons : Option < Value > ,
114121}
115122
116123#[ derive( Debug , Deserialize , Default ) ]
@@ -621,6 +628,96 @@ mod tests {
621628 ) ;
622629 }
623630
631+ // --- Issue #106: v2.1.145+ Stop/SubagentStop gain background_tasks and session_crons ---
632+
633+ #[ test]
634+ fn parse_entry_captures_background_tasks_and_session_crons_v2_1_145 ( ) {
635+ // v2.1.145+: Stop and SubagentStop hook input payloads include background_tasks
636+ // (array of running task descriptors) and session_crons (array of registered cron jobs).
637+ // Both must be captured as Value so callers can inspect them.
638+ let line = json ! ( {
639+ "type" : "system" ,
640+ "subtype" : "hook_progress" ,
641+ "uuid" : "stop-hook-uuid-145" ,
642+ "timestamp" : "2026-05-19T10:00:00Z" ,
643+ "hookEvent" : "Stop" ,
644+ "hookName" : "on-stop" ,
645+ "background_tasks" : [ { "id" : "task-1" , "description" : "running bg job" } ] ,
646+ "session_crons" : [ { "id" : "cron-1" , "schedule" : "*/5 * * * *" } ]
647+ } ) ;
648+ let bytes = serde_json:: to_vec ( & line) . unwrap ( ) ;
649+ let entry =
650+ parse_entry ( & bytes) . expect ( "must parse Stop hook entry with new v2.1.145 fields" ) ;
651+
652+ let tasks = entry
653+ . background_tasks
654+ . expect ( "background_tasks must be captured" ) ;
655+ assert ! ( tasks. is_array( ) , "background_tasks must be an array" ) ;
656+ assert_eq ! ( tasks. as_array( ) . unwrap( ) . len( ) , 1 ) ;
657+ assert_eq ! ( tasks[ 0 ] . get( "id" ) . and_then( |v| v. as_str( ) ) , Some ( "task-1" ) ) ;
658+
659+ let crons = entry. session_crons . expect ( "session_crons must be captured" ) ;
660+ assert ! ( crons. is_array( ) , "session_crons must be an array" ) ;
661+ assert_eq ! ( crons. as_array( ) . unwrap( ) . len( ) , 1 ) ;
662+ assert_eq ! (
663+ crons[ 0 ] . get( "schedule" ) . and_then( |v| v. as_str( ) ) ,
664+ Some ( "*/5 * * * *" )
665+ ) ;
666+ }
667+
668+ #[ test]
669+ fn parse_entry_background_tasks_and_session_crons_default_to_none_when_absent ( ) {
670+ // Stop/SubagentStop hook entries from before v2.1.145 have no background_tasks or
671+ // session_crons fields — both must default to None.
672+ let line = json ! ( {
673+ "type" : "system" ,
674+ "subtype" : "hook_progress" ,
675+ "uuid" : "stop-hook-uuid-old" ,
676+ "timestamp" : "2026-05-01T10:00:00Z" ,
677+ "hookEvent" : "Stop" ,
678+ "hookName" : "on-stop"
679+ } ) ;
680+ let bytes = serde_json:: to_vec ( & line) . unwrap ( ) ;
681+ let entry = parse_entry ( & bytes) . expect ( "must parse old Stop hook entry" ) ;
682+ assert ! (
683+ entry. background_tasks. is_none( ) ,
684+ "background_tasks must be None when absent"
685+ ) ;
686+ assert ! (
687+ entry. session_crons. is_none( ) ,
688+ "session_crons must be None when absent"
689+ ) ;
690+ }
691+
692+ #[ test]
693+ fn parse_entry_subagent_stop_with_background_tasks_and_session_crons ( ) {
694+ // SubagentStop hook input also gains these fields in v2.1.145+.
695+ let line = json ! ( {
696+ "type" : "system" ,
697+ "subtype" : "hook_progress" ,
698+ "uuid" : "subagent-stop-uuid-145" ,
699+ "timestamp" : "2026-05-19T11:00:00Z" ,
700+ "hookEvent" : "SubagentStop" ,
701+ "hookName" : "on-subagent-stop" ,
702+ "background_tasks" : [ ] ,
703+ "session_crons" : [ { "id" : "c1" } , { "id" : "c2" } ]
704+ } ) ;
705+ let bytes = serde_json:: to_vec ( & line) . unwrap ( ) ;
706+ let entry =
707+ parse_entry ( & bytes) . expect ( "must parse SubagentStop hook entry with new fields" ) ;
708+
709+ let tasks = entry
710+ . background_tasks
711+ . expect ( "background_tasks must be captured" ) ;
712+ assert ! (
713+ tasks. as_array( ) . unwrap( ) . is_empty( ) ,
714+ "empty array must be preserved"
715+ ) ;
716+
717+ let crons = entry. session_crons . expect ( "session_crons must be captured" ) ;
718+ assert_eq ! ( crons. as_array( ) . unwrap( ) . len( ) , 2 ) ;
719+ }
720+
624721 // --- Issue #85: lone UTF-16 surrogate sanitization ---
625722
626723 #[ test]
0 commit comments