diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 1f1f2d7086..e80ada43c2 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -926,6 +926,7 @@ See the Codex keymap documentation for supported actions and examples." tui.frame_requester().schedule_frame(); app.refresh_startup_skills(&app_server); + app.refresh_startup_hooks(&app_server); // Kick off a non-blocking rate-limit prefetch so the first `/status` // already has data, without delaying the initial frame render. if requires_openai_auth && has_chatgpt_account { diff --git a/codex-rs/tui/src/app/background_requests.rs b/codex-rs/tui/src/app/background_requests.rs index 36155fb339..c66a40e7f7 100644 --- a/codex-rs/tui/src/app/background_requests.rs +++ b/codex-rs/tui/src/app/background_requests.rs @@ -5,6 +5,7 @@ //! the main event loop remains single-threaded. use super::*; +use codex_app_server_protocol::HookTrustStatus; use codex_app_server_protocol::MarketplaceAddParams; use codex_app_server_protocol::MarketplaceAddResponse; use codex_app_server_protocol::MarketplaceRemoveParams; @@ -88,6 +89,47 @@ impl App { }); } + /// Emits the initial hook review warning without delaying the first interactive frame. + pub(super) fn refresh_startup_hooks(&mut self, app_server: &AppServerSession) { + let request_handle = app_server.request_handle(); + let app_event_tx = self.app_event_tx.clone(); + let cwd = self.config.cwd.to_path_buf(); + tokio::spawn(async move { + let result = fetch_hooks_list(request_handle, cwd.clone()).await; + let response = match result { + Ok(response) => response, + Err(err) => { + tracing::warn!("failed to load startup hook review state: {err:#}"); + return; + } + }; + let hooks_needing_review = response + .data + .into_iter() + .find(|entry| entry.cwd.as_path() == cwd.as_path()) + .map(|entry| { + entry + .hooks + .into_iter() + .filter(|hook| { + matches!( + hook.trust_status, + HookTrustStatus::Untrusted | HookTrustStatus::Changed + ) + }) + .count() + }) + .unwrap_or_default(); + if let Some(message) = + startup_prompts::hooks_needing_review_warning(hooks_needing_review) + { + app_event_tx.send(AppEvent::InsertHistoryCell(Box::new( + history_cell::new_warning_event(message), + ))); + } + }); + } + pub(super) fn fetch_plugins_list(&mut self, app_server: &AppServerSession, cwd: PathBuf) { let request_handle = app_server.request_handle(); let app_event_tx = self.app_event_tx.clone(); @@ -322,6 +364,24 @@ impl App { }); } + pub(super) fn trust_hook( + &mut self, + app_server: &AppServerSession, + key: String, + trusted_hash: String, + enable: bool, + ) { + let request_handle = app_server.request_handle(); + let app_event_tx = self.app_event_tx.clone(); + tokio::spawn(async move { + let result = write_hook_trust(request_handle, key, trusted_hash, enable) + .await + .map(|_| ()) + .map_err(|err| format!("Failed to trust hook: {err}")); + app_event_tx.send(AppEvent::HookTrusted { result }); + }); + } + pub(super) fn refresh_plugin_mentions(&mut self) { let config = self.config.clone(); let app_event_tx = self.app_event_tx.clone(); @@ -805,6 +865,45 @@ pub(super) async fn write_hook_enabled( .wrap_err("config/batchWrite failed while updating hook enablement in TUI") } +pub(super) async fn write_hook_trust( + request_handle: AppServerRequestHandle, + key: String, + trusted_hash: String, + enable: bool, +) -> Result { + let request_id = RequestId::String(format!("hooks-config-write-{}", Uuid::new_v4())); + let value = if enable { + serde_json::json!({ + key: { + "enabled": true, + "trusted_hash": trusted_hash, + } + }) + } else { + serde_json::json!({ + key: { + "trusted_hash": trusted_hash, + } + }) + }; + request_handle + .request_typed(ClientRequest::ConfigBatchWrite { + request_id, + params: ConfigBatchWriteParams { + edits: vec![codex_app_server_protocol::ConfigEdit { + key_path: "hooks.state".to_string(), + value, + merge_strategy: MergeStrategy::Upsert, + }], + file_path: None, + expected_version: None, + reload_user_config: true, + }, + }) + .await + .wrap_err("config/batchWrite failed while updating hook trust in TUI") +} + pub(super) fn build_feedback_upload_params( origin_thread_id: Option, rollout_path: Option, diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index 6bdc413725..6b658fe819 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -1692,6 +1692,13 @@ impl App { AppEvent::SetHookEnabled { key, enabled } => { self.set_hook_enabled(app_server, key, enabled); } + AppEvent::TrustHook { + key, + trusted_hash, + enable, + } => { + self.trust_hook(app_server, key, trusted_hash, enable); + } AppEvent::HookEnabledSet { key, enabled, @@ -1716,6 +1723,11 @@ impl App { } } } + AppEvent::HookTrusted { result } => { + if let Err(err) = result { + self.chat_widget.add_error_message(err); + } + } AppEvent::OpenPermissionsPopup => { self.chat_widget.open_permissions_popup(); } diff --git a/codex-rs/tui/src/app/startup_prompts.rs b/codex-rs/tui/src/app/startup_prompts.rs index 41972e6751..482c75b3fa 100644 --- a/codex-rs/tui/src/app/startup_prompts.rs +++ b/codex-rs/tui/src/app/startup_prompts.rs @@ -77,6 +77,16 @@ pub(super) fn emit_system_bwrap_warning(app_event_tx: &AppEventSender, config: & ))); } +pub(super) fn hooks_needing_review_warning(count: usize) -> Option { + match count { + 0 => None, + 1 => Some("1 hook needs review before it can run. Open /hooks to review it.".to_string()), + count => Some(format!( + "{count} hooks need review before they can run. Open /hooks to review them." + )), + } +} + pub(super) fn should_show_model_migration_prompt( current_model: &str, target_model: &str, diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 84500e3edf..9a81db6766 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -263,6 +263,16 @@ async fn ignore_same_thread_resume_allows_reattaching_displayed_inactive_thread( assert!(app.transcript_cells.is_empty()); } +#[test] +fn hooks_needing_review_startup_warning_snapshot() { + let message = startup_prompts::hooks_needing_review_warning(2) + .expect("review-needed hooks should produce a startup warning"); + let rendered = + lines_to_single_string(&history_cell::new_warning_event(message).display_lines(80)); + + assert_app_snapshot!("hooks_needing_review_startup_warning", rendered); +} + #[tokio::test] async fn enqueue_primary_thread_session_replays_buffered_approval_after_attach() -> Result<()> { let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await; diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 5e45bf38e0..46a62bd0d0 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -749,6 +749,13 @@ pub(crate) enum AppEvent { enabled: bool, }, + /// Trust the current definition for a hook by stable hook key. + TrustHook { + key: String, + trusted_hash: String, + enable: bool, + }, + /// Result of persisting hook enabled state. HookEnabledSet { key: String, @@ -756,6 +763,11 @@ pub(crate) enum AppEvent { result: Result<(), String>, }, + /// Result of persisting hook trust state. + HookTrusted { + result: Result<(), String>, + }, + /// Notify that the manage skills popup was closed. ManageSkillsClosed, diff --git a/codex-rs/tui/src/bottom_pane/hooks_browser_view.rs b/codex-rs/tui/src/bottom_pane/hooks_browser_view.rs index 78fca8e745..22abd331e4 100644 --- a/codex-rs/tui/src/bottom_pane/hooks_browser_view.rs +++ b/codex-rs/tui/src/bottom_pane/hooks_browser_view.rs @@ -68,7 +68,12 @@ impl HooksBrowserView { app_event_tx, }; if view.page_len() > 0 { - view.state.selected_idx = Some(0); + view.state.selected_idx = Some( + view.event_rows() + .iter() + .position(|row| row.needs_review > 0) + .unwrap_or(0), + ); } view } @@ -87,10 +92,16 @@ impl HooksBrowserView { .iter() .filter(|hook| hook.event_name == event_name && hook_is_active(hook)) .count(); + let needs_review = self + .hooks + .iter() + .filter(|hook| hook.event_name == event_name && hook_needs_review(hook)) + .count(); EventRow { event_name, installed, active, + needs_review, } }) .collect() @@ -168,6 +179,9 @@ impl HooksBrowserView { if hook.is_managed { return; } + if hook_needs_review(hook) { + return; + } hook.enabled = !hook.enabled; self.app_event_tx.send(AppEvent::SetHookEnabled { @@ -176,6 +190,29 @@ impl HooksBrowserView { }); } + fn trust_selected_hook(&mut self, event_name: HookEventName) { + let Some(idx) = self.selected_hook_index(event_name) else { + return; + }; + let Some(hook) = self.hooks.get_mut(idx) else { + return; + }; + if !hook_needs_review(hook) { + return; + } + + let enable = !hook.enabled; + hook.trust_status = HookTrustStatus::Trusted; + if enable { + hook.enabled = true; + } + self.app_event_tx.send(AppEvent::TrustHook { + key: hook.key.clone(), + current_hash: hook.current_hash.clone(), + enable, + }); + } + fn close(&mut self) { self.complete = true; } @@ -207,7 +244,7 @@ impl HooksBrowserView { fn handler_header_lines(event_name: HookEventName) -> Vec> { vec![ format!("{} hooks", event_label(event_name)).bold().into(), - "Turn hooks on or off. Your changes are saved automatically." + "Review new or changed hooks, then turn them on or off." .dim() .into(), ] @@ -219,6 +256,7 @@ impl HooksBrowserView { format!("{: { + format!("[{marker}] {} · modified", hook_title(idx)) + } + HookTrustStatus::Untrusted => format!("[{marker}] {} · new", hook_title(idx)), + HookTrustStatus::Managed | HookTrustStatus::Trusted => { + format!("[{marker}] {}", hook_title(idx)) + } + }; let mut line = Line::from(row); line = truncate_line_with_ellipsis_if_overflow(line, width); if hook.is_managed { @@ -330,6 +386,7 @@ impl HooksBrowserView { Some(MAX_COMMAND_DETAIL_LINES), )); lines.push(detail_line("Timeout", &format!("{}s", hook.timeout_sec))); + lines.push(detail_line("Trust", hook_trust_label(hook.trust_status))); lines } @@ -362,6 +419,14 @@ impl HooksBrowserView { key_hint::plain(KeyCode::Esc).into(), " to go back".into(), ]) + } else if selected_hook.is_some_and(hook_needs_review) { + Line::from(vec![ + "Press ".into(), + key_hint::plain(KeyCode::Char('t')).into(), + " to trust; ".into(), + key_hint::plain(KeyCode::Esc).into(), + " to go back".into(), + ]) } else { Line::from(vec![ "Press ".into(), @@ -422,6 +487,15 @@ impl BottomPaneView for HooksBrowserView { self.toggle_selected_hook(event_name); } } + KeyEvent { + code: KeyCode::Char('t'), + modifiers: KeyModifiers::NONE, + .. + } => { + if let HooksBrowserPage::Handlers(event_name) = self.page { + self.trust_selected_hook(event_name); + } + } KeyEvent { code: KeyCode::Esc, .. } => match self.page { @@ -532,6 +606,23 @@ struct EventRow { event_name: HookEventName, installed: usize, active: usize, + needs_review: usize, +} + +fn hook_needs_review(hook: &HookMetadata) -> bool { + matches!( + hook.trust_status, + HookTrustStatus::Untrusted | HookTrustStatus::Modified + ) +} + +fn hook_trust_label(status: HookTrustStatus) -> &'static str { + match status { + HookTrustStatus::Managed => "Managed", + HookTrustStatus::Trusted => "Trusted", + HookTrustStatus::Untrusted => "New hook - review required", + HookTrustStatus::Modified => "Modified since last trusted - review required", + } } fn event_label(event_name: HookEventName) -> &'static str { @@ -987,14 +1078,14 @@ mod tests { } #[test] - fn untrusted_enabled_hooks_do_not_count_as_active() { + fn review_needed_hooks_are_not_active() { let (tx_raw, _rx) = unbounded_channel::(); let mut untrusted_hook = hook( "path:untrusted", HookEventName::PreToolUse, HookSource::User, /*plugin_id*/ None, - "~/bin/untrusted.sh", + "/tmp/pre-tool-use-check.sh", /*enabled*/ true, /*is_managed*/ false, /*display_order*/ 0, @@ -1015,6 +1106,62 @@ mod tests { assert_eq!(pre_tool_use.installed, 1); assert_eq!(pre_tool_use.active, 0); + assert_eq!(pre_tool_use.needs_review, 1); + } + + #[test] + fn review_needed_event_is_selected_by_default() { + let (tx_raw, _rx) = unbounded_channel::(); + let mut untrusted_hook = hook( + "path:untrusted", + HookEventName::PermissionRequest, + HookSource::User, + /*plugin_id*/ None, + "/tmp/permission-request-check.sh", + /*enabled*/ false, + /*is_managed*/ false, + /*display_order*/ 0, + ); + untrusted_hook.trust_status = HookTrustStatus::Untrusted; + let view = HooksBrowserView::new( + vec![untrusted_hook], + Vec::new(), + Vec::new(), + AppEventSender::new(tx_raw), + ); + + assert_eq!( + view.selected_event(), + Some(HookEventName::PermissionRequest) + ); + } + + #[test] + fn renders_review_needed_handler() { + let (tx_raw, _rx) = unbounded_channel::(); + let mut untrusted_hook = hook( + "path:untrusted", + HookEventName::PreToolUse, + HookSource::User, + /*plugin_id*/ None, + "/tmp/pre-tool-use-check.sh", + /*enabled*/ false, + /*is_managed*/ false, + /*display_order*/ 0, + ); + untrusted_hook.trust_status = HookTrustStatus::Untrusted; + let mut view = HooksBrowserView::new( + vec![untrusted_hook], + Vec::new(), + Vec::new(), + AppEventSender::new(tx_raw), + ); + view.handle_key_event(KeyEvent::from(KeyCode::Enter)); + + assert_snapshot!( + "hooks_browser_review_needed_handler", + render_lines(&view, /*width*/ 112) + ); } fn assert_unmanaged_toggle_key(key_code: KeyCode) { @@ -1077,6 +1224,44 @@ mod tests { assert!(rx.try_recv().is_err()); } + #[test] + fn trust_key_trusts_review_needed_handler() { + let (tx_raw, mut rx) = unbounded_channel::(); + let mut untrusted_hook = hook( + "path:untrusted", + HookEventName::PreToolUse, + HookSource::User, + /*plugin_id*/ None, + "/tmp/pre-tool-use-check.sh", + /*enabled*/ false, + /*is_managed*/ false, + /*display_order*/ 0, + ); + untrusted_hook.trust_status = HookTrustStatus::Untrusted; + let current_hash = untrusted_hook.current_hash.clone(); + let mut view = HooksBrowserView::new( + vec![untrusted_hook], + Vec::new(), + Vec::new(), + AppEventSender::new(tx_raw), + ); + view.handle_key_event(KeyEvent::from(KeyCode::Enter)); + view.handle_key_event(KeyEvent::from(KeyCode::Char('t'))); + + match rx.try_recv().expect("trust event") { + AppEvent::TrustHook { + key, + current_hash: trusted_hash, + enable, + } => { + assert_eq!(key, "path:untrusted"); + assert_eq!(trusted_hash, current_hash); + assert!(enable); + } + other => panic!("expected hook trust event, got {other:?}"), + } + } + #[test] fn escape_returns_to_the_selected_event() { let mut view = view(); diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_capped_command_details.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_capped_command_details.snap index 7af93e3c5a..f6efb4995a 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_capped_command_details.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_capped_command_details.snap @@ -4,7 +4,7 @@ expression: "render_lines(&view, 44)" --- PreToolUse hooks - Turn hooks on or off. Your changes are s + Review new or changed hooks, then turn t [x] Hook 1 @@ -15,5 +15,6 @@ expression: "render_lines(&view, 44)" seven eight nine ten eleven twelve thirteen fourteen… Timeout 30s + Trust Trusted Press space or enter to toggle; esc to go diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_empty_handlers.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_empty_handlers.snap index 33321eec2b..753ee2331d 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_empty_handlers.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_empty_handlers.snap @@ -4,7 +4,7 @@ expression: "render_lines(&view, 112)" --- PermissionRequest hooks - Turn hooks on or off. Your changes are saved automatically. + Review new or changed hooks, then turn them on or off. No hooks installed for this event. diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_events.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_events.snap index 522105c30d..096fb562f8 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_events.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_events.snap @@ -6,12 +6,12 @@ expression: "render_lines(&view, 112)" Hooks Lifecycle hooks from config and enabled plugins. - Event Installed Active Description - PreToolUse 2 1 Before a tool executes - PermissionRequest 1 1 When permission is requested - PostToolUse 0 0 After a tool executes - SessionStart 0 0 When a new session starts - UserPromptSubmit 0 0 When the user submits a prompt - Stop 0 0 Right before Codex ends its turn + Event Installed Active Review Description + PreToolUse 2 1 0 Before a tool executes + PermissionRequest 1 1 0 When permission is requested + PostToolUse 0 0 0 After a tool executes + SessionStart 0 0 0 When a new session starts + UserPromptSubmit 0 0 0 When the user submits a prompt + Stop 0 0 0 Right before Codex ends its turn Press enter to view hooks; esc to close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_events_with_issues.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_events_with_issues.snap index 18e3b9f849..8d10909d37 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_events_with_issues.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_events_with_issues.snap @@ -10,12 +10,12 @@ expression: "render_lines(&view, 112)" ⚠ skipped invalid matcher for PreToolUse ■ /tmp/hooks.json: failed to parse hooks config - Event Installed Active Description - PreToolUse 0 0 Before a tool executes - PermissionRequest 0 0 When permission is requested - PostToolUse 0 0 After a tool executes - SessionStart 0 0 When a new session starts - UserPromptSubmit 0 0 When the user submits a prompt - Stop 0 0 Right before Codex ends its turn + Event Installed Active Review Description + PreToolUse 0 0 0 Before a tool executes + PermissionRequest 0 0 0 When permission is requested + PostToolUse 0 0 0 After a tool executes + SessionStart 0 0 0 When a new session starts + UserPromptSubmit 0 0 0 When the user submits a prompt + Stop 0 0 0 Right before Codex ends its turn Press enter to view hooks; esc to close diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_handlers.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_handlers.snap index c44f4b866a..7e5fcf627f 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_handlers.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_handlers.snap @@ -4,7 +4,7 @@ expression: "render_lines(&view, 112)" --- PreToolUse hooks - Turn hooks on or off. Your changes are saved automatically. + Review new or changed hooks, then turn them on or off. [x] Hook 1 [ ] Hook 2 @@ -14,5 +14,6 @@ expression: "render_lines(&view, 112)" Source Plugin - superpowers@openai-curated Command ${CODEX_PLUGIN_ROOT}/hooks/pre-tool-use-check.sh Timeout 30s + Trust Trusted Press space or enter to toggle; esc to go back diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_managed_handler.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_managed_handler.snap index 21c59065f5..48bbd9cd01 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_managed_handler.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_managed_handler.snap @@ -4,7 +4,7 @@ expression: "render_lines(&view, 112)" --- PermissionRequest hooks - Turn hooks on or off. Your changes are saved automatically. + Review new or changed hooks, then turn them on or off. [x] Hook 1 @@ -13,5 +13,6 @@ expression: "render_lines(&view, 112)" Source Admin config Command /enterprise/hooks/permission-check.sh Timeout 30s + Trust Managed Managed hooks are always on; press esc to go back diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_review_needed_handler.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_review_needed_handler.snap new file mode 100644 index 0000000000..1b7c45701b --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_review_needed_handler.snap @@ -0,0 +1,18 @@ +--- +source: tui/src/bottom_pane/hooks_browser_view.rs +expression: "render_lines(&view, 112)" +--- + + PreToolUse hooks + Review new or changed hooks, then turn them on or off. + + [!] Hook 1 · new + + Event PreToolUse + Matcher Bash + Source User config - /tmp/hooks.json + Command /tmp/pre-tool-use-check.sh + Timeout 30s + Trust New hook - review required + + Press t to trust; esc to go back diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_scrolled_handlers.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_scrolled_handlers.snap index 4f4a4377c6..fe896ea59c 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_scrolled_handlers.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_scrolled_handlers.snap @@ -4,7 +4,7 @@ expression: "render_lines(&view, 112)" --- PreToolUse hooks - Turn hooks on or off. Your changes are saved automatically. + Review new or changed hooks, then turn them on or off. [x] Hook 2 [x] Hook 3 @@ -20,5 +20,6 @@ expression: "render_lines(&view, 112)" Source User config - /tmp/hooks.json Command /tmp/hook-8.sh Timeout 30s + Trust Trusted Press space or enter to toggle; esc to go back diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_selected_managed_handler.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_selected_managed_handler.snap index 9a53b95d6d..0c5a7ccf7a 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_selected_managed_handler.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_selected_managed_handler.snap @@ -4,7 +4,7 @@ expression: "render_lines(&view, 112)" --- PreToolUse hooks - Turn hooks on or off. Your changes are saved automatically. + Review new or changed hooks, then turn them on or off. [x] Hook 1 [x] Hook 2 @@ -14,5 +14,6 @@ expression: "render_lines(&view, 112)" Source Admin config Command /enterprise/hooks/pre-tool-use-2.sh Timeout 30s + Trust Managed Managed hooks are always on; press esc to go back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__hooks_popup_shows_list_diagnostics.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__hooks_popup_shows_list_diagnostics.snap index 865d19031f..1db52419a1 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__hooks_popup_shows_list_diagnostics.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__hooks_popup_shows_list_diagnostics.snap @@ -9,12 +9,12 @@ expression: popup ⚠ skipped invalid matcher for PreToolUse ■ /tmp/hooks.json: failed to parse hooks config - Event Installed Active Description - PreToolUse 0 0 Before a tool executes - PermissionRequest 0 0 When permission is requested - PostToolUse 0 0 After a tool executes - SessionStart 0 0 When a new session starts - UserPromptSubmit 0 0 When the user submits a prompt - Stop 0 0 Right before Codex ends its turn + Event Installed Active Review Description + PreToolUse 0 0 0 Before a tool executes + PermissionRequest 0 0 0 When permission is requested + PostToolUse 0 0 0 After a tool executes + SessionStart 0 0 0 When a new session starts + UserPromptSubmit 0 0 0 When the user submits a prompt + Stop 0 0 0 Right before Codex ends its turn Press enter to view hooks; esc to close diff --git a/codex-rs/tui/src/snapshots/codex_tui__app__tests__hooks_needing_review_startup_warning.snap b/codex-rs/tui/src/snapshots/codex_tui__app__tests__hooks_needing_review_startup_warning.snap new file mode 100644 index 0000000000..f044b95e86 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__app__tests__hooks_needing_review_startup_warning.snap @@ -0,0 +1,5 @@ +--- +source: tui/src/app/tests.rs +expression: rendered +--- +⚠ 2 hooks need review before they can run. Open /hooks to review them.