Skip to content

Commit 888c276

Browse files
hugoncostaHugo Costa
andauthored
feat: add Stop hook (#3070)
Co-authored-by: Hugo Costa <[email protected]>
1 parent 1a915f2 commit 888c276

File tree

5 files changed

+80
-0
lines changed

5 files changed

+80
-0
lines changed

crates/chat-cli/src/cli/agent/hook.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ pub enum HookTrigger {
2121
PreToolUse,
2222
/// Triggered after tool execution
2323
PostToolUse,
24+
/// Triggered when the assistant finishes responding
25+
Stop,
2426
}
2527

2628
impl Display for HookTrigger {
@@ -30,6 +32,7 @@ impl Display for HookTrigger {
3032
HookTrigger::UserPromptSubmit => write!(f, "userPromptSubmit"),
3133
HookTrigger::PreToolUse => write!(f, "preToolUse"),
3234
HookTrigger::PostToolUse => write!(f, "postToolUse"),
35+
HookTrigger::Stop => write!(f, "stop"),
3336
}
3437
}
3538
}

crates/chat-cli/src/cli/chat/cli/hooks.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ impl HookExecutor {
254254
HookTrigger::UserPromptSubmit => Some(Instant::now() + Duration::from_secs(hook.cache_ttl_seconds)),
255255
HookTrigger::PreToolUse => Some(Instant::now() + Duration::from_secs(hook.cache_ttl_seconds)),
256256
HookTrigger::PostToolUse => Some(Instant::now() + Duration::from_secs(hook.cache_ttl_seconds)),
257+
HookTrigger::Stop => Some(Instant::now() + Duration::from_secs(hook.cache_ttl_seconds)),
257258
},
258259
});
259260
}
@@ -707,4 +708,46 @@ mod tests {
707708
assert_eq!(*exit_code, 2);
708709
assert!(hook_output.contains("Tool execution blocked by security policy"));
709710
}
711+
712+
#[tokio::test]
713+
async fn test_stop_hook() {
714+
let mut executor = HookExecutor::new();
715+
let mut output = Vec::new();
716+
717+
// Create a simple Stop hook that outputs a message
718+
#[cfg(unix)]
719+
let command = "echo 'Turn completed successfully'";
720+
#[cfg(windows)]
721+
let command = "echo Turn completed successfully";
722+
723+
let hook = Hook {
724+
command: command.to_string(),
725+
timeout_ms: 5000,
726+
cache_ttl_seconds: 0,
727+
max_output_size: 1000,
728+
matcher: None, // Stop hooks don't use matchers
729+
source: crate::cli::agent::hook::Source::Session,
730+
};
731+
732+
let hooks = HashMap::from([(HookTrigger::Stop, vec![hook])]);
733+
734+
let results = executor
735+
.run_hooks(
736+
hooks,
737+
&mut output,
738+
".", // cwd
739+
None, // prompt
740+
None, // tool_context - Stop doesn't have tool context
741+
)
742+
.await
743+
.unwrap();
744+
745+
// Should have one result
746+
assert_eq!(results.len(), 1);
747+
748+
let ((trigger, _hook), (exit_code, hook_output)) = &results[0];
749+
assert_eq!(*trigger, HookTrigger::Stop);
750+
assert_eq!(*exit_code, 0);
751+
assert!(hook_output.contains("Turn completed successfully"));
752+
}
710753
}

crates/chat-cli/src/cli/chat/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3034,6 +3034,19 @@ impl ChatSession {
30343034
self.send_chat_telemetry(os, TelemetryResult::Succeeded, None, None, None, true)
30353035
.await;
30363036

3037+
// Run Stop hooks when the assistant finishes responding
3038+
if let Some(cm) = self.conversation.context_manager.as_mut() {
3039+
let _ = cm
3040+
.run_hooks(
3041+
crate::cli::agent::hook::HookTrigger::Stop,
3042+
&mut std::io::stderr(),
3043+
os,
3044+
None,
3045+
None,
3046+
)
3047+
.await;
3048+
}
3049+
30373050
Ok(ChatState::PromptUser {
30383051
skip_printing_tools: false,
30393052
})

docs/agent-format.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ Available hook triggers:
302302
- `userPromptSubmit`: Triggered when the user submits a message.
303303
- `preToolUse`: Triggered before a tool is executed. Can block the tool use.
304304
- `postToolUse`: Triggered after a tool is executed.
305+
- `stop`: Triggered when the assistant finishes responding.
305306

306307
## UseLegacyMcpJson Field
307308

docs/hooks.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,26 @@ Runs after tool execution with access to tool results.
133133
- **0**: Hook succeeded.
134134
- **Other**: Show STDERR warning to user. Tool already ran.
135135

136+
### Stop
137+
138+
Runs when the assistant finishes responding to the user (at the end of each turn).
139+
This is useful for running post-processing tasks like code compilation, testing, formatting,
140+
or cleanup after the assistant's response.
141+
142+
**Hook Event**
143+
```json
144+
{
145+
"hook_event_name": "stop",
146+
"cwd": "/current/working/directory"
147+
}
148+
```
149+
150+
**Exit Code Behavior:**
151+
- **0**: Hook succeeded.
152+
- **Other**: Show STDERR warning to user.
153+
154+
**Note**: Stop hooks do not use matchers since they don't relate to specific tools.
155+
136156
### MCP Example
137157

138158
For MCP tools, the tool name includes the full namespaced format including the MCP Server name:

0 commit comments

Comments
 (0)