From f366ac33269e00919166de1646c8396c0a17f889 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Thu, 7 May 2026 05:28:26 +0300 Subject: [PATCH] Keep invalid hook types advisory --- codex-rs/config/src/hook_config.rs | 31 ++++++++++++++++- codex-rs/config/src/hooks_tests.rs | 56 ++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/codex-rs/config/src/hook_config.rs b/codex-rs/config/src/hook_config.rs index 27cca781c7..8ade5f2d38 100644 --- a/codex-rs/config/src/hook_config.rs +++ b/codex-rs/config/src/hook_config.rs @@ -5,7 +5,10 @@ use std::path::PathBuf; use codex_protocol::protocol::HookEventName; use schemars::JsonSchema; use serde::Deserialize; +use serde::Deserializer; use serde::Serialize; +use serde::de::Error as SerdeError; +use serde_json::Value as JsonValue; #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct HooksFile { @@ -102,10 +105,36 @@ impl HookEventsToml { pub struct MatcherGroup { #[serde(default)] pub matcher: Option, - #[serde(default)] + #[serde(default, deserialize_with = "deserialize_hook_handlers")] pub hooks: Vec, } +/// Deserialize hook handlers while dropping entries with unknown tagged variants. +/// +/// The schema warning pass reports invalid `type` values before typed config +/// deserialization. Dropping only those entries keeps startup warnings +/// non-blocking without making unrelated hook shape errors silent. +fn deserialize_hook_handlers<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let values = Vec::::deserialize(deserializer)?; + let mut handlers = Vec::new(); + + for value in values { + let invalid_type = value.get("type").is_some_and(|handler_type| { + !matches!(handler_type.as_str(), Some("command" | "prompt" | "agent")) + }); + match serde_json::from_value(value) { + Ok(handler) => handlers.push(handler), + Err(_) if invalid_type => {} + Err(err) => return Err(SerdeError::custom(err)), + } + } + + Ok(handlers) +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(tag = "type")] pub enum HookHandlerConfig { diff --git a/codex-rs/config/src/hooks_tests.rs b/codex-rs/config/src/hooks_tests.rs index 69fcd3fe95..50f23ba8e1 100644 --- a/codex-rs/config/src/hooks_tests.rs +++ b/codex-rs/config/src/hooks_tests.rs @@ -128,6 +128,62 @@ command = "python3 /tmp/pre.py" ); } +#[test] +fn hooks_toml_drops_unknown_handler_type() { + let parsed: HooksToml = toml::from_str( + r#" +[[UserPromptSubmit]] +matcher = "^UserPromptSubmit$" + +[[UserPromptSubmit.hooks]] +type = "python" +command = "python3 /tmp/ignored.py" + +[[UserPromptSubmit.hooks]] +type = 7 +command = "python3 /tmp/also-ignored.py" + +[[UserPromptSubmit.hooks]] +type = "command" +command = "python3 /tmp/kept.py" +"#, + ) + .expect("unknown hook handler type should be dropped"); + + assert_eq!( + parsed, + HooksToml { + events: HookEventsToml { + user_prompt_submit: vec![MatcherGroup { + matcher: Some("^UserPromptSubmit$".to_string()), + hooks: vec![HookHandlerConfig::Command { + command: "python3 /tmp/kept.py".to_string(), + timeout_sec: None, + r#async: false, + status_message: None, + }], + }], + ..Default::default() + }, + state: BTreeMap::new(), + } + ); +} + +#[test] +fn hooks_toml_keeps_non_enum_handler_errors_strict() { + let result = toml::from_str::( + r#" +[[UserPromptSubmit]] + +[[UserPromptSubmit.hooks]] +type = "command" +"#, + ); + + assert!(result.is_err()); +} + #[test] fn managed_hooks_requirements_flatten_hook_events() { let parsed: ManagedHooksRequirementsToml = toml::from_str(