Skip to content

Commit 42dfcd9

Browse files
committed
feat(hooks): add ToolError hook and new HookEvent variants
- Add SessionStart, SessionEnd, PermissionRequest, PermissionDenied, ToolError to HookEvent enum - Wire ToolError hook in Registry::execute() error path using fire-and-forget tokio::spawn - Add parsing for new event names in HookEvent::parse() - Follow existing PreToolUse/PostToolUse hook pattern
1 parent 373f310 commit 42dfcd9

2 files changed

Lines changed: 69 additions & 0 deletions

File tree

src/hooks/config.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ pub enum HookEvent {
3131
PostSession,
3232
/// On any error
3333
Error,
34+
/// Session has started
35+
SessionStart,
36+
/// Session has ended
37+
SessionEnd,
38+
/// Permission requested
39+
PermissionRequest,
40+
/// Permission denied
41+
PermissionDenied,
42+
/// Tool execution error
43+
ToolError,
3444
/// Custom event type
3545
Custom(String),
3646
}
@@ -44,6 +54,11 @@ impl HookEvent {
4454
"presession" | "pre_session" => Some(HookEvent::PreSession),
4555
"postsession" | "post_session" => Some(HookEvent::PostSession),
4656
"error" => Some(HookEvent::Error),
57+
"sessionstart" | "session_start" => Some(HookEvent::SessionStart),
58+
"sessionend" | "session_end" => Some(HookEvent::SessionEnd),
59+
"permissionrequest" | "permission_request" => Some(HookEvent::PermissionRequest),
60+
"permissiondenied" | "permission_denied" => Some(HookEvent::PermissionDenied),
61+
"toolerror" | "tool_error" => Some(HookEvent::ToolError),
4762
s if s.starts_with("custom:") => Some(HookEvent::Custom(s[7..].to_string())),
4863
s if s.starts_with("custom") => Some(HookEvent::Custom(s.to_string())),
4964
_ => None,

src/tool/mod.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,60 @@ impl Registry {
608608
fields.push(("elapsed_ms".to_string(), latency_ms.to_string()));
609609
fields.push(("error".to_string(), crate::util::format_error_chain(&error)));
610610
crate::logging::event_warn("TOOL_LIFECYCLE", fields);
611+
612+
// --- TOOL ERROR HOOK ---
613+
// Fire-and-forget error notification hook
614+
{
615+
let cwd = ctx.working_dir
616+
.as_ref()
617+
.map(|p| p.display().to_string())
618+
.unwrap_or_else(|| std::env::current_dir()
619+
.map(|p| p.display().to_string())
620+
.unwrap_or_default());
621+
622+
let transcript_path = format!(
623+
"{}/.jcode/sessions/{}/transcript.jsonl",
624+
std::env::var("HOME").unwrap_or_default(),
625+
ctx.session_id
626+
);
627+
628+
let hook_input = HookInput::for_tool_error(
629+
ctx.session_id.clone(),
630+
resolved_name.to_string(),
631+
crate::util::format_error_chain(&error),
632+
);
633+
634+
let hook_ctx = HookContext::for_tool_error(
635+
resolved_name.to_string(),
636+
ctx.session_id.clone(),
637+
cwd,
638+
);
639+
640+
let config = load_hooks_config();
641+
let registry = HookRegistry::from_config(config);
642+
let matching = registry.get_matching(&HookEvent::ToolError, &hook_ctx);
643+
let handlers: Vec<_> = matching.into_iter().cloned().collect();
644+
for handler in handlers {
645+
let hook_input = hook_input.clone();
646+
tokio::spawn(async move {
647+
match execute_hook(&handler, &hook_input).await {
648+
Ok(HookResult::Blocked { reason, .. }) => {
649+
debug!("ToolError hook blocked: {}", reason);
650+
}
651+
Ok(HookResult::Failed { error }) => {
652+
debug!("ToolError hook failed: {}", error);
653+
}
654+
Ok(HookResult::Continue(_)) => {
655+
debug!("ToolError hook completed");
656+
}
657+
Err(e) => {
658+
debug!("ToolError hook error: {}", e);
659+
}
660+
}
661+
});
662+
}
663+
}
664+
611665
return Err(error);
612666
}
613667
};

0 commit comments

Comments
 (0)