use std::collections::BTreeMap; use std::path::Path; use std::path::PathBuf; use codex_protocol::protocol::HookEventName; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct HooksFile { #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(default)] pub hooks: HookEventsToml, } #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct HooksToml { #[serde(flatten)] pub events: HookEventsToml, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub state: BTreeMap, } #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct HookStateToml { #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub trusted_hash: Option, } #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct HookEventsToml { #[serde(rename = "PreToolUse", default)] pub pre_tool_use: Vec, #[serde(rename = "PermissionRequest", default)] pub permission_request: Vec, #[serde(rename = "PostToolUse", default)] pub post_tool_use: Vec, #[serde(rename = "PreCompact", default)] pub pre_compact: Vec, #[serde(rename = "PostCompact", default)] pub post_compact: Vec, #[serde(rename = "SessionStart", default)] pub session_start: Vec, #[serde(rename = "SessionEnd", default)] pub session_end: Vec, #[serde(rename = "UserPromptSubmit", default)] pub user_prompt_submit: Vec, #[serde(rename = "SubagentStart", default)] pub subagent_start: Vec, #[serde(rename = "SubagentStop", default)] pub subagent_stop: Vec, #[serde(rename = "Stop", default)] pub stop: Vec, #[serde(rename = "Interrupt", default)] pub interrupt: Vec, } impl HookEventsToml { pub fn is_empty(&self) -> bool { let Self { pre_tool_use, permission_request, post_tool_use, pre_compact, post_compact, session_start, session_end, user_prompt_submit, subagent_start, subagent_stop, stop, interrupt, } = self; pre_tool_use.is_empty() && permission_request.is_empty() && post_tool_use.is_empty() && pre_compact.is_empty() && post_compact.is_empty() && session_start.is_empty() && session_end.is_empty() && user_prompt_submit.is_empty() && subagent_start.is_empty() && subagent_stop.is_empty() && stop.is_empty() && interrupt.is_empty() } pub fn handler_count(&self) -> usize { let Self { pre_tool_use, permission_request, post_tool_use, pre_compact, post_compact, session_start, session_end, user_prompt_submit, subagent_start, subagent_stop, stop, interrupt, } = self; [ pre_tool_use, permission_request, post_tool_use, pre_compact, post_compact, session_start, session_end, user_prompt_submit, subagent_start, subagent_stop, stop, interrupt, ] .into_iter() .flatten() .map(|group| group.hooks.len()) .sum() } pub fn into_matcher_groups(mut self) -> [(HookEventName, Vec); 12] { self.matcher_groups_mut() .map(|(event, groups)| (event, std::mem::take(groups))) } pub fn matcher_groups_mut(&mut self) -> [(HookEventName, &mut Vec); 12] { use HookEventName as Event; [ (Event::PreToolUse, &mut self.pre_tool_use), (Event::PermissionRequest, &mut self.permission_request), (Event::PostToolUse, &mut self.post_tool_use), (Event::PreCompact, &mut self.pre_compact), (Event::PostCompact, &mut self.post_compact), (Event::SessionStart, &mut self.session_start), (Event::SessionEnd, &mut self.session_end), (Event::UserPromptSubmit, &mut self.user_prompt_submit), (Event::SubagentStart, &mut self.subagent_start), (Event::SubagentStop, &mut self.subagent_stop), (Event::Stop, &mut self.stop), (Event::Interrupt, &mut self.interrupt), ] } } #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct MatcherGroup { #[serde(default)] pub matcher: Option, #[serde(default)] pub hooks: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(tag = "type")] pub enum HookHandlerConfig { #[serde(rename = "command")] Command { command: String, #[serde(default, rename = "commandWindows", alias = "command_windows")] command_windows: Option, #[serde(default, rename = "timeout")] timeout_sec: Option, #[serde(default)] r#async: bool, #[serde(default, rename = "statusMessage")] status_message: Option, /// Approximate token threshold for spilling this hook's `additionalContext` to disk. /// Unset uses 2,500 tokens; `0` disables spilling for this hook. The threshold is /// evaluated against the original context; a spilled preview also includes recovery /// metadata. #[serde( default, rename = "additionalContextLimit", skip_serializing_if = "Option::is_none" )] additional_context_limit: Option, }, #[serde(rename = "mcp_tool")] McpTool { server: String, tool: String, #[serde(default, deserialize_with = "deserialize_mcp_tool_input")] input: serde_json::Map, #[serde(default, rename = "timeout")] timeout_sec: Option, #[serde(default, rename = "statusMessage")] status_message: Option, }, #[serde(rename = "prompt")] Prompt {}, #[serde(rename = "agent")] Agent {}, } // Reject values such as null that cannot be represented in TOML for trust hashing. fn deserialize_mcp_tool_input<'de, D>( deserializer: D, ) -> Result, D::Error> where D: serde::Deserializer<'de>, { let input = serde_json::Map::deserialize(deserializer)?; toml::Value::try_from(&input).map_err(|error| { serde::de::Error::custom(format!( "MCP hook input must be representable as TOML: {error}" )) })?; Ok(input) } #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ManagedHooksRequirementsToml { pub managed_dir: Option, pub windows_managed_dir: Option, #[serde(flatten)] pub hooks: HookEventsToml, } impl ManagedHooksRequirementsToml { pub fn is_empty(&self) -> bool { let Self { managed_dir, windows_managed_dir, hooks, } = self; managed_dir.is_none() && windows_managed_dir.is_none() && hooks.is_empty() } pub fn handler_count(&self) -> usize { self.hooks.handler_count() } pub fn managed_dir_for_current_platform(&self) -> Option<&Path> { #[cfg(windows)] { self.windows_managed_dir.as_deref() } #[cfg(not(windows))] { self.managed_dir.as_deref() } } } #[cfg(test)] #[path = "hooks_tests.rs"] mod tests;