Activate prompt hooks

This commit is contained in:
Abhinav Vedmala
2026-07-09 18:35:42 -07:00
parent ced3120855
commit 9d14b483ec
13 changed files with 564 additions and 19 deletions
@@ -465,7 +465,7 @@ fn map_hook_handler_to_api(handler: CoreHookHandlerConfig) -> ConfiguredHookHand
r#async,
status_message,
},
CoreHookHandlerConfig::Prompt {} => ConfiguredHookHandler::Prompt {},
CoreHookHandlerConfig::Prompt { .. } => ConfiguredHookHandler::Prompt {},
CoreHookHandlerConfig::Agent {} => ConfiguredHookHandler::Agent {},
}
}
+11 -1
View File
@@ -153,7 +153,17 @@ pub enum HookHandlerConfig {
status_message: Option<String>,
},
#[serde(rename = "prompt")]
Prompt {},
Prompt {
prompt: String,
#[serde(default)]
model: Option<String>,
#[serde(default, rename = "timeout", alias = "timeoutSec")]
timeout_sec: Option<u64>,
#[serde(default, rename = "statusMessage")]
status_message: Option<String>,
#[serde(default, rename = "continueOnBlock")]
continue_on_block: bool,
},
#[serde(rename = "agent")]
Agent {},
}
+22
View File
@@ -1210,6 +1210,27 @@
},
{
"properties": {
"continueOnBlock": {
"default": false,
"type": "boolean"
},
"model": {
"default": null,
"type": "string"
},
"prompt": {
"type": "string"
},
"statusMessage": {
"default": null,
"type": "string"
},
"timeout": {
"default": null,
"format": "uint64",
"minimum": 0.0,
"type": "integer"
},
"type": {
"enum": [
"prompt"
@@ -1218,6 +1239,7 @@
}
},
"required": [
"prompt",
"type"
],
"type": "object"
+18 -3
View File
@@ -272,6 +272,7 @@ pub struct ModelClient {
pub struct ModelClientSession {
client: ModelClient,
websocket_session: WebsocketSession,
cache_websocket_session_on_drop: bool,
/// Turn state for sticky routing.
///
/// This is an `OnceLock` that stores the turn state value received from the server
@@ -480,6 +481,18 @@ impl ModelClient {
ModelClientSession {
client: self.clone(),
websocket_session: self.take_cached_websocket_session(),
cache_websocket_session_on_drop: true,
turn_state: Arc::new(OnceLock::new()),
}
}
/// Creates a fresh streaming session that does not read or write the session's cached
/// WebSocket state.
pub(crate) fn new_isolated_session(&self) -> ModelClientSession {
ModelClientSession {
client: self.clone(),
websocket_session: WebsocketSession::default(),
cache_websocket_session_on_drop: false,
turn_state: Arc::new(OnceLock::new()),
}
}
@@ -1107,9 +1120,11 @@ impl ModelClient {
impl Drop for ModelClientSession {
fn drop(&mut self) {
let websocket_session = std::mem::take(&mut self.websocket_session);
self.client
.store_cached_websocket_session(websocket_session);
if self.cache_websocket_session_on_drop {
let websocket_session = std::mem::take(&mut self.websocket_session);
self.client
.store_cached_websocket_session(websocket_session);
}
}
}
+19
View File
@@ -4,6 +4,7 @@ use super::ModelClient;
use super::PendingUnauthorizedRetry;
use super::Prompt;
use super::UnauthorizedRecoveryExecution;
use super::WebsocketSession;
use super::X_CODEX_INSTALLATION_ID_HEADER;
use super::X_CODEX_PARENT_THREAD_ID_HEADER;
use super::X_CODEX_TURN_METADATA_HEADER;
@@ -276,6 +277,24 @@ fn test_model_info() -> ModelInfo {
.expect("deserialize test model info")
}
#[test]
fn isolated_session_does_not_touch_cached_websocket_state() {
let model_client = test_model_client(SessionSource::Cli);
let cached_websocket_session = WebsocketSession::default();
cached_websocket_session.set_connection_reused(/*connection_reused*/ true);
model_client.store_cached_websocket_session(cached_websocket_session);
let isolated_session = model_client.new_isolated_session();
assert!(!isolated_session.websocket_session.connection_reused());
drop(isolated_session);
assert!(
model_client
.take_cached_websocket_session()
.connection_reused()
);
}
fn test_session_telemetry() -> SessionTelemetry {
SessionTelemetry::new(
ThreadId::new(),
+168
View File
@@ -0,0 +1,168 @@
use std::sync::Arc;
use codex_hooks::PromptHookRequest;
use codex_hooks::PromptHookRunner;
use codex_models_manager::ModelsManagerConfig;
use codex_models_manager::manager::SharedModelsManager;
use codex_otel::SessionTelemetry;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::models::BaseInstructions;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use codex_rollout_trace::InferenceTraceContext;
use futures::StreamExt;
use serde_json::json;
use crate::client::ModelClient;
use crate::client_common::Prompt;
use crate::client_common::ResponseEvent;
use crate::config::Config;
use crate::responses_metadata::CodexResponsesMetadata;
use crate::stream_events_utils::raw_assistant_output_text_from_item;
const PROMPT_HOOK_BASE_INSTRUCTIONS: &str = r#"You evaluate a Codex prompt hook.
The user message contains:
1. The hook author's instructions.
2. The hook input JSON.
Decide whether the hook input satisfies the hook author's instructions.
Return only JSON:
{"ok": true}
or
{"ok": false, "reason": "concise actionable reason"}
Use ok:false only when the hook criteria fail. Do not answer the user's task. Do not include Markdown or extra text."#;
pub(crate) fn build_prompt_hook_runner(
model_client: ModelClient,
models_manager: SharedModelsManager,
config: &Config,
session_telemetry: SessionTelemetry,
service_tier: Option<String>,
responses_metadata: CodexResponsesMetadata,
) -> PromptHookRunner {
let models_manager_config = config.to_models_manager_config();
PromptHookRunner::new(move |request| {
let model_client = model_client.clone();
let models_manager = Arc::clone(&models_manager);
let models_manager_config = models_manager_config.clone();
let session_telemetry = session_telemetry.clone();
let service_tier = service_tier.clone();
let responses_metadata = responses_metadata.clone();
async move {
run_prompt_hook(
model_client,
models_manager,
models_manager_config,
session_telemetry,
service_tier,
responses_metadata,
request,
)
.await
}
})
}
/// Run the hook as an isolated model request: resolve the configured model,
/// send the rendered hook prompt as the only user message, disable all
/// tools and reasoning summaries, and constrain the final answer to the
/// small `{ ok, reason }` contract that `codex-hooks` maps back into existing
/// hook output semantics.
async fn run_prompt_hook(
model_client: ModelClient,
models_manager: SharedModelsManager,
models_manager_config: ModelsManagerConfig,
session_telemetry: SessionTelemetry,
service_tier: Option<String>,
responses_metadata: CodexResponsesMetadata,
request: PromptHookRequest,
) -> anyhow::Result<String> {
let model_info = models_manager
.get_model_info(request.model.as_str(), &models_manager_config)
.await;
let prompt = Prompt {
input: vec![ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: request.prompt,
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
}],
tools: Vec::new(),
parallel_tool_calls: false,
base_instructions: BaseInstructions {
text: PROMPT_HOOK_BASE_INSTRUCTIONS.to_string(),
},
output_schema: Some(prompt_hook_output_schema()),
output_schema_strict: false,
};
let disabled_trace = InferenceTraceContext::disabled();
let mut client_session = model_client.new_isolated_session();
let mut stream = client_session
.stream(
&prompt,
&model_info,
&session_telemetry,
/*effort*/ None,
ReasoningSummaryConfig::None,
service_tier,
&responses_metadata,
&disabled_trace,
)
.await?;
let mut delta_text = String::new();
let mut item_texts = Vec::new();
while let Some(event) = stream.next().await {
match event? {
ResponseEvent::OutputItemDone(item) => {
if let Some(text) = raw_assistant_output_text_from_item(&item) {
item_texts.push(text);
}
}
ResponseEvent::OutputTextDelta(delta) => delta_text.push_str(delta.as_str()),
ResponseEvent::Completed { .. } => break,
ResponseEvent::Created
| ResponseEvent::SafetyBuffering(_)
| ResponseEvent::TurnModerationMetadata(_)
| ResponseEvent::ReasoningSummaryDone { .. }
| ResponseEvent::OutputItemAdded(_)
| ResponseEvent::ServerModel(_)
| ResponseEvent::ModelVerifications(_)
| ResponseEvent::ServerReasoningIncluded(_)
| ResponseEvent::ToolCallInputDelta { .. }
| ResponseEvent::ReasoningSummaryDelta { .. }
| ResponseEvent::ReasoningContentDelta { .. }
| ResponseEvent::ReasoningSummaryPartAdded { .. }
| ResponseEvent::RateLimits(_)
| ResponseEvent::ModelsEtag(_) => {}
}
}
if item_texts.is_empty() {
Ok(delta_text)
} else {
Ok(item_texts.join(""))
}
}
fn prompt_hook_output_schema() -> serde_json::Value {
json!({
"type": "object",
"properties": {
"ok": {
"type": "boolean"
},
"reason": {
"type": "string"
}
},
"required": ["ok"],
"additionalProperties": false
})
}
+25 -5
View File
@@ -5,6 +5,7 @@ use std::time::Duration;
use codex_analytics::CompactionTrigger;
use codex_analytics::HookRunFact;
use codex_analytics::build_track_events_context;
use codex_hooks::Hooks;
use codex_hooks::PermissionRequestDecision;
use codex_hooks::PermissionRequestOutcome;
use codex_hooks::PermissionRequestRequest;
@@ -42,6 +43,7 @@ use tracing::instrument;
use crate::context::ContextualUserFragment;
use crate::context::HookAdditionalContext;
use crate::event_mapping::parse_turn_item;
use crate::responses_metadata::CodexResponsesRequestKind;
use crate::session::TurnInput;
use crate::session::session::Session;
use crate::session::turn_context::TurnContext;
@@ -99,6 +101,24 @@ impl From<UserPromptSubmitOutcome> for ContextInjectingHookOutcome {
}
}
async fn hooks_with_prompt_runner(sess: &Arc<Session>, turn_context: &Arc<TurnContext>) -> Hooks {
let responses_metadata = turn_context.turn_metadata_state.to_responses_metadata(
sess.installation_id.clone(),
sess.current_window_id().await,
CodexResponsesRequestKind::Turn,
);
let prompt_hook_runner = crate::hook_prompt::build_prompt_hook_runner(
sess.services.model_client.clone(),
Arc::clone(&sess.services.models_manager),
turn_context.config.as_ref(),
turn_context.session_telemetry.clone(),
turn_context.config.service_tier.clone(),
responses_metadata,
);
sess.hooks()
.clone_with_prompt_hook_runner(prompt_hook_runner)
}
#[instrument(level = "trace", skip_all)]
pub(crate) async fn run_pending_session_start_hooks(
sess: &Arc<Session>,
@@ -181,7 +201,7 @@ pub(crate) async fn run_pre_tool_use_hooks(
tool_use_id,
tool_input: tool_input.clone(),
};
let hooks = sess.hooks();
let hooks = hooks_with_prompt_runner(sess, turn_context).await;
let preview_runs = hooks.preview_pre_tool_use(&request);
emit_hook_started_events(sess, turn_context, preview_runs).await;
@@ -242,7 +262,7 @@ pub(crate) async fn run_permission_request_hooks(
run_id_suffix: run_id_suffix.to_string(),
tool_input: payload.tool_input,
};
let hooks = sess.hooks();
let hooks = hooks_with_prompt_runner(sess, turn_context).await;
let preview_runs = hooks.preview_permission_request(&request);
emit_hook_started_events(sess, turn_context, preview_runs).await;
@@ -285,7 +305,7 @@ pub(crate) async fn run_post_tool_use_hooks(
tool_input,
tool_response,
};
let hooks = sess.hooks();
let hooks = hooks_with_prompt_runner(sess, turn_context).await;
let preview_runs = hooks.preview_post_tool_use(&request);
emit_hook_started_events(sess, turn_context, preview_runs).await;
@@ -357,7 +377,7 @@ pub(crate) async fn run_turn_stop_hooks(
last_assistant_message,
target,
};
let hooks = sess.hooks();
let hooks = hooks_with_prompt_runner(sess, turn_context).await;
emit_hook_started_events(sess, turn_context, hooks.preview_stop(&request)).await;
let mut outcome = hooks.run_stop(request).await;
@@ -515,7 +535,7 @@ pub(crate) async fn inspect_pending_input(
permission_mode: hook_permission_mode(turn_context),
prompt: UserMessageItem::new(content).message(),
};
let hooks = sess.hooks();
let hooks = hooks_with_prompt_runner(sess, turn_context).await;
let preview_runs = hooks.preview_user_prompt_submit(&request);
run_context_injecting_hook(
sess,
+1
View File
@@ -49,6 +49,7 @@ mod exec_policy;
#[cfg(test)]
mod git_info_tests;
mod guardian;
mod hook_prompt;
mod hook_runtime;
mod image_preparation;
mod installation_id;
+97
View File
@@ -46,6 +46,7 @@ use core_test_support::test_codex::test_codex;
use core_test_support::wait_for_event;
use pretty_assertions::assert_eq;
use serde_json::Value;
use serde_json::json;
use std::sync::Arc;
use std::time::Duration;
use tempfile::TempDir;
@@ -259,6 +260,23 @@ if payload.get("prompt") == {blocked_prompt_json}:
Ok(())
}
fn write_user_prompt_submit_prompt_hook(home: &Path) -> Result<()> {
let hooks = serde_json::json!({
"hooks": {
"UserPromptSubmit": [{
"hooks": [{
"type": "prompt",
"prompt": "Reject prompts that mention secrets: $ARGUMENTS",
"statusMessage": "checking prompt",
}]
}]
}
});
fs::write(home.join("hooks.json"), hooks.to_string()).context("write hooks.json")?;
Ok(())
}
fn write_session_start_and_user_prompt_submit_order_hooks(home: &Path) -> Result<()> {
let session_start_script_path = home.join("session_start_order_hook.py");
let user_prompt_submit_script_path = home.join("user_prompt_submit_order_hook.py");
@@ -1705,6 +1723,85 @@ async fn multiple_blocking_stop_hooks_persist_multiple_hook_prompt_fragments() -
Ok(())
}
#[tokio::test]
async fn user_prompt_submit_prompt_hook_runs_isolated_request() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let response = mount_sse_once(
&server,
sse(vec![
ev_response_created("prompt-hook-resp"),
ev_assistant_message(
"prompt-hook-msg",
r#"{"ok":false,"reason":"do not mention secrets"}"#,
),
ev_completed("prompt-hook-resp"),
]),
)
.await;
let mut builder = test_codex()
.with_model("gpt-5.4")
.with_pre_build_hook(|home| {
if let Err(error) = write_user_prompt_submit_prompt_hook(home) {
panic!("failed to write user prompt submit prompt hook fixture: {error}");
}
})
.with_config(trust_discovered_hooks);
let test = builder.build(&server).await?;
let prompt = "secret launch codes";
test.submit_turn(prompt).await?;
let request = response.single_request();
let body = request.body_json();
let turn_metadata: Value = serde_json::from_str(
body["client_metadata"]["x-codex-turn-metadata"]
.as_str()
.context("prompt hook turn metadata")?,
)?;
assert_eq!(turn_metadata["request_kind"], json!("turn"));
assert!(
turn_metadata["turn_id"]
.as_str()
.is_some_and(|turn_id| !turn_id.is_empty()),
"prompt hook request should carry the current turn id",
);
assert_eq!(
turn_metadata["window_id"].as_str(),
request.header("x-codex-window-id").as_deref(),
"prompt hook request should carry the current context window id",
);
assert_eq!(body["model"], json!("gpt-5.4"));
assert_eq!(body["tools"], json!([]));
assert!(
request
.instructions_text()
.starts_with("You evaluate a Codex prompt hook."),
"prompt hook request should use isolated hook instructions",
);
let user_inputs = request.message_input_texts("user");
assert_eq!(
user_inputs.len(),
1,
"prompt hook request should contain exactly one user message",
);
assert_eq!(
request.message_input_texts("developer"),
Vec::<String>::new(),
"prompt hook request should not inherit developer context",
);
let hook_input = user_inputs.first().context("prompt hook user input")?;
assert!(hook_input.contains("Reject prompts that mention secrets:"));
assert!(
hook_input.contains(prompt),
"prompt hook request should include the submitted prompt",
);
Ok(())
}
#[tokio::test]
async fn blocked_user_prompt_submit_persists_additional_context_for_next_turn() -> Result<()> {
skip_if_no_network!(Ok(()));
+7 -1
View File
@@ -62,7 +62,13 @@ mod tests {
pre_tool_use: vec![MatcherGroup {
matcher: None,
hooks: vec![
HookHandlerConfig::Prompt {},
HookHandlerConfig::Prompt {
prompt: "check $ARGUMENTS".to_string(),
model: None,
timeout_sec: None,
status_message: None,
continue_on_block: false,
},
HookHandlerConfig::Command {
command: "echo hi".to_string(),
command_windows: None,
+176 -4
View File
@@ -25,9 +25,12 @@ use serde::Serialize;
use super::ConfiguredHandler;
use super::ConfiguredHandlerKind;
use super::HookListEntry;
use super::prompt_runner::PromptHookBehavior;
use super::prompt_runner::prompt_hook_behavior;
use crate::config_rules::hook_states_from_stack;
use crate::events::common::matcher_pattern_for_event;
use crate::events::common::validate_matcher_pattern;
use crate::schema::hook_event_wire_name;
use codex_protocol::protocol::HookEventName;
use codex_protocol::protocol::HookHandlerType;
use codex_protocol::protocol::HookSource;
@@ -513,6 +516,9 @@ fn append_matcher_groups(
handler_type: HookHandlerType::Command,
matcher: matcher.map(ToOwned::to_owned),
command: Some(command.clone()),
prompt: None,
model: None,
continue_on_block: None,
timeout_sec,
status_message: status_message.clone(),
source_path: source.path.clone(),
@@ -547,10 +553,92 @@ fn append_matcher_groups(
}
*display_order += 1;
}
HookHandlerConfig::Prompt {} => warnings.push(format!(
"skipping prompt hook in {}: prompt hooks are not supported yet",
source.path.display()
)),
HookHandlerConfig::Prompt {
prompt,
model,
timeout_sec,
status_message,
continue_on_block,
} => {
if matches!(
prompt_hook_behavior(event_name),
PromptHookBehavior::Unsupported
) {
warnings.push(format!(
"skipping prompt hook in {}: prompt hooks are not supported for {}",
source.path.display(),
hook_event_wire_name(event_name)
));
continue;
}
if prompt.trim().is_empty() {
warnings.push(format!(
"skipping empty hook prompt in {}",
source.path.display()
));
continue;
}
let timeout_sec = timeout_sec.unwrap_or(30).max(1);
let normalized_handler = HookHandlerConfig::Prompt {
prompt: prompt.clone(),
model: model.clone(),
timeout_sec: Some(timeout_sec),
status_message: status_message.clone(),
continue_on_block,
};
let current_hash = hook_hash(event_name, matcher, &group, normalized_handler);
let key =
crate::hook_key(&source.key_source, event_name, group_index, handler_index);
let state = source.hook_states.get(&key);
let enabled = hook_enabled(source.is_managed, state);
let trusted_hash = hook_trusted_hash(source.is_managed, state);
let trust_status =
hook_trust_status(source.is_managed, &current_hash, trusted_hash);
hook_entries.push(HookListEntry {
key,
event_name,
handler_type: HookHandlerType::Prompt,
matcher: matcher.map(ToOwned::to_owned),
command: None,
prompt: Some(prompt.clone()),
model: model.clone(),
continue_on_block: Some(continue_on_block),
timeout_sec,
status_message: status_message.clone(),
source_path: source.path.clone(),
source: source.source,
plugin_id: source.plugin_id.clone(),
display_order: *display_order,
enabled,
is_managed: source.is_managed,
current_hash,
trust_status,
});
if enabled
&& (source.bypass_hook_trust
|| matches!(
trust_status,
HookTrustStatus::Managed | HookTrustStatus::Trusted
))
{
handlers.push(ConfiguredHandler {
event_name,
matcher: matcher.map(ToOwned::to_owned),
kind: ConfiguredHandlerKind::Prompt {
prompt,
model,
timeout_sec,
continue_on_block,
},
status_message,
source_path: source.path.clone(),
source: source.source,
display_order: *display_order,
env: source.env.clone(),
});
}
*display_order += 1;
}
HookHandlerConfig::Agent {} => warnings.push(format!(
"skipping agent hook in {}: agent hooks are not supported yet",
source.path.display()
@@ -673,6 +761,7 @@ mod tests {
use codex_config::HookStateToml;
use codex_config::MatcherGroup;
use codex_config::TomlValue;
use codex_protocol::protocol::HookHandlerType;
use codex_protocol::protocol::HookTrustStatus;
fn source_path() -> AbsolutePathBuf {
@@ -764,6 +853,19 @@ mod tests {
}
}
fn prompt_group(matcher: Option<&str>) -> MatcherGroup {
MatcherGroup {
matcher: matcher.map(str::to_string),
hooks: vec![HookHandlerConfig::Prompt {
prompt: "Check this hook input: $ARGUMENTS".to_string(),
model: Some("gpt-5-mini".to_string()),
timeout_sec: None,
status_message: Some("Checking prompt hook".to_string()),
continue_on_block: true,
}],
}
}
#[test]
fn user_prompt_submit_ignores_invalid_matcher_during_discovery() {
let mut handlers = Vec::new();
@@ -951,6 +1053,76 @@ mod tests {
assert_eq!(handlers[0].matcher.as_deref(), Some("Edit|Write"));
}
#[test]
fn permission_request_prompt_hook_is_discovered_as_supported() {
let mut handlers = Vec::new();
let mut hook_entries = Vec::new();
let mut warnings = Vec::new();
let mut display_order = 0;
let source_path = source_path();
let hook_states = std::collections::HashMap::new();
append_matcher_groups(
&mut handlers,
&mut hook_entries,
&mut warnings,
&mut display_order,
&hook_handler_source(&source_path, &hook_states),
HookEventName::PermissionRequest,
vec![prompt_group(Some(".*"))],
);
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(handlers.len(), 1);
assert_eq!(hook_entries.len(), 1);
assert_eq!(hook_entries[0].handler_type, HookHandlerType::Prompt);
assert_eq!(hook_entries[0].command, None);
assert_eq!(
hook_entries[0].prompt.as_deref(),
Some("Check this hook input: $ARGUMENTS")
);
assert_eq!(hook_entries[0].timeout_sec, 30);
assert_eq!(
handlers[0].kind,
ConfiguredHandlerKind::Prompt {
prompt: "Check this hook input: $ARGUMENTS".to_string(),
model: Some("gpt-5-mini".to_string()),
timeout_sec: 30,
continue_on_block: true,
}
);
}
#[test]
fn unsupported_prompt_hook_is_skipped_during_discovery() {
let mut handlers = Vec::new();
let mut hook_entries = Vec::new();
let mut warnings = Vec::new();
let mut display_order = 0;
let source_path = source_path();
let hook_states = std::collections::HashMap::new();
append_matcher_groups(
&mut handlers,
&mut hook_entries,
&mut warnings,
&mut display_order,
&hook_handler_source(&source_path, &hook_states),
HookEventName::SessionStart,
vec![prompt_group(/*matcher*/ None)],
);
assert_eq!(handlers, Vec::<ConfiguredHandler>::new());
assert_eq!(hook_entries.len(), 0);
assert_eq!(
warnings,
vec![format!(
"skipping prompt hook in {}: prompt hooks are not supported for SessionStart",
source_path.display()
)]
);
}
#[test]
fn toml_hook_discovery_ignores_malformed_state_entries() {
let layer = ConfigLayerEntry::new(
+11 -4
View File
@@ -60,10 +60,6 @@ pub(crate) enum ConfiguredHandlerKind {
command: String,
timeout_sec: u64,
},
#[allow(
dead_code,
reason = "constructed by prompt-hook discovery in the follow-up"
)]
Prompt {
prompt: String,
model: Option<String>,
@@ -127,6 +123,9 @@ pub struct HookListEntry {
pub handler_type: HookHandlerType,
pub matcher: Option<String>,
pub command: Option<String>,
pub prompt: Option<String>,
pub model: Option<String>,
pub continue_on_block: Option<bool>,
pub timeout_sec: u64,
pub status_message: Option<String>,
pub source_path: AbsolutePathBuf,
@@ -183,6 +182,14 @@ impl ClaudeHooksEngine {
output_spiller: HookOutputSpiller::new(),
}
}
pub(crate) fn clone_with_prompt_hook_runner(
&self,
prompt_hook_runner: PromptHookRunner,
) -> Self {
let mut engine = self.clone();
engine.prompt_hook_runner = Some(prompt_hook_runner);
engine
}
pub(crate) fn warnings(&self) -> &[String] {
&self.warnings
+8
View File
@@ -83,6 +83,14 @@ impl Hooks {
engine,
}
}
pub fn clone_with_prompt_hook_runner(&self, prompt_hook_runner: PromptHookRunner) -> Self {
Self {
after_agent: self.after_agent.clone(),
engine: self
.engine
.clone_with_prompt_hook_runner(prompt_hook_runner),
}
}
pub fn startup_warnings(&self) -> &[String] {
self.engine.warnings()