diff --git a/crates/chat-cli/src/cli/agent/hook.rs b/crates/chat-cli/src/cli/agent/hook.rs index 89ca74146..0ad4dba48 100644 --- a/crates/chat-cli/src/cli/agent/hook.rs +++ b/crates/chat-cli/src/cli/agent/hook.rs @@ -21,6 +21,8 @@ pub enum HookTrigger { PreToolUse, /// Triggered after tool execution PostToolUse, + /// Triggered when the assistant finishes responding + Stop, } impl Display for HookTrigger { @@ -30,6 +32,7 @@ impl Display for HookTrigger { HookTrigger::UserPromptSubmit => write!(f, "userPromptSubmit"), HookTrigger::PreToolUse => write!(f, "preToolUse"), HookTrigger::PostToolUse => write!(f, "postToolUse"), + HookTrigger::Stop => write!(f, "stop"), } } } diff --git a/crates/chat-cli/src/cli/chat/cli/hooks.rs b/crates/chat-cli/src/cli/chat/cli/hooks.rs index e391d9823..28207d0ac 100644 --- a/crates/chat-cli/src/cli/chat/cli/hooks.rs +++ b/crates/chat-cli/src/cli/chat/cli/hooks.rs @@ -254,6 +254,7 @@ impl HookExecutor { HookTrigger::UserPromptSubmit => Some(Instant::now() + Duration::from_secs(hook.cache_ttl_seconds)), HookTrigger::PreToolUse => Some(Instant::now() + Duration::from_secs(hook.cache_ttl_seconds)), HookTrigger::PostToolUse => Some(Instant::now() + Duration::from_secs(hook.cache_ttl_seconds)), + HookTrigger::Stop => Some(Instant::now() + Duration::from_secs(hook.cache_ttl_seconds)), }, }); } @@ -707,4 +708,46 @@ mod tests { assert_eq!(*exit_code, 2); assert!(hook_output.contains("Tool execution blocked by security policy")); } + + #[tokio::test] + async fn test_stop_hook() { + let mut executor = HookExecutor::new(); + let mut output = Vec::new(); + + // Create a simple Stop hook that outputs a message + #[cfg(unix)] + let command = "echo 'Turn completed successfully'"; + #[cfg(windows)] + let command = "echo Turn completed successfully"; + + let hook = Hook { + command: command.to_string(), + timeout_ms: 5000, + cache_ttl_seconds: 0, + max_output_size: 1000, + matcher: None, // Stop hooks don't use matchers + source: crate::cli::agent::hook::Source::Session, + }; + + let hooks = HashMap::from([(HookTrigger::Stop, vec![hook])]); + + let results = executor + .run_hooks( + hooks, + &mut output, + ".", // cwd + None, // prompt + None, // tool_context - Stop doesn't have tool context + ) + .await + .unwrap(); + + // Should have one result + assert_eq!(results.len(), 1); + + let ((trigger, _hook), (exit_code, hook_output)) = &results[0]; + assert_eq!(*trigger, HookTrigger::Stop); + assert_eq!(*exit_code, 0); + assert!(hook_output.contains("Turn completed successfully")); + } } diff --git a/crates/chat-cli/src/cli/chat/mod.rs b/crates/chat-cli/src/cli/chat/mod.rs index 2aea9921e..aa8f35162 100644 --- a/crates/chat-cli/src/cli/chat/mod.rs +++ b/crates/chat-cli/src/cli/chat/mod.rs @@ -3034,6 +3034,19 @@ impl ChatSession { self.send_chat_telemetry(os, TelemetryResult::Succeeded, None, None, None, true) .await; + // Run Stop hooks when the assistant finishes responding + if let Some(cm) = self.conversation.context_manager.as_mut() { + let _ = cm + .run_hooks( + crate::cli::agent::hook::HookTrigger::Stop, + &mut std::io::stderr(), + os, + None, + None, + ) + .await; + } + Ok(ChatState::PromptUser { skip_printing_tools: false, }) diff --git a/docs/agent-format.md b/docs/agent-format.md index 35adc8cfa..9005eb47c 100644 --- a/docs/agent-format.md +++ b/docs/agent-format.md @@ -302,6 +302,7 @@ Available hook triggers: - `userPromptSubmit`: Triggered when the user submits a message. - `preToolUse`: Triggered before a tool is executed. Can block the tool use. - `postToolUse`: Triggered after a tool is executed. +- `stop`: Triggered when the assistant finishes responding. ## UseLegacyMcpJson Field diff --git a/docs/hooks.md b/docs/hooks.md index d7cfa3d50..f342780c6 100644 --- a/docs/hooks.md +++ b/docs/hooks.md @@ -133,6 +133,26 @@ Runs after tool execution with access to tool results. - **0**: Hook succeeded. - **Other**: Show STDERR warning to user. Tool already ran. +### Stop + +Runs when the assistant finishes responding to the user (at the end of each turn). +This is useful for running post-processing tasks like code compilation, testing, formatting, +or cleanup after the assistant's response. + +**Hook Event** +```json +{ + "hook_event_name": "stop", + "cwd": "/current/working/directory" +} +``` + +**Exit Code Behavior:** +- **0**: Hook succeeded. +- **Other**: Show STDERR warning to user. + +**Note**: Stop hooks do not use matchers since they don't relate to specific tools. + ### MCP Example For MCP tools, the tool name includes the full namespaced format including the MCP Server name: