Skip to content

Commit 1617ff7

Browse files
delexwclaude
andauthored
fix(parser): rescue hook_progress entries written as system entries in verbose mode (#22)
Hook events in Claude Code's verbose/stream-json output mode are written as {type:"system", subtype:"hook_progress", hookEvent:..., hookName:...} entries, not as progress entries. Since "system" is in NOISE_ENTRY_TYPES, these were silently dropped, causing hooks to never appear in the GUI. Fix: add subtype/hookEvent/hookName fields to Entry, then rescue system entries with subtype=="hook_progress" or non-empty hookEvent before the noise filter runs. https://claude.ai/code/session_01MK63kGRSmtkGSXCDaqh8p3 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 93036c9 commit 1617ff7

2 files changed

Lines changed: 81 additions & 0 deletions

File tree

src-tauri/src/parser/classify.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,18 @@ pub fn classify(e: Entry) -> Option<ClassifiedMsg> {
178178
}
179179
}
180180

181+
// Rescue hook_progress entries written as type:"system", subtype:"hook_progress"
182+
// in verbose/stream-json mode (Claude Code v2.x). These must be rescued before the
183+
// NOISE_ENTRY_TYPES filter discards all "system" entries.
184+
if e.entry_type == "system" && (e.subtype == "hook_progress" || !e.hook_event.is_empty()) {
185+
return Some(ClassifiedMsg::Hook(HookMsg {
186+
timestamp: ts,
187+
hook_event: e.hook_event.clone(),
188+
hook_name: e.hook_name.clone(),
189+
command: String::new(),
190+
}));
191+
}
192+
181193
// Hard noise: structural metadata types.
182194
if NOISE_ENTRY_TYPES.contains(&e.entry_type.as_str()) {
183195
return None;
@@ -829,6 +841,68 @@ mod tests {
829841
);
830842
}
831843

844+
#[test]
845+
fn classify_rescues_system_hook_progress_subtype() {
846+
// Verbose/stream-json mode: hooks arrive as type:"system", subtype:"hook_progress".
847+
// These must be rescued before the noise filter discards all "system" entries.
848+
let e = Entry {
849+
entry_type: "system".to_string(),
850+
uuid: "uuid-sys-hook".to_string(),
851+
timestamp: "2025-01-15T10:30:00Z".to_string(),
852+
subtype: "hook_progress".to_string(),
853+
hook_event: "PreToolUse".to_string(),
854+
hook_name: "pre-commit".to_string(),
855+
..Default::default()
856+
};
857+
match classify(e) {
858+
Some(ClassifiedMsg::Hook(h)) => {
859+
assert_eq!(h.hook_event, "PreToolUse");
860+
assert_eq!(h.hook_name, "pre-commit");
861+
}
862+
other => panic!(
863+
"Expected Hook for system/hook_progress entry, got {:?}",
864+
other
865+
),
866+
}
867+
}
868+
869+
#[test]
870+
fn classify_rescues_system_entry_with_hook_event_field() {
871+
// Forward-compat: any system entry carrying a hookEvent field is treated as a hook.
872+
let e = Entry {
873+
entry_type: "system".to_string(),
874+
uuid: "uuid-sys-hook2".to_string(),
875+
timestamp: "2025-01-15T10:30:00Z".to_string(),
876+
hook_event: "PostToolUse".to_string(),
877+
hook_name: "post-hook".to_string(),
878+
..Default::default()
879+
};
880+
match classify(e) {
881+
Some(ClassifiedMsg::Hook(h)) => {
882+
assert_eq!(h.hook_event, "PostToolUse");
883+
}
884+
other => panic!(
885+
"Expected Hook for system entry with hookEvent, got {:?}",
886+
other
887+
),
888+
}
889+
}
890+
891+
#[test]
892+
fn classify_drops_plain_system_entry_without_hook_fields() {
893+
// Regular system entries (no subtype/hookEvent) must still be dropped as noise.
894+
let e = Entry {
895+
entry_type: "system".to_string(),
896+
uuid: "uuid-plain-sys".to_string(),
897+
timestamp: "2025-01-15T10:30:00Z".to_string(),
898+
..Default::default()
899+
};
900+
assert!(
901+
classify(e).is_none(),
902+
"Plain system entry must remain noise"
903+
);
904+
}
905+
832906
// --- Unknown / structural entry type tests (compat: v2.1.79-v2.1.83) ---
833907

834908
#[test]

src-tauri/src/parser/entry.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ pub struct Entry {
3939
pub agent_name: String,
4040
#[serde(default)]
4141
pub data: Option<Value>,
42+
// Top-level fields present in system/hook_progress entries (verbose/stream-json mode).
43+
#[serde(default)]
44+
pub subtype: String,
45+
#[serde(default, rename = "hookEvent")]
46+
pub hook_event: String,
47+
#[serde(default, rename = "hookName")]
48+
pub hook_name: String,
4249
}
4350

4451
#[derive(Debug, Deserialize, Default)]

0 commit comments

Comments
 (0)