From 558e6c3888da43d7785846ce433b39f2480b6ac4 Mon Sep 17 00:00:00 2001 From: Abhinav Vedmala Date: Mon, 11 May 2026 10:40:28 -0400 Subject: [PATCH 1/7] Add platform-specific hook commands --- .../codex_app_server_protocol.schemas.json | 27 +++++++++- .../codex_app_server_protocol.v2.schemas.json | 27 +++++++++- .../v2/ConfigRequirementsReadResponse.json | 27 +++++++++- .../typescript/v2/ConfiguredHookCommand.ts | 6 +++ .../v2/ConfiguredHookCommandByPlatform.ts | 5 ++ .../typescript/v2/ConfiguredHookHandler.ts | 3 +- .../schema/typescript/v2/index.ts | 2 + .../src/protocol/v2/config.rs | 18 ++++++- .../request_processors/config_processor.rs | 15 +++++- .../app-server/tests/suite/v2/hooks_list.rs | 2 +- codex-rs/config/src/hook_config.rs | 38 +++++++++++++- codex-rs/config/src/hooks_tests.rs | 46 +++++++++++++++-- codex-rs/config/src/lib.rs | 2 + .../core/src/config/config_loader_tests.rs | 5 +- codex-rs/core/src/session/tests.rs | 4 +- codex-rs/hooks/src/declarations.rs | 3 +- codex-rs/hooks/src/engine/discovery.rs | 50 +++++++++++++++++-- codex-rs/hooks/src/engine/mod_tests.rs | 26 +++++++--- codex-rs/tui/src/debug_config.rs | 4 +- 19 files changed, 284 insertions(+), 26 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookCommand.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookCommandByPlatform.ts 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 215842d9292c..154258cb4158 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 @@ -7714,6 +7714,31 @@ "title": "ConfigWriteResponse", "type": "object" }, + "ConfiguredHookCommand": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/v2/ConfiguredHookCommandByPlatform" + } + ] + }, + "ConfiguredHookCommandByPlatform": { + "properties": { + "unix": { + "type": "string" + }, + "windows": { + "type": "string" + } + }, + "required": [ + "unix", + "windows" + ], + "type": "object" + }, "ConfiguredHookHandler": { "oneOf": [ { @@ -7722,7 +7747,7 @@ "type": "boolean" }, "command": { - "type": "string" + "$ref": "#/definitions/v2/ConfiguredHookCommand" }, "statusMessage": { "type": [ 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 085744e8191b..c6a666e758f1 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 @@ -4103,6 +4103,31 @@ "title": "ConfigWriteResponse", "type": "object" }, + "ConfiguredHookCommand": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/ConfiguredHookCommandByPlatform" + } + ] + }, + "ConfiguredHookCommandByPlatform": { + "properties": { + "unix": { + "type": "string" + }, + "windows": { + "type": "string" + } + }, + "required": [ + "unix", + "windows" + ], + "type": "object" + }, "ConfiguredHookHandler": { "oneOf": [ { @@ -4111,7 +4136,7 @@ "type": "boolean" }, "command": { - "type": "string" + "$ref": "#/definitions/ConfiguredHookCommand" }, "statusMessage": { "type": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json index 14a8d572d618..f1d7e5eae56d 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -111,6 +111,31 @@ }, "type": "object" }, + "ConfiguredHookCommand": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/ConfiguredHookCommandByPlatform" + } + ] + }, + "ConfiguredHookCommandByPlatform": { + "properties": { + "unix": { + "type": "string" + }, + "windows": { + "type": "string" + } + }, + "required": [ + "unix", + "windows" + ], + "type": "object" + }, "ConfiguredHookHandler": { "oneOf": [ { @@ -119,7 +144,7 @@ "type": "boolean" }, "command": { - "type": "string" + "$ref": "#/definitions/ConfiguredHookCommand" }, "statusMessage": { "type": [ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookCommand.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookCommand.ts new file mode 100644 index 000000000000..9ed8cfe8f7d5 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookCommand.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConfiguredHookCommandByPlatform } from "./ConfiguredHookCommandByPlatform"; + +export type ConfiguredHookCommand = string | ConfiguredHookCommandByPlatform; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookCommandByPlatform.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookCommandByPlatform.ts new file mode 100644 index 000000000000..743e23feef89 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookCommandByPlatform.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ConfiguredHookCommandByPlatform = { unix: string, windows: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookHandler.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookHandler.ts index a81ce61f6e97..41745c14d66d 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookHandler.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookHandler.ts @@ -1,5 +1,6 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConfiguredHookCommand } from "./ConfiguredHookCommand"; -export type ConfiguredHookHandler = { "type": "command", command: string, timeoutSec: bigint | null, async: boolean, statusMessage: string | null, } | { "type": "prompt", } | { "type": "agent", }; +export type ConfiguredHookHandler = { "type": "command", command: ConfiguredHookCommand, timeoutSec: bigint | null, async: boolean, statusMessage: string | null, } | { "type": "prompt", } | { "type": "agent", }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index a6b961366e0f..5e8ee434e4ce 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -76,6 +76,8 @@ export type { ConfigRequirementsReadResponse } from "./ConfigRequirementsReadRes export type { ConfigValueWriteParams } from "./ConfigValueWriteParams"; export type { ConfigWarningNotification } from "./ConfigWarningNotification"; export type { ConfigWriteResponse } from "./ConfigWriteResponse"; +export type { ConfiguredHookCommand } from "./ConfiguredHookCommand"; +export type { ConfiguredHookCommandByPlatform } from "./ConfiguredHookCommandByPlatform"; export type { ConfiguredHookHandler } from "./ConfiguredHookHandler"; export type { ConfiguredHookMatcherGroup } from "./ConfiguredHookMatcherGroup"; export type { ContextCompactedNotification } from "./ContextCompactedNotification"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/config.rs b/codex-rs/app-server-protocol/src/protocol/v2/config.rs index 8bc50bb1f22f..cfdbb31f3897 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -412,7 +412,7 @@ pub enum ConfiguredHookHandler { #[serde(rename = "command")] #[ts(rename = "command")] Command { - command: String, + command: ConfiguredHookCommand, #[serde(rename = "timeoutSec")] #[ts(rename = "timeoutSec")] timeout_sec: Option, @@ -429,6 +429,22 @@ pub enum ConfiguredHookHandler { Agent {}, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(untagged)] +#[ts(export_to = "v2/")] +pub enum ConfiguredHookCommand { + Single(String), + ByPlatform(ConfiguredHookCommandByPlatform), +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase", export_to = "v2/")] +pub struct ConfiguredHookCommandByPlatform { + pub unix: String, + pub windows: String, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server/src/request_processors/config_processor.rs b/codex-rs/app-server/src/request_processors/config_processor.rs index cda2bbe61d8e..10c45b936ae9 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -18,6 +18,8 @@ use codex_app_server_protocol::ConfigRequirementsReadResponse; use codex_app_server_protocol::ConfigValueWriteParams; use codex_app_server_protocol::ConfigWriteErrorCode; use codex_app_server_protocol::ConfigWriteResponse; +use codex_app_server_protocol::ConfiguredHookCommand; +use codex_app_server_protocol::ConfiguredHookCommandByPlatform; use codex_app_server_protocol::ConfiguredHookHandler; use codex_app_server_protocol::ConfiguredHookMatcherGroup; use codex_app_server_protocol::ExperimentalFeatureEnablementSetParams; @@ -32,6 +34,8 @@ use codex_app_server_protocol::SandboxMode; use codex_app_server_protocol::ServerNotification; use codex_chatgpt::connectors; use codex_config::ConfigRequirementsToml; +use codex_config::HookCommandByPlatformConfig; +use codex_config::HookCommandConfig; use codex_config::HookEventsToml; use codex_config::HookHandlerConfig as CoreHookHandlerConfig; use codex_config::ManagedHooksRequirementsToml; @@ -518,7 +522,7 @@ fn map_hook_handler_to_api(handler: CoreHookHandlerConfig) -> ConfiguredHookHand r#async, status_message, } => ConfiguredHookHandler::Command { - command, + command: map_hook_command_to_api(command), timeout_sec, r#async, status_message, @@ -528,6 +532,15 @@ fn map_hook_handler_to_api(handler: CoreHookHandlerConfig) -> ConfiguredHookHand } } +fn map_hook_command_to_api(command: HookCommandConfig) -> ConfiguredHookCommand { + match command { + HookCommandConfig::Single(command) => ConfiguredHookCommand::Single(command), + HookCommandConfig::ByPlatform(HookCommandByPlatformConfig { unix, windows }) => { + ConfiguredHookCommand::ByPlatform(ConfiguredHookCommandByPlatform { unix, windows }) + } + } +} + fn map_sandbox_mode_requirement_to_api(mode: CoreSandboxModeRequirement) -> Option { match mode { CoreSandboxModeRequirement::ReadOnly => Some(SandboxMode::ReadOnly), 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 623896626c8b..8b90335480fe 100644 --- a/codex-rs/app-server/tests/suite/v2/hooks_list.rs +++ b/codex-rs/app-server/tests/suite/v2/hooks_list.rs @@ -52,7 +52,7 @@ fn command_hook_hash( group: codex_config::MatcherGroup { matcher: matcher.map(ToOwned::to_owned), hooks: vec![codex_config::HookHandlerConfig::Command { - command: command.to_string(), + command: codex_config::HookCommandConfig::Single(command.to_string()), timeout_sec: Some(timeout_sec), r#async: false, status_message: status_message.map(ToOwned::to_owned), diff --git a/codex-rs/config/src/hook_config.rs b/codex-rs/config/src/hook_config.rs index 630d18c569f6..791693e16fec 100644 --- a/codex-rs/config/src/hook_config.rs +++ b/codex-rs/config/src/hook_config.rs @@ -125,7 +125,7 @@ pub struct MatcherGroup { pub enum HookHandlerConfig { #[serde(rename = "command")] Command { - command: String, + command: HookCommandConfig, #[serde(default, rename = "timeout")] timeout_sec: Option, #[serde(default)] @@ -139,6 +139,42 @@ pub enum HookHandlerConfig { Agent {}, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(untagged)] +pub enum HookCommandConfig { + Single(String), + ByPlatform(HookCommandByPlatformConfig), +} + +impl HookCommandConfig { + pub fn for_current_platform(&self) -> &str { + match self { + Self::Single(command) => command, + Self::ByPlatform(command) => command.for_current_platform(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct HookCommandByPlatformConfig { + pub unix: String, + pub windows: String, +} + +impl HookCommandByPlatformConfig { + fn for_current_platform(&self) -> &str { + #[cfg(windows)] + { + &self.windows + } + + #[cfg(not(windows))] + { + &self.unix + } + } +} + #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ManagedHooksRequirementsToml { pub managed_dir: Option, diff --git a/codex-rs/config/src/hooks_tests.rs b/codex-rs/config/src/hooks_tests.rs index 69fcd3fe9574..2d7fcc430527 100644 --- a/codex-rs/config/src/hooks_tests.rs +++ b/codex-rs/config/src/hooks_tests.rs @@ -2,6 +2,8 @@ use pretty_assertions::assert_eq; use std::collections::BTreeMap; +use super::HookCommandByPlatformConfig; +use super::HookCommandConfig; use super::HookEventsToml; use super::HookHandlerConfig; use super::HooksFile; @@ -39,7 +41,7 @@ fn hooks_file_deserializes_existing_json_shape() { pre_tool_use: vec![MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { - command: "python3 /tmp/pre.py".to_string(), + command: HookCommandConfig::Single("python3 /tmp/pre.py".to_string()), timeout_sec: Some(10), r#async: false, status_message: Some("checking".to_string()), @@ -73,7 +75,7 @@ statusMessage = "checking" pre_tool_use: vec![MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { - command: "python3 /tmp/pre.py".to_string(), + command: HookCommandConfig::Single("python3 /tmp/pre.py".to_string()), timeout_sec: Some(10), r#async: false, status_message: Some("checking".to_string()), @@ -109,7 +111,7 @@ command = "python3 /tmp/pre.py" pre_tool_use: vec![MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { - command: "python3 /tmp/pre.py".to_string(), + command: HookCommandConfig::Single("python3 /tmp/pre.py".to_string()), timeout_sec: None, r#async: false, status_message: None, @@ -153,7 +155,9 @@ command = "python3 /enterprise/place/pre.py" pre_tool_use: vec![MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { - command: "python3 /enterprise/place/pre.py".to_string(), + command: HookCommandConfig::Single( + "python3 /enterprise/place/pre.py".to_string(), + ), timeout_sec: None, r#async: false, status_message: None, @@ -164,3 +168,37 @@ command = "python3 /enterprise/place/pre.py" } ); } + +#[test] +fn hook_events_deserialize_platform_command_from_toml() { + let parsed: HookEventsToml = toml::from_str( + r#" +[[PreToolUse]] +matcher = "^Bash$" + +[[PreToolUse.hooks]] +type = "command" +command = { unix = "bash /enterprise/hooks/pre.sh", windows = "powershell -File C:\\enterprise\\hooks\\pre.ps1" } +"#, + ) + .expect("platform hook command TOML should deserialize"); + + assert_eq!( + parsed, + HookEventsToml { + pre_tool_use: vec![MatcherGroup { + matcher: Some("^Bash$".to_string()), + hooks: vec![HookHandlerConfig::Command { + command: HookCommandConfig::ByPlatform(HookCommandByPlatformConfig { + unix: "bash /enterprise/hooks/pre.sh".to_string(), + windows: r"powershell -File C:\enterprise\hooks\pre.ps1".to_string(), + }), + timeout_sec: None, + r#async: false, + status_message: None, + }], + }], + ..Default::default() + } + ); +} diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs index e88c736db0f8..cced3e589261 100644 --- a/codex-rs/config/src/lib.rs +++ b/codex-rs/config/src/lib.rs @@ -72,6 +72,8 @@ pub use diagnostics::format_config_error; pub use diagnostics::format_config_error_with_source; pub use diagnostics::io_error_from_config_error; pub use fingerprint::version_for_toml; +pub use hook_config::HookCommandByPlatformConfig; +pub use hook_config::HookCommandConfig; pub use hook_config::HookEventsToml; pub use hook_config::HookHandlerConfig; pub use hook_config::HookStateToml; diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 4a7a33b7e6fd..2ee9e308ee86 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -1156,7 +1156,10 @@ async fn load_config_layers_includes_cloud_hook_requirements() -> anyhow::Result pre_tool_use: vec![codex_config::MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![codex_config::HookHandlerConfig::Command { - command: format!("python3 {}/pre.py", managed_dir.display()), + command: codex_config::HookCommandConfig::Single(format!( + "python3 {}/pre.py", + managed_dir.display() + )), timeout_sec: Some(10), r#async: false, status_message: Some("checking".to_string()), diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 6a5b08c12425..df45539104b4 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -1311,7 +1311,9 @@ async fn refresh_runtime_config_refreshes_hooks() -> anyhow::Result<()> { group: codex_config::MatcherGroup { matcher: None, hooks: vec![codex_config::HookHandlerConfig::Command { - command: "python3 /tmp/user.py".to_string(), + command: codex_config::HookCommandConfig::Single( + "python3 /tmp/user.py".to_string(), + ), timeout_sec: Some(600), r#async: false, status_message: None, diff --git a/codex-rs/hooks/src/declarations.rs b/codex-rs/hooks/src/declarations.rs index 6c414eaf8195..5905b286df2c 100644 --- a/codex-rs/hooks/src/declarations.rs +++ b/codex-rs/hooks/src/declarations.rs @@ -38,6 +38,7 @@ pub(crate) fn plugin_hook_key_source(plugin_id: &str, source_relative_path: &str #[cfg(test)] mod tests { + use codex_config::HookCommandConfig; use codex_config::HookEventsToml; use codex_config::HookHandlerConfig; use codex_config::MatcherGroup; @@ -64,7 +65,7 @@ mod tests { hooks: vec![ HookHandlerConfig::Prompt {}, HookHandlerConfig::Command { - command: "echo hi".to_string(), + command: HookCommandConfig::Single("echo hi".to_string()), timeout_sec: None, r#async: false, status_message: None, diff --git a/codex-rs/hooks/src/engine/discovery.rs b/codex-rs/hooks/src/engine/discovery.rs index cc180325b65e..e3ab9acfc177 100644 --- a/codex-rs/hooks/src/engine/discovery.rs +++ b/codex-rs/hooks/src/engine/discovery.rs @@ -392,6 +392,7 @@ fn append_matcher_groups( r#async, status_message, } => { + let command = command.for_current_platform().to_string(); if r#async { warnings.push(format!( "skipping async hook in {}: async hooks are not supported yet", @@ -408,7 +409,7 @@ fn append_matcher_groups( } let timeout_sec = timeout_sec.unwrap_or(600).max(1); let normalized_handler = HookHandlerConfig::Command { - command: command.clone(), + command: codex_config::HookCommandConfig::Single(command.clone()), timeout_sec: Some(timeout_sec), r#async, status_message: status_message.clone(), @@ -565,6 +566,8 @@ fn hook_source_for_requirement_source(source: Option<&RequirementSource>) -> Hoo mod tests { use codex_config::ConfigLayerEntry; use codex_config::ConfigLayerSource; + use codex_config::HookCommandByPlatformConfig; + use codex_config::HookCommandConfig; use codex_config::HookEventsToml; use codex_protocol::protocol::HookEventName; use codex_protocol::protocol::HookSource; @@ -607,7 +610,7 @@ mod tests { MatcherGroup { matcher: matcher.map(str::to_string), hooks: vec![HookHandlerConfig::Command { - command: "echo hello".to_string(), + command: HookCommandConfig::Single("echo hello".to_string()), timeout_sec: None, r#async: false, status_message: None, @@ -752,7 +755,7 @@ mod tests { session_start: vec![MatcherGroup { matcher: None, hooks: vec![HookHandlerConfig::Command { - command: "echo hello".to_string(), + command: HookCommandConfig::Single("echo hello".to_string()), timeout_sec: None, r#async: false, status_message: None, @@ -763,6 +766,47 @@ mod tests { ); } + #[test] + fn pre_tool_use_resolves_platform_specific_command_during_discovery() { + let mut handlers = 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 Vec::new(), + &mut warnings, + &mut display_order, + &hook_handler_source(&source_path, &hook_states), + HookEventName::PreToolUse, + vec![MatcherGroup { + matcher: Some("^Bash$".to_string()), + hooks: vec![HookHandlerConfig::Command { + command: HookCommandConfig::ByPlatform(HookCommandByPlatformConfig { + unix: "echo unix".to_string(), + windows: "echo windows".to_string(), + }), + timeout_sec: None, + r#async: false, + status_message: None, + }], + }], + ); + + assert_eq!(warnings, Vec::::new()); + assert_eq!(handlers.len(), 1); + assert_eq!( + handlers[0].command, + if cfg!(windows) { + "echo windows" + } else { + "echo unix" + } + ); + } + fn config_with_malformed_state_and_session_start_hook() -> TomlValue { serde_json::from_value(serde_json::json!({ "hooks": { diff --git a/codex-rs/hooks/src/engine/mod_tests.rs b/codex-rs/hooks/src/engine/mod_tests.rs index 32739165f1a3..74f54bccd8d6 100644 --- a/codex-rs/hooks/src/engine/mod_tests.rs +++ b/codex-rs/hooks/src/engine/mod_tests.rs @@ -10,6 +10,7 @@ use codex_config::ConfigRequirements; use codex_config::ConfigRequirementsToml; use codex_config::Constrained; use codex_config::ConstrainedWithSource; +use codex_config::HookCommandConfig; use codex_config::HookEventsToml; use codex_config::HookHandlerConfig; use codex_config::ManagedHooksRequirementsToml; @@ -84,7 +85,10 @@ with Path(r"{log_path}").open("a", encoding="utf-8") as handle: pre_tool_use: vec![MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { - command: format!("python3 {}", script_path.display()), + command: HookCommandConfig::Single(format!( + "python3 {}", + script_path.display() + )), timeout_sec: Some(10), r#async: false, status_message: Some("checking".to_string()), @@ -181,7 +185,7 @@ fn unknown_requirement_source_hooks_stay_managed() { pre_tool_use: vec![MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { - command: "python3 /tmp/managed.py".to_string(), + command: HookCommandConfig::Single("python3 /tmp/managed.py".to_string()), timeout_sec: Some(10), r#async: false, status_message: Some("checking".to_string()), @@ -243,7 +247,7 @@ fn user_disablement_filters_non_managed_hooks_but_not_managed_hooks() { pre_tool_use: vec![MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { - command: "python3 /tmp/managed.py".to_string(), + command: HookCommandConfig::Single("python3 /tmp/managed.py".to_string()), timeout_sec: Some(10), r#async: false, status_message: Some("checking".to_string()), @@ -462,7 +466,10 @@ fn requirements_managed_hooks_warn_when_managed_dir_is_missing() { pre_tool_use: vec![MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { - command: format!("python3 {}", missing_dir.join("pre.py").display()), + command: HookCommandConfig::Single(format!( + "python3 {}", + missing_dir.join("pre.py").display() + )), timeout_sec: Some(10), r#async: false, status_message: Some("checking".to_string()), @@ -673,7 +680,10 @@ print(json.dumps({ pre_tool_use: vec![MatcherGroup { matcher: Some("Bash".to_string()), hooks: vec![HookHandlerConfig::Command { - command: format!("python3 {}", script_path.display()), + command: HookCommandConfig::Single(format!( + "python3 {}", + script_path.display() + )), timeout_sec: Some(10), r#async: false, status_message: None, @@ -780,8 +790,10 @@ fn plugin_hook_sources_expand_plugin_placeholders() { pre_tool_use: vec![MatcherGroup { matcher: Some("Bash".to_string()), hooks: vec![HookHandlerConfig::Command { - command: "run ${PLUGIN_ROOT} ${CLAUDE_PLUGIN_ROOT} ${PLUGIN_DATA} ${CLAUDE_PLUGIN_DATA}" - .to_string(), + command: HookCommandConfig::Single( + "run ${PLUGIN_ROOT} ${CLAUDE_PLUGIN_ROOT} ${PLUGIN_DATA} ${CLAUDE_PLUGIN_DATA}" + .to_string(), + ), timeout_sec: Some(5), r#async: false, status_message: None, diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 1d4bf2b858ec..29eb6958efed 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -929,7 +929,9 @@ approval_policy = "never" pre_tool_use: vec![MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { - command: "python3 /enterprise/hooks/pre.py".to_string(), + command: codex_config::HookCommandConfig::Single( + "python3 /enterprise/hooks/pre.py".to_string(), + ), timeout_sec: Some(10), r#async: false, status_message: Some("checking".to_string()), From 41cc0fb925366d56d9babf85748e9b105c67dbcb Mon Sep 17 00:00:00 2001 From: Abhinav Vedmala Date: Mon, 11 May 2026 10:44:59 -0400 Subject: [PATCH 2/7] Cover platform hook command execution --- codex-rs/hooks/src/engine/mod_tests.rs | 90 ++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/codex-rs/hooks/src/engine/mod_tests.rs b/codex-rs/hooks/src/engine/mod_tests.rs index 74f54bccd8d6..786d7f690ed3 100644 --- a/codex-rs/hooks/src/engine/mod_tests.rs +++ b/codex-rs/hooks/src/engine/mod_tests.rs @@ -10,6 +10,7 @@ use codex_config::ConfigRequirements; use codex_config::ConfigRequirementsToml; use codex_config::Constrained; use codex_config::ConstrainedWithSource; +use codex_config::HookCommandByPlatformConfig; use codex_config::HookCommandConfig; use codex_config::HookEventsToml; use codex_config::HookHandlerConfig; @@ -173,6 +174,95 @@ with Path(r"{log_path}").open("a", encoding="utf-8") as handle: assert!(log_contents.contains("\"hook_event_name\": \"PreToolUse\"")); } +#[tokio::test] +async fn requirements_managed_hooks_execute_platform_specific_command() { + let temp = tempdir().expect("create temp dir"); + let managed_dir = + AbsolutePathBuf::try_from(temp.path().join("managed-hooks")).expect("absolute path"); + fs::create_dir_all(managed_dir.as_path()).expect("create managed hooks dir"); + let unix_log_path = managed_dir.join("unix_pre_tool_use_log.jsonl"); + let windows_log_path = managed_dir.join("windows_pre_tool_use_log.jsonl"); + + let python_command = |log_path: &Path| { + format!( + r#"python3 -c 'import json, pathlib, sys; payload = json.load(sys.stdin); pathlib.Path(r"{log_path}").write_text(json.dumps(payload), encoding="utf-8")'"#, + log_path = log_path.display() + ) + }; + + let managed_hooks = managed_hooks_for_current_platform( + managed_dir, + HookEventsToml { + pre_tool_use: vec![MatcherGroup { + matcher: Some("^Bash$".to_string()), + hooks: vec![HookHandlerConfig::Command { + command: HookCommandConfig::ByPlatform(HookCommandByPlatformConfig { + unix: python_command(unix_log_path.as_path()), + windows: python_command(windows_log_path.as_path()), + }), + timeout_sec: Some(10), + r#async: false, + status_message: Some("checking".to_string()), + }], + }], + ..Default::default() + }, + ); + let config_layer_stack = ConfigLayerStack::new( + Vec::new(), + ConfigRequirements { + managed_hooks: Some(ConstrainedWithSource::new( + Constrained::allow_any(managed_hooks.clone()), + Some(RequirementSource::CloudRequirements), + )), + ..ConfigRequirements::default() + }, + ConfigRequirementsToml { + hooks: Some(managed_hooks), + ..ConfigRequirementsToml::default() + }, + ) + .expect("config layer stack"); + + let engine = ClaudeHooksEngine::new( + /*enabled*/ true, + Some(&config_layer_stack), + Vec::new(), + Vec::new(), + CommandShell { + program: String::new(), + args: Vec::new(), + }, + ); + + let outcome = engine + .run_pre_tool_use(PreToolUseRequest { + session_id: ThreadId::new(), + turn_id: "turn-1".to_string(), + cwd: cwd(), + transcript_path: None, + model: "gpt-test".to_string(), + permission_mode: "default".to_string(), + tool_name: "Bash".to_string(), + matcher_aliases: Vec::new(), + tool_use_id: "tool-1".to_string(), + tool_input: serde_json::json!({ "command": "echo hello" }), + }) + .await; + + assert!(!outcome.should_block); + assert_eq!( + unix_log_path.exists(), + cfg!(not(windows)), + "only the Unix command should run off Windows" + ); + assert_eq!( + windows_log_path.exists(), + cfg!(windows), + "only the Windows command should run on Windows" + ); +} + #[test] fn unknown_requirement_source_hooks_stay_managed() { let temp = tempdir().expect("create temp dir"); From 9aba3525438c74a64c00eb33b7e7214573176666 Mon Sep 17 00:00:00 2001 From: Abhinav Vedmala Date: Mon, 11 May 2026 10:58:32 -0400 Subject: [PATCH 3/7] Refresh config schema for platform hook commands --- codex-rs/core/config.schema.json | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index edbf45694f75..cd840fa2e372 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -945,6 +945,31 @@ } ] }, + "HookCommandByPlatformConfig": { + "properties": { + "unix": { + "type": "string" + }, + "windows": { + "type": "string" + } + }, + "required": [ + "unix", + "windows" + ], + "type": "object" + }, + "HookCommandConfig": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/HookCommandByPlatformConfig" + } + ] + }, "HookHandlerConfig": { "oneOf": [ { @@ -954,7 +979,7 @@ "type": "boolean" }, "command": { - "type": "string" + "$ref": "#/definitions/HookCommandConfig" }, "statusMessage": { "default": null, From bb8257cd70ed46cbd871e8f23701a41f74778332 Mon Sep 17 00:00:00 2001 From: Abhinav Vedmala Date: Mon, 11 May 2026 11:11:40 -0400 Subject: [PATCH 4/7] Fix platform hook command test portability --- codex-rs/hooks/src/engine/mod_tests.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/codex-rs/hooks/src/engine/mod_tests.rs b/codex-rs/hooks/src/engine/mod_tests.rs index 786d7f690ed3..69c49a4ac6b1 100644 --- a/codex-rs/hooks/src/engine/mod_tests.rs +++ b/codex-rs/hooks/src/engine/mod_tests.rs @@ -183,12 +183,8 @@ async fn requirements_managed_hooks_execute_platform_specific_command() { let unix_log_path = managed_dir.join("unix_pre_tool_use_log.jsonl"); let windows_log_path = managed_dir.join("windows_pre_tool_use_log.jsonl"); - let python_command = |log_path: &Path| { - format!( - r#"python3 -c 'import json, pathlib, sys; payload = json.load(sys.stdin); pathlib.Path(r"{log_path}").write_text(json.dumps(payload), encoding="utf-8")'"#, - log_path = log_path.display() - ) - }; + let unix_command = |log_path: &Path| format!(r#"printf ok > "{}""#, log_path.display()); + let windows_command = |log_path: &Path| format!(r#"echo ok > "{}""#, log_path.display()); let managed_hooks = managed_hooks_for_current_platform( managed_dir, @@ -197,8 +193,8 @@ async fn requirements_managed_hooks_execute_platform_specific_command() { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { command: HookCommandConfig::ByPlatform(HookCommandByPlatformConfig { - unix: python_command(unix_log_path.as_path()), - windows: python_command(windows_log_path.as_path()), + unix: unix_command(unix_log_path.as_path()), + windows: windows_command(windows_log_path.as_path()), }), timeout_sec: Some(10), r#async: false, From c77db45be65efb97cf9b18a8a486c5eef35a6e6d Mon Sep 17 00:00:00 2001 From: Abhinav Vedmala Date: Mon, 11 May 2026 11:39:50 -0400 Subject: [PATCH 5/7] Make platform hook command test portable --- codex-rs/hooks/src/engine/mod_tests.rs | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/codex-rs/hooks/src/engine/mod_tests.rs b/codex-rs/hooks/src/engine/mod_tests.rs index 69c49a4ac6b1..ab8e5b71da33 100644 --- a/codex-rs/hooks/src/engine/mod_tests.rs +++ b/codex-rs/hooks/src/engine/mod_tests.rs @@ -21,6 +21,7 @@ use codex_config::TomlValue; use codex_plugin::PluginHookSource; use codex_plugin::PluginId; use codex_protocol::ThreadId; +use codex_protocol::protocol::HookOutputEntry; use codex_protocol::protocol::HookOutputEntryKind; use codex_protocol::protocol::HookRunStatus; use codex_protocol::protocol::HookSource; @@ -180,11 +181,6 @@ async fn requirements_managed_hooks_execute_platform_specific_command() { let managed_dir = AbsolutePathBuf::try_from(temp.path().join("managed-hooks")).expect("absolute path"); fs::create_dir_all(managed_dir.as_path()).expect("create managed hooks dir"); - let unix_log_path = managed_dir.join("unix_pre_tool_use_log.jsonl"); - let windows_log_path = managed_dir.join("windows_pre_tool_use_log.jsonl"); - - let unix_command = |log_path: &Path| format!(r#"printf ok > "{}""#, log_path.display()); - let windows_command = |log_path: &Path| format!(r#"echo ok > "{}""#, log_path.display()); let managed_hooks = managed_hooks_for_current_platform( managed_dir, @@ -193,8 +189,8 @@ async fn requirements_managed_hooks_execute_platform_specific_command() { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { command: HookCommandConfig::ByPlatform(HookCommandByPlatformConfig { - unix: unix_command(unix_log_path.as_path()), - windows: windows_command(windows_log_path.as_path()), + unix: "exit 17".to_string(), + windows: "exit /B 19".to_string(), }), timeout_sec: Some(10), r#async: false, @@ -247,15 +243,15 @@ async fn requirements_managed_hooks_execute_platform_specific_command() { .await; assert!(!outcome.should_block); + let expected_exit_code = if cfg!(windows) { 19 } else { 17 }; + assert_eq!(outcome.hook_events.len(), 1); + assert_eq!(outcome.hook_events[0].run.status, HookRunStatus::Failed); assert_eq!( - unix_log_path.exists(), - cfg!(not(windows)), - "only the Unix command should run off Windows" - ); - assert_eq!( - windows_log_path.exists(), - cfg!(windows), - "only the Windows command should run on Windows" + outcome.hook_events[0].run.entries, + vec![HookOutputEntry { + kind: HookOutputEntryKind::Error, + text: format!("hook exited with code {expected_exit_code}"), + }] ); } From 8061b25e0d1378097c90249e724dd57a6a559fd5 Mon Sep 17 00:00:00 2001 From: Abhinav Vedmala Date: Mon, 11 May 2026 16:46:18 -0400 Subject: [PATCH 6/7] Use command_windows for hook overrides --- .../codex_app_server_protocol.schemas.json | 33 ++++----------- .../codex_app_server_protocol.v2.schemas.json | 33 ++++----------- .../v2/ConfigRequirementsReadResponse.json | 33 ++++----------- .../typescript/v2/ConfiguredHookCommand.ts | 6 --- .../v2/ConfiguredHookCommandByPlatform.ts | 5 --- .../typescript/v2/ConfiguredHookHandler.ts | 3 +- .../schema/typescript/v2/index.ts | 2 - .../src/protocol/v2/config.rs | 21 ++-------- .../request_processors/config_processor.rs | 17 ++------ .../app-server/tests/suite/v2/hooks_list.rs | 3 +- codex-rs/config/src/hook_config.rs | 40 ++----------------- codex-rs/config/src/hooks_tests.rs | 31 +++++++------- codex-rs/config/src/lib.rs | 2 - codex-rs/core/config.schema.json | 31 +++----------- .../core/src/config/config_loader_tests.rs | 6 +-- codex-rs/core/src/session/tests.rs | 5 +-- codex-rs/hooks/src/declarations.rs | 4 +- codex-rs/hooks/src/engine/discovery.rs | 26 +++++++----- codex-rs/hooks/src/engine/mod_tests.rs | 38 +++++++----------- codex-rs/tui/src/debug_config.rs | 5 +-- 20 files changed, 93 insertions(+), 251 deletions(-) delete mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookCommand.ts delete mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookCommandByPlatform.ts 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 154258cb4158..4e061f8591f1 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 @@ -7714,31 +7714,6 @@ "title": "ConfigWriteResponse", "type": "object" }, - "ConfiguredHookCommand": { - "anyOf": [ - { - "type": "string" - }, - { - "$ref": "#/definitions/v2/ConfiguredHookCommandByPlatform" - } - ] - }, - "ConfiguredHookCommandByPlatform": { - "properties": { - "unix": { - "type": "string" - }, - "windows": { - "type": "string" - } - }, - "required": [ - "unix", - "windows" - ], - "type": "object" - }, "ConfiguredHookHandler": { "oneOf": [ { @@ -7747,7 +7722,13 @@ "type": "boolean" }, "command": { - "$ref": "#/definitions/v2/ConfiguredHookCommand" + "type": "string" + }, + "commandWindows": { + "type": [ + "string", + "null" + ] }, "statusMessage": { "type": [ 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 c6a666e758f1..c7130126f9bc 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 @@ -4103,31 +4103,6 @@ "title": "ConfigWriteResponse", "type": "object" }, - "ConfiguredHookCommand": { - "anyOf": [ - { - "type": "string" - }, - { - "$ref": "#/definitions/ConfiguredHookCommandByPlatform" - } - ] - }, - "ConfiguredHookCommandByPlatform": { - "properties": { - "unix": { - "type": "string" - }, - "windows": { - "type": "string" - } - }, - "required": [ - "unix", - "windows" - ], - "type": "object" - }, "ConfiguredHookHandler": { "oneOf": [ { @@ -4136,7 +4111,13 @@ "type": "boolean" }, "command": { - "$ref": "#/definitions/ConfiguredHookCommand" + "type": "string" + }, + "commandWindows": { + "type": [ + "string", + "null" + ] }, "statusMessage": { "type": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json index f1d7e5eae56d..b229d8ef9484 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -111,31 +111,6 @@ }, "type": "object" }, - "ConfiguredHookCommand": { - "anyOf": [ - { - "type": "string" - }, - { - "$ref": "#/definitions/ConfiguredHookCommandByPlatform" - } - ] - }, - "ConfiguredHookCommandByPlatform": { - "properties": { - "unix": { - "type": "string" - }, - "windows": { - "type": "string" - } - }, - "required": [ - "unix", - "windows" - ], - "type": "object" - }, "ConfiguredHookHandler": { "oneOf": [ { @@ -144,7 +119,13 @@ "type": "boolean" }, "command": { - "$ref": "#/definitions/ConfiguredHookCommand" + "type": "string" + }, + "commandWindows": { + "type": [ + "string", + "null" + ] }, "statusMessage": { "type": [ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookCommand.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookCommand.ts deleted file mode 100644 index 9ed8cfe8f7d5..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookCommand.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConfiguredHookCommandByPlatform } from "./ConfiguredHookCommandByPlatform"; - -export type ConfiguredHookCommand = string | ConfiguredHookCommandByPlatform; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookCommandByPlatform.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookCommandByPlatform.ts deleted file mode 100644 index 743e23feef89..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookCommandByPlatform.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ConfiguredHookCommandByPlatform = { unix: string, windows: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookHandler.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookHandler.ts index 41745c14d66d..42b05cf8cc01 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookHandler.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfiguredHookHandler.ts @@ -1,6 +1,5 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConfiguredHookCommand } from "./ConfiguredHookCommand"; -export type ConfiguredHookHandler = { "type": "command", command: ConfiguredHookCommand, timeoutSec: bigint | null, async: boolean, statusMessage: string | null, } | { "type": "prompt", } | { "type": "agent", }; +export type ConfiguredHookHandler = { "type": "command", command: string, commandWindows: string | null, timeoutSec: bigint | null, async: boolean, statusMessage: string | null, } | { "type": "prompt", } | { "type": "agent", }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 5e8ee434e4ce..a6b961366e0f 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -76,8 +76,6 @@ export type { ConfigRequirementsReadResponse } from "./ConfigRequirementsReadRes export type { ConfigValueWriteParams } from "./ConfigValueWriteParams"; export type { ConfigWarningNotification } from "./ConfigWarningNotification"; export type { ConfigWriteResponse } from "./ConfigWriteResponse"; -export type { ConfiguredHookCommand } from "./ConfiguredHookCommand"; -export type { ConfiguredHookCommandByPlatform } from "./ConfiguredHookCommandByPlatform"; export type { ConfiguredHookHandler } from "./ConfiguredHookHandler"; export type { ConfiguredHookMatcherGroup } from "./ConfiguredHookMatcherGroup"; export type { ContextCompactedNotification } from "./ContextCompactedNotification"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/config.rs b/codex-rs/app-server-protocol/src/protocol/v2/config.rs index cfdbb31f3897..35e487e45ee2 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -412,7 +412,10 @@ pub enum ConfiguredHookHandler { #[serde(rename = "command")] #[ts(rename = "command")] Command { - command: ConfiguredHookCommand, + command: String, + #[serde(rename = "commandWindows")] + #[ts(rename = "commandWindows")] + command_windows: Option, #[serde(rename = "timeoutSec")] #[ts(rename = "timeoutSec")] timeout_sec: Option, @@ -429,22 +432,6 @@ pub enum ConfiguredHookHandler { Agent {}, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(untagged)] -#[ts(export_to = "v2/")] -pub enum ConfiguredHookCommand { - Single(String), - ByPlatform(ConfiguredHookCommandByPlatform), -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(rename_all = "camelCase", export_to = "v2/")] -pub struct ConfiguredHookCommandByPlatform { - pub unix: String, - pub windows: String, -} - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server/src/request_processors/config_processor.rs b/codex-rs/app-server/src/request_processors/config_processor.rs index 10c45b936ae9..2f2a7b1f9ba4 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -18,8 +18,6 @@ use codex_app_server_protocol::ConfigRequirementsReadResponse; use codex_app_server_protocol::ConfigValueWriteParams; use codex_app_server_protocol::ConfigWriteErrorCode; use codex_app_server_protocol::ConfigWriteResponse; -use codex_app_server_protocol::ConfiguredHookCommand; -use codex_app_server_protocol::ConfiguredHookCommandByPlatform; use codex_app_server_protocol::ConfiguredHookHandler; use codex_app_server_protocol::ConfiguredHookMatcherGroup; use codex_app_server_protocol::ExperimentalFeatureEnablementSetParams; @@ -34,8 +32,6 @@ use codex_app_server_protocol::SandboxMode; use codex_app_server_protocol::ServerNotification; use codex_chatgpt::connectors; use codex_config::ConfigRequirementsToml; -use codex_config::HookCommandByPlatformConfig; -use codex_config::HookCommandConfig; use codex_config::HookEventsToml; use codex_config::HookHandlerConfig as CoreHookHandlerConfig; use codex_config::ManagedHooksRequirementsToml; @@ -518,11 +514,13 @@ fn map_hook_handler_to_api(handler: CoreHookHandlerConfig) -> ConfiguredHookHand match handler { CoreHookHandlerConfig::Command { command, + command_windows, timeout_sec, r#async, status_message, } => ConfiguredHookHandler::Command { - command: map_hook_command_to_api(command), + command, + command_windows, timeout_sec, r#async, status_message, @@ -532,15 +530,6 @@ fn map_hook_handler_to_api(handler: CoreHookHandlerConfig) -> ConfiguredHookHand } } -fn map_hook_command_to_api(command: HookCommandConfig) -> ConfiguredHookCommand { - match command { - HookCommandConfig::Single(command) => ConfiguredHookCommand::Single(command), - HookCommandConfig::ByPlatform(HookCommandByPlatformConfig { unix, windows }) => { - ConfiguredHookCommand::ByPlatform(ConfiguredHookCommandByPlatform { unix, windows }) - } - } -} - fn map_sandbox_mode_requirement_to_api(mode: CoreSandboxModeRequirement) -> Option { match mode { CoreSandboxModeRequirement::ReadOnly => Some(SandboxMode::ReadOnly), 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 8b90335480fe..f3ce4cbf2d10 100644 --- a/codex-rs/app-server/tests/suite/v2/hooks_list.rs +++ b/codex-rs/app-server/tests/suite/v2/hooks_list.rs @@ -52,7 +52,8 @@ fn command_hook_hash( group: codex_config::MatcherGroup { matcher: matcher.map(ToOwned::to_owned), hooks: vec![codex_config::HookHandlerConfig::Command { - command: codex_config::HookCommandConfig::Single(command.to_string()), + command: command.to_string(), + command_windows: None, timeout_sec: Some(timeout_sec), r#async: false, status_message: status_message.map(ToOwned::to_owned), diff --git a/codex-rs/config/src/hook_config.rs b/codex-rs/config/src/hook_config.rs index 791693e16fec..feb844215b99 100644 --- a/codex-rs/config/src/hook_config.rs +++ b/codex-rs/config/src/hook_config.rs @@ -125,7 +125,9 @@ pub struct MatcherGroup { pub enum HookHandlerConfig { #[serde(rename = "command")] Command { - command: HookCommandConfig, + command: String, + #[serde(default)] + command_windows: Option, #[serde(default, rename = "timeout")] timeout_sec: Option, #[serde(default)] @@ -139,42 +141,6 @@ pub enum HookHandlerConfig { Agent {}, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(untagged)] -pub enum HookCommandConfig { - Single(String), - ByPlatform(HookCommandByPlatformConfig), -} - -impl HookCommandConfig { - pub fn for_current_platform(&self) -> &str { - match self { - Self::Single(command) => command, - Self::ByPlatform(command) => command.for_current_platform(), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct HookCommandByPlatformConfig { - pub unix: String, - pub windows: String, -} - -impl HookCommandByPlatformConfig { - fn for_current_platform(&self) -> &str { - #[cfg(windows)] - { - &self.windows - } - - #[cfg(not(windows))] - { - &self.unix - } - } -} - #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ManagedHooksRequirementsToml { pub managed_dir: Option, diff --git a/codex-rs/config/src/hooks_tests.rs b/codex-rs/config/src/hooks_tests.rs index 2d7fcc430527..c8d1ab78d3c7 100644 --- a/codex-rs/config/src/hooks_tests.rs +++ b/codex-rs/config/src/hooks_tests.rs @@ -2,8 +2,6 @@ use pretty_assertions::assert_eq; use std::collections::BTreeMap; -use super::HookCommandByPlatformConfig; -use super::HookCommandConfig; use super::HookEventsToml; use super::HookHandlerConfig; use super::HooksFile; @@ -41,7 +39,8 @@ fn hooks_file_deserializes_existing_json_shape() { pre_tool_use: vec![MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { - command: HookCommandConfig::Single("python3 /tmp/pre.py".to_string()), + command: "python3 /tmp/pre.py".to_string(), + command_windows: None, timeout_sec: Some(10), r#async: false, status_message: Some("checking".to_string()), @@ -75,7 +74,8 @@ statusMessage = "checking" pre_tool_use: vec![MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { - command: HookCommandConfig::Single("python3 /tmp/pre.py".to_string()), + command: "python3 /tmp/pre.py".to_string(), + command_windows: None, timeout_sec: Some(10), r#async: false, status_message: Some("checking".to_string()), @@ -111,7 +111,8 @@ command = "python3 /tmp/pre.py" pre_tool_use: vec![MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { - command: HookCommandConfig::Single("python3 /tmp/pre.py".to_string()), + command: "python3 /tmp/pre.py".to_string(), + command_windows: None, timeout_sec: None, r#async: false, status_message: None, @@ -155,9 +156,8 @@ command = "python3 /enterprise/place/pre.py" pre_tool_use: vec![MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { - command: HookCommandConfig::Single( - "python3 /enterprise/place/pre.py".to_string(), - ), + command: "python3 /enterprise/place/pre.py".to_string(), + command_windows: None, timeout_sec: None, r#async: false, status_message: None, @@ -170,7 +170,7 @@ command = "python3 /enterprise/place/pre.py" } #[test] -fn hook_events_deserialize_platform_command_from_toml() { +fn hook_events_deserialize_windows_override_from_toml() { let parsed: HookEventsToml = toml::from_str( r#" [[PreToolUse]] @@ -178,10 +178,11 @@ matcher = "^Bash$" [[PreToolUse.hooks]] type = "command" -command = { unix = "bash /enterprise/hooks/pre.sh", windows = "powershell -File C:\\enterprise\\hooks\\pre.ps1" } +command = "bash /enterprise/hooks/pre.sh" +command_windows = "powershell -File C:\\enterprise\\hooks\\pre.ps1" "#, ) - .expect("platform hook command TOML should deserialize"); + .expect("hook command Windows override TOML should deserialize"); assert_eq!( parsed, @@ -189,10 +190,10 @@ command = { unix = "bash /enterprise/hooks/pre.sh", windows = "powershell -File pre_tool_use: vec![MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { - command: HookCommandConfig::ByPlatform(HookCommandByPlatformConfig { - unix: "bash /enterprise/hooks/pre.sh".to_string(), - windows: r"powershell -File C:\enterprise\hooks\pre.ps1".to_string(), - }), + command: "bash /enterprise/hooks/pre.sh".to_string(), + command_windows: Some( + r"powershell -File C:\enterprise\hooks\pre.ps1".to_string(), + ), timeout_sec: None, r#async: false, status_message: None, diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs index cced3e589261..e88c736db0f8 100644 --- a/codex-rs/config/src/lib.rs +++ b/codex-rs/config/src/lib.rs @@ -72,8 +72,6 @@ pub use diagnostics::format_config_error; pub use diagnostics::format_config_error_with_source; pub use diagnostics::io_error_from_config_error; pub use fingerprint::version_for_toml; -pub use hook_config::HookCommandByPlatformConfig; -pub use hook_config::HookCommandConfig; pub use hook_config::HookEventsToml; pub use hook_config::HookHandlerConfig; pub use hook_config::HookStateToml; diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index cd840fa2e372..79e47e0a1dfc 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -945,31 +945,6 @@ } ] }, - "HookCommandByPlatformConfig": { - "properties": { - "unix": { - "type": "string" - }, - "windows": { - "type": "string" - } - }, - "required": [ - "unix", - "windows" - ], - "type": "object" - }, - "HookCommandConfig": { - "anyOf": [ - { - "type": "string" - }, - { - "$ref": "#/definitions/HookCommandByPlatformConfig" - } - ] - }, "HookHandlerConfig": { "oneOf": [ { @@ -979,7 +954,11 @@ "type": "boolean" }, "command": { - "$ref": "#/definitions/HookCommandConfig" + "type": "string" + }, + "command_windows": { + "default": null, + "type": "string" }, "statusMessage": { "default": null, diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 2ee9e308ee86..6296d1388622 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -1156,10 +1156,8 @@ async fn load_config_layers_includes_cloud_hook_requirements() -> anyhow::Result pre_tool_use: vec![codex_config::MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![codex_config::HookHandlerConfig::Command { - command: codex_config::HookCommandConfig::Single(format!( - "python3 {}/pre.py", - managed_dir.display() - )), + command: format!("python3 {}/pre.py", managed_dir.display()), + command_windows: None, timeout_sec: Some(10), r#async: false, status_message: Some("checking".to_string()), diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index df45539104b4..a001162cf026 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -1311,9 +1311,8 @@ async fn refresh_runtime_config_refreshes_hooks() -> anyhow::Result<()> { group: codex_config::MatcherGroup { matcher: None, hooks: vec![codex_config::HookHandlerConfig::Command { - command: codex_config::HookCommandConfig::Single( - "python3 /tmp/user.py".to_string(), - ), + command: "python3 /tmp/user.py".to_string(), + command_windows: None, timeout_sec: Some(600), r#async: false, status_message: None, diff --git a/codex-rs/hooks/src/declarations.rs b/codex-rs/hooks/src/declarations.rs index 5905b286df2c..468fbc947a79 100644 --- a/codex-rs/hooks/src/declarations.rs +++ b/codex-rs/hooks/src/declarations.rs @@ -38,7 +38,6 @@ pub(crate) fn plugin_hook_key_source(plugin_id: &str, source_relative_path: &str #[cfg(test)] mod tests { - use codex_config::HookCommandConfig; use codex_config::HookEventsToml; use codex_config::HookHandlerConfig; use codex_config::MatcherGroup; @@ -65,7 +64,8 @@ mod tests { hooks: vec![ HookHandlerConfig::Prompt {}, HookHandlerConfig::Command { - command: HookCommandConfig::Single("echo hi".to_string()), + command: "echo hi".to_string(), + command_windows: None, timeout_sec: None, r#async: false, status_message: None, diff --git a/codex-rs/hooks/src/engine/discovery.rs b/codex-rs/hooks/src/engine/discovery.rs index e3ab9acfc177..8dac67e30cb9 100644 --- a/codex-rs/hooks/src/engine/discovery.rs +++ b/codex-rs/hooks/src/engine/discovery.rs @@ -388,11 +388,16 @@ fn append_matcher_groups( match handler { HookHandlerConfig::Command { command, + command_windows, timeout_sec, r#async, status_message, } => { - let command = command.for_current_platform().to_string(); + let command = if cfg!(windows) { + command_windows.unwrap_or(command) + } else { + command + }; if r#async { warnings.push(format!( "skipping async hook in {}: async hooks are not supported yet", @@ -409,7 +414,8 @@ fn append_matcher_groups( } let timeout_sec = timeout_sec.unwrap_or(600).max(1); let normalized_handler = HookHandlerConfig::Command { - command: codex_config::HookCommandConfig::Single(command.clone()), + command: command.clone(), + command_windows: None, timeout_sec: Some(timeout_sec), r#async, status_message: status_message.clone(), @@ -566,8 +572,6 @@ fn hook_source_for_requirement_source(source: Option<&RequirementSource>) -> Hoo mod tests { use codex_config::ConfigLayerEntry; use codex_config::ConfigLayerSource; - use codex_config::HookCommandByPlatformConfig; - use codex_config::HookCommandConfig; use codex_config::HookEventsToml; use codex_protocol::protocol::HookEventName; use codex_protocol::protocol::HookSource; @@ -610,7 +614,8 @@ mod tests { MatcherGroup { matcher: matcher.map(str::to_string), hooks: vec![HookHandlerConfig::Command { - command: HookCommandConfig::Single("echo hello".to_string()), + command: "echo hello".to_string(), + command_windows: None, timeout_sec: None, r#async: false, status_message: None, @@ -755,7 +760,8 @@ mod tests { session_start: vec![MatcherGroup { matcher: None, hooks: vec![HookHandlerConfig::Command { - command: HookCommandConfig::Single("echo hello".to_string()), + command: "echo hello".to_string(), + command_windows: None, timeout_sec: None, r#async: false, status_message: None, @@ -767,7 +773,7 @@ mod tests { } #[test] - fn pre_tool_use_resolves_platform_specific_command_during_discovery() { + fn pre_tool_use_resolves_windows_command_override_during_discovery() { let mut handlers = Vec::new(); let mut warnings = Vec::new(); let mut display_order = 0; @@ -784,10 +790,8 @@ mod tests { vec![MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { - command: HookCommandConfig::ByPlatform(HookCommandByPlatformConfig { - unix: "echo unix".to_string(), - windows: "echo windows".to_string(), - }), + command: "echo unix".to_string(), + command_windows: Some("echo windows".to_string()), timeout_sec: None, r#async: false, status_message: None, diff --git a/codex-rs/hooks/src/engine/mod_tests.rs b/codex-rs/hooks/src/engine/mod_tests.rs index ab8e5b71da33..cb56826900bb 100644 --- a/codex-rs/hooks/src/engine/mod_tests.rs +++ b/codex-rs/hooks/src/engine/mod_tests.rs @@ -10,8 +10,6 @@ use codex_config::ConfigRequirements; use codex_config::ConfigRequirementsToml; use codex_config::Constrained; use codex_config::ConstrainedWithSource; -use codex_config::HookCommandByPlatformConfig; -use codex_config::HookCommandConfig; use codex_config::HookEventsToml; use codex_config::HookHandlerConfig; use codex_config::ManagedHooksRequirementsToml; @@ -87,10 +85,8 @@ with Path(r"{log_path}").open("a", encoding="utf-8") as handle: pre_tool_use: vec![MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { - command: HookCommandConfig::Single(format!( - "python3 {}", - script_path.display() - )), + command: format!("python3 {}", script_path.display()), + command_windows: None, timeout_sec: Some(10), r#async: false, status_message: Some("checking".to_string()), @@ -176,7 +172,7 @@ with Path(r"{log_path}").open("a", encoding="utf-8") as handle: } #[tokio::test] -async fn requirements_managed_hooks_execute_platform_specific_command() { +async fn requirements_managed_hooks_execute_windows_command_override() { let temp = tempdir().expect("create temp dir"); let managed_dir = AbsolutePathBuf::try_from(temp.path().join("managed-hooks")).expect("absolute path"); @@ -188,10 +184,8 @@ async fn requirements_managed_hooks_execute_platform_specific_command() { pre_tool_use: vec![MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { - command: HookCommandConfig::ByPlatform(HookCommandByPlatformConfig { - unix: "exit 17".to_string(), - windows: "exit /B 19".to_string(), - }), + command: "exit 17".to_string(), + command_windows: Some("exit /B 19".to_string()), timeout_sec: Some(10), r#async: false, status_message: Some("checking".to_string()), @@ -267,7 +261,8 @@ fn unknown_requirement_source_hooks_stay_managed() { pre_tool_use: vec![MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { - command: HookCommandConfig::Single("python3 /tmp/managed.py".to_string()), + command: "python3 /tmp/managed.py".to_string(), + command_windows: None, timeout_sec: Some(10), r#async: false, status_message: Some("checking".to_string()), @@ -329,7 +324,8 @@ fn user_disablement_filters_non_managed_hooks_but_not_managed_hooks() { pre_tool_use: vec![MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { - command: HookCommandConfig::Single("python3 /tmp/managed.py".to_string()), + command: "python3 /tmp/managed.py".to_string(), + command_windows: None, timeout_sec: Some(10), r#async: false, status_message: Some("checking".to_string()), @@ -548,10 +544,8 @@ fn requirements_managed_hooks_warn_when_managed_dir_is_missing() { pre_tool_use: vec![MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { - command: HookCommandConfig::Single(format!( - "python3 {}", - missing_dir.join("pre.py").display() - )), + command: format!("python3 {}", missing_dir.join("pre.py").display()), + command_windows: None, timeout_sec: Some(10), r#async: false, status_message: Some("checking".to_string()), @@ -762,10 +756,8 @@ print(json.dumps({ pre_tool_use: vec![MatcherGroup { matcher: Some("Bash".to_string()), hooks: vec![HookHandlerConfig::Command { - command: HookCommandConfig::Single(format!( - "python3 {}", - script_path.display() - )), + command: format!("python3 {}", script_path.display()), + command_windows: None, timeout_sec: Some(10), r#async: false, status_message: None, @@ -872,10 +864,10 @@ fn plugin_hook_sources_expand_plugin_placeholders() { pre_tool_use: vec![MatcherGroup { matcher: Some("Bash".to_string()), hooks: vec![HookHandlerConfig::Command { - command: HookCommandConfig::Single( + command: "run ${PLUGIN_ROOT} ${CLAUDE_PLUGIN_ROOT} ${PLUGIN_DATA} ${CLAUDE_PLUGIN_DATA}" .to_string(), - ), + command_windows: None, timeout_sec: Some(5), r#async: false, status_message: None, diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 29eb6958efed..3f5b53266c40 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -929,9 +929,8 @@ approval_policy = "never" pre_tool_use: vec![MatcherGroup { matcher: Some("^Bash$".to_string()), hooks: vec![HookHandlerConfig::Command { - command: codex_config::HookCommandConfig::Single( - "python3 /enterprise/hooks/pre.py".to_string(), - ), + command: "python3 /enterprise/hooks/pre.py".to_string(), + command_windows: None, timeout_sec: Some(10), r#async: false, status_message: Some("checking".to_string()), From 062c6f01fc13b2544632a4a154d0ae70e886bb15 Mon Sep 17 00:00:00 2001 From: Abhinav Vedmala Date: Mon, 11 May 2026 17:55:44 -0400 Subject: [PATCH 7/7] Accept camelCase hook Windows overrides --- codex-rs/config/src/hook_config.rs | 2 +- codex-rs/config/src/hooks_tests.rs | 35 ++++++++++++++++++++++++++++++ codex-rs/core/config.schema.json | 2 +- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/codex-rs/config/src/hook_config.rs b/codex-rs/config/src/hook_config.rs index feb844215b99..869761bdaa54 100644 --- a/codex-rs/config/src/hook_config.rs +++ b/codex-rs/config/src/hook_config.rs @@ -126,7 +126,7 @@ pub enum HookHandlerConfig { #[serde(rename = "command")] Command { command: String, - #[serde(default)] + #[serde(default, rename = "commandWindows", alias = "command_windows")] command_windows: Option, #[serde(default, rename = "timeout")] timeout_sec: Option, diff --git a/codex-rs/config/src/hooks_tests.rs b/codex-rs/config/src/hooks_tests.rs index c8d1ab78d3c7..915bec7d0c89 100644 --- a/codex-rs/config/src/hooks_tests.rs +++ b/codex-rs/config/src/hooks_tests.rs @@ -203,3 +203,38 @@ command_windows = "powershell -File C:\\enterprise\\hooks\\pre.ps1" } ); } + +#[test] +fn hook_events_deserialize_camel_case_windows_override_from_toml() { + let parsed: HookEventsToml = toml::from_str( + r#" +[[PreToolUse]] +matcher = "^Bash$" + +[[PreToolUse.hooks]] +type = "command" +command = "bash /enterprise/hooks/pre.sh" +commandWindows = "powershell -File C:\\enterprise\\hooks\\pre.ps1" +"#, + ) + .expect("camelCase hook command Windows override TOML should deserialize"); + + assert_eq!( + parsed, + HookEventsToml { + pre_tool_use: vec![MatcherGroup { + matcher: Some("^Bash$".to_string()), + hooks: vec![HookHandlerConfig::Command { + command: "bash /enterprise/hooks/pre.sh".to_string(), + command_windows: Some( + r"powershell -File C:\enterprise\hooks\pre.ps1".to_string(), + ), + timeout_sec: None, + r#async: false, + status_message: None, + }], + }], + ..Default::default() + } + ); +} diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 79e47e0a1dfc..8b1d0f391ba5 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -956,7 +956,7 @@ "command": { "type": "string" }, - "command_windows": { + "commandWindows": { "default": null, "type": "string" },