diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 735925eb3c..8edefd5ae1 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -11794,6 +11794,14 @@ "eventName": { "$ref": "#/definitions/v2/HookEventName" }, + "executionMode": { + "allOf": [ + { + "$ref": "#/definitions/v2/HookExecutionMode" + } + ], + "default": "sync" + }, "handlerType": { "$ref": "#/definitions/v2/HookHandlerType" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 8091b2fb55..697968b485 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -8070,6 +8070,14 @@ "eventName": { "$ref": "#/definitions/HookEventName" }, + "executionMode": { + "allOf": [ + { + "$ref": "#/definitions/HookExecutionMode" + } + ], + "default": "sync" + }, "handlerType": { "$ref": "#/definitions/HookHandlerType" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/HooksListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/HooksListResponse.json index b0840b792d..2ebaa4bc09 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/HooksListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/HooksListResponse.json @@ -36,6 +36,13 @@ ], "type": "string" }, + "HookExecutionMode": { + "enum": [ + "sync", + "async" + ], + "type": "string" + }, "HookHandlerType": { "enum": [ "command", @@ -74,6 +81,14 @@ "eventName": { "$ref": "#/definitions/HookEventName" }, + "executionMode": { + "allOf": [ + { + "$ref": "#/definitions/HookExecutionMode" + } + ], + "default": "sync" + }, "handlerType": { "$ref": "#/definitions/HookHandlerType" }, diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 2bbd3cb0bf..1500e1b27d 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index d600220c24..f718a720a2 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/HookMetadata.ts b/codex-rs/app-server-protocol/schema/typescript/v2/HookMetadata.ts index 82244f0a5a..a831f81be1 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/HookMetadata.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/HookMetadata.ts @@ -3,11 +3,12 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AbsolutePathBuf } from "../AbsolutePathBuf"; import type { HookEventName } from "./HookEventName"; +import type { HookExecutionMode } from "./HookExecutionMode"; import type { HookHandlerType } from "./HookHandlerType"; import type { HookSource } from "./HookSource"; import type { HookTrustStatus } from "./HookTrustStatus"; -export type HookMetadata = { key: string, eventName: HookEventName, handlerType: HookHandlerType, matcher: string | null, command: string | null, timeoutSec: bigint, statusMessage: string | null, +export type HookMetadata = { key: string, eventName: HookEventName, handlerType: HookHandlerType, executionMode: HookExecutionMode, matcher: string | null, command: string | null, timeoutSec: bigint, statusMessage: string | null, /** * Configured `additionalContext` spill threshold. * `null` uses 2,500 tokens; `0` disables spilling. diff --git a/codex-rs/app-server-protocol/src/protocol/v2/hook.rs b/codex-rs/app-server-protocol/src/protocol/v2/hook.rs index 69984e8657..5d08afee3d 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/hook.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/hook.rs @@ -28,8 +28,11 @@ v2_enum_from_core!( ); v2_enum_from_core!( + #[derive(Default)] pub enum HookExecutionMode from CoreHookExecutionMode { - Sync, Async + #[default] + Sync, + Async } ); diff --git a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs index e0f521b60f..6d0e011342 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs @@ -1,5 +1,6 @@ use super::AppSummary; use super::HookEventName; +use super::HookExecutionMode; use super::HookHandlerType; use super::HookSource; use super::HookTrustStatus; @@ -524,6 +525,8 @@ pub struct HookMetadata { pub key: String, pub event_name: HookEventName, pub handler_type: HookHandlerType, + #[serde(default)] + pub execution_mode: HookExecutionMode, pub matcher: Option, pub command: Option, pub timeout_sec: u64, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index c997b6a3d5..6f9eeddfc9 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -1926,6 +1926,8 @@ For linked Git worktrees, project hook declarations come from the matching `.cod Hooks are returned even when disabled so clients can render and re-enable them. User-controlled state lives under `hooks.state`. Managed hooks are non-configurable, and user entries for managed hook keys are ignored during loading. +`executionMode` reports how a command hook runs. `sync` hooks participate in the current operation, while `async` hooks run in the background and deliver informational output through the existing steer-based injection path. Output is injected immediately into an active turn or persisted without starting a new turn when the session is idle. + For unmanaged hooks, `currentHash` and `trustStatus` describe whether the current definition is first-seen, approved, or changed since approval. Only trusted unmanaged hooks become runnable. Hook keys combine the source identity with a trailing event/group/handler selector that is currently positional. ```json @@ -1948,6 +1950,7 @@ For unmanaged hooks, `currentHash` and `trustStatus` describe whether the curren "key": "/Users/me/.codex/config.toml:pre_tool_use:0:0", "eventName": "pre_tool_use", "handlerType": "command", + "executionMode": "sync", "isManaged": false, "matcher": "Bash", "command": "python3 /Users/me/hook.py", diff --git a/codex-rs/app-server/src/request_processors/catalog_processor.rs b/codex-rs/app-server/src/request_processors/catalog_processor.rs index 4d80271fa2..38fd521e44 100644 --- a/codex-rs/app-server/src/request_processors/catalog_processor.rs +++ b/codex-rs/app-server/src/request_processors/catalog_processor.rs @@ -70,6 +70,7 @@ fn hooks_to_info(hooks: &[codex_hooks::HookListEntry]) -> Vec { key: hook.key.clone(), event_name: hook.event_name.into(), handler_type: hook.handler_type.into(), + execution_mode: hook.execution_mode.into(), matcher: hook.matcher.clone(), command: hook.command.clone(), timeout_sec: hook.timeout_sec, diff --git a/codex-rs/app-server/tests/suite/v2/hooks_list.rs b/codex-rs/app-server/tests/suite/v2/hooks_list.rs index dd653e77d9..632e1de0ce 100644 --- a/codex-rs/app-server/tests/suite/v2/hooks_list.rs +++ b/codex-rs/app-server/tests/suite/v2/hooks_list.rs @@ -8,6 +8,7 @@ use app_test_support::create_mock_responses_server_sequence_unchecked; use codex_app_server_protocol::ConfigBatchWriteParams; use codex_app_server_protocol::ConfigEdit; use codex_app_server_protocol::HookEventName; +use codex_app_server_protocol::HookExecutionMode; use codex_app_server_protocol::HookHandlerType; use codex_app_server_protocol::HookMetadata; use codex_app_server_protocol::HookSource; @@ -45,6 +46,7 @@ fn command_hook_hash( matcher: Option<&str>, command: &str, timeout_sec: u64, + execution_mode: HookExecutionMode, status_message: Option<&str>, additional_context_limit: Option, ) -> String { @@ -56,7 +58,7 @@ fn command_hook_hash( command: command.to_string(), command_windows: None, timeout_sec: Some(timeout_sec), - r#async: false, + r#async: execution_mode == HookExecutionMode::Async, status_message: status_message.map(ToOwned::to_owned), additional_context_limit, }], @@ -80,6 +82,7 @@ matcher = "Bash" type = "command" command = "python3 /tmp/listed-hook.py" timeout = 5 +async = true statusMessage = "running listed hook" additionalContextLimit = 4096 "#, @@ -161,6 +164,7 @@ async fn hooks_list_shows_discovered_hook() -> Result<()> { key: format!("{}:pre_tool_use:0:0", config_path.as_path().display()), event_name: HookEventName::PreToolUse, handler_type: HookHandlerType::Command, + execution_mode: HookExecutionMode::Async, matcher: Some("Bash".to_string()), command: Some("python3 /tmp/listed-hook.py".to_string()), timeout_sec: 5, @@ -177,6 +181,7 @@ async fn hooks_list_shows_discovered_hook() -> Result<()> { Some("Bash"), "python3 /tmp/listed-hook.py", /*timeout_sec*/ 5, + HookExecutionMode::Async, Some("running listed hook"), /*additional_context_limit*/ Some(4_096), ), @@ -240,6 +245,7 @@ async fn hooks_list_shows_discovered_plugin_hook() -> Result<()> { key: "demo@test:hooks/hooks.json:pre_tool_use:0:0".to_string(), event_name: HookEventName::PreToolUse, handler_type: HookHandlerType::Command, + execution_mode: HookExecutionMode::Sync, matcher: Some("Bash".to_string()), command: Some("echo plugin hook".to_string()), timeout_sec: 7, @@ -256,6 +262,7 @@ async fn hooks_list_shows_discovered_plugin_hook() -> Result<()> { Some("Bash"), "echo plugin hook", /*timeout_sec*/ 7, + HookExecutionMode::Sync, Some("running plugin hook"), /*additional_context_limit*/ None, ), @@ -436,6 +443,7 @@ timeout = 5 ), event_name: HookEventName::PreToolUse, handler_type: HookHandlerType::Command, + execution_mode: HookExecutionMode::Sync, matcher: Some("Bash".to_string()), command: Some("echo project hook".to_string()), timeout_sec: 5, @@ -452,6 +460,7 @@ timeout = 5 Some("Bash"), "echo project hook", /*timeout_sec*/ 5, + HookExecutionMode::Sync, /*status_message*/ None, /*additional_context_limit*/ None, ), diff --git a/codex-rs/hooks/src/engine/discovery.rs b/codex-rs/hooks/src/engine/discovery.rs index 3d5af4d60a..651f83ed9f 100644 --- a/codex-rs/hooks/src/engine/discovery.rs +++ b/codex-rs/hooks/src/engine/discovery.rs @@ -558,6 +558,7 @@ fn append_matcher_groups( is_managed: source.is_managed, current_hash, trust_status, + execution_mode, }); if enabled && (source.bypass_hook_trust diff --git a/codex-rs/hooks/src/engine/mod.rs b/codex-rs/hooks/src/engine/mod.rs index 38c94e8128..49aa7e93fd 100644 --- a/codex-rs/hooks/src/engine/mod.rs +++ b/codex-rs/hooks/src/engine/mod.rs @@ -108,6 +108,7 @@ pub struct HookListEntry { pub is_managed: bool, pub current_hash: String, pub trust_status: HookTrustStatus, + pub execution_mode: codex_protocol::protocol::HookExecutionMode, } #[derive(Clone)] 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 1e3646dbfd..359b64d254 100644 --- a/codex-rs/tui/src/bottom_pane/hooks_browser_view.rs +++ b/codex-rs/tui/src/bottom_pane/hooks_browser_view.rs @@ -1,4 +1,5 @@ use codex_app_server_protocol::HookEventName; +use codex_app_server_protocol::HookExecutionMode; use codex_app_server_protocol::HookMetadata; use codex_app_server_protocol::HookSource; use codex_app_server_protocol::HookTrustStatus; @@ -492,6 +493,11 @@ impl HooksBrowserView { width, Some(MAX_COMMAND_DETAIL_LINES), )); + let execution_mode = match hook.execution_mode { + HookExecutionMode::Sync => "Sync", + HookExecutionMode::Async => "Async", + }; + lines.push(detail_line("Mode", execution_mode)); lines.push(detail_line("Timeout", &format!("{}s", hook.timeout_sec))); if let Some(limit) = hook.additional_context_limit { let value = if limit == 0 { @@ -883,6 +889,7 @@ mod tests { use crate::test_support::test_path_display; use codex_app_server_protocol::HookErrorInfo; use codex_app_server_protocol::HookEventName; + use codex_app_server_protocol::HookExecutionMode; use codex_app_server_protocol::HookHandlerType; use codex_app_server_protocol::HookMetadata; use codex_app_server_protocol::HookSource; @@ -947,6 +954,7 @@ mod tests { key: key.to_string(), event_name, handler_type: HookHandlerType::Command, + execution_mode: HookExecutionMode::Sync, is_managed, matcher: Some("Bash".to_string()), command: Some(command.to_string()), @@ -1142,6 +1150,7 @@ mod tests { /*display_order*/ 0, ); untrusted_hook.trust_status = HookTrustStatus::Untrusted; + untrusted_hook.execution_mode = HookExecutionMode::Async; let mut view = HooksBrowserView::new( vec![untrusted_hook], Vec::new(), diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_additional_context_limit.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_additional_context_limit.snap index 9f7a0807cd..5df46276dc 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_additional_context_limit.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_additional_context_limit.snap @@ -12,6 +12,7 @@ expression: "render_lines(&view, 112)" Matcher Bash Source User config - /tmp/hooks.json Command /tmp/pre-tool-use.sh + Mode Sync Timeout 30s Context unlimited Trust Trusted 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 808b9dedbf..6597de15be 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 @@ -14,6 +14,7 @@ expression: "render_lines(&view, 44)" Command one two three four five six seven eight nine ten eleven twelve thirteen fourteen… + Mode Sync Timeout 30s Trust Trusted 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 6e88734980..53998ecf72 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 @@ -13,6 +13,7 @@ expression: "render_lines(&view, 112)" Matcher Bash Source Plugin - superpowers@openai-curated Command ${CODEX_PLUGIN_ROOT}/hooks/pre-tool-use-check.sh + Mode Sync Timeout 30s Trust Trusted 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 d073b11b3c..646741c283 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 @@ -12,6 +12,7 @@ expression: "render_lines(&view, 112)" Matcher Bash Source Admin config Command /enterprise/hooks/permission-check.sh + Mode Sync Timeout 30s Trust Managed 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 index b4a5c117e1..f08bb8cc81 100644 --- 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 @@ -12,6 +12,7 @@ expression: "render_lines(&view, 112)" Matcher Bash Source User config - /tmp/hooks.json Command /tmp/pre-tool-use-check.sh + Mode Sync Timeout 30s Trust New hook - review required 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 efeb0b2405..5622c6d188 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 @@ -19,6 +19,7 @@ expression: "render_lines(&view, 112)" Matcher Bash Source User config - /tmp/hooks.json Command /tmp/hook-8.sh + Mode Sync Timeout 30s Trust Trusted 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 514a8917a4..3884c083b0 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 @@ -13,6 +13,7 @@ expression: "render_lines(&view, 112)" Matcher Bash Source Admin config Command /enterprise/hooks/pre-tool-use-2.sh + Mode Sync Timeout 30s Trust Managed diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_untrusted_enabled_handler.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_untrusted_enabled_handler.snap index 4fa01776f6..ffd59c8e08 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_untrusted_enabled_handler.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__hooks_browser_view__tests__hooks_browser_untrusted_enabled_handler.snap @@ -12,6 +12,7 @@ expression: "render_lines(&view, 112)" Matcher Bash Source User config - /tmp/hooks.json Command ~/bin/untrusted.sh + Mode Async Timeout 30s Trust New hook - review required diff --git a/codex-rs/tui/src/startup_hooks_review.rs b/codex-rs/tui/src/startup_hooks_review.rs index 3c7d8ae661..bef4ea3131 100644 --- a/codex-rs/tui/src/startup_hooks_review.rs +++ b/codex-rs/tui/src/startup_hooks_review.rs @@ -305,6 +305,7 @@ mod tests { use crate::test_support::PathBufExt; use crate::test_support::test_path_buf; use codex_app_server_protocol::HookEventName; + use codex_app_server_protocol::HookExecutionMode; use codex_app_server_protocol::HookHandlerType; use codex_app_server_protocol::HookMetadata; use codex_app_server_protocol::HookSource; @@ -320,6 +321,7 @@ mod tests { key: key.to_string(), event_name: HookEventName::PreToolUse, handler_type: HookHandlerType::Command, + execution_mode: HookExecutionMode::Sync, is_managed: false, matcher: Some("Bash".to_string()), command: Some("/tmp/hook.sh".to_string()),