Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/chat-cli/src/cli/agent/hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ pub enum HookTrigger {
PreToolUse,
/// Triggered after tool execution
PostToolUse,
/// Triggered when the assistant finishes responding
Stop,
}

impl Display for HookTrigger {
Expand All @@ -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"),
}
}
}
Expand Down
43 changes: 43 additions & 0 deletions crates/chat-cli/src/cli/chat/cli/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
},
});
}
Expand Down Expand Up @@ -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"));
}
}
13 changes: 13 additions & 0 deletions crates/chat-cli/src/cli/chat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand Down
1 change: 1 addition & 0 deletions docs/agent-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 20 additions & 0 deletions docs/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down