Codex Handbook
config/src/hook_config.rs 196 lines
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)]    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<String, HookStateToml>,}#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]pub struct HookStateToml {    #[serde(default, skip_serializing_if = "Option::is_none")]    pub enabled: Option<bool>,    #[serde(default, skip_serializing_if = "Option::is_none")]    pub trusted_hash: Option<String>,}#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]pub struct HookEventsToml {    #[serde(rename = "PreToolUse", default)]    pub pre_tool_use: Vec<MatcherGroup>,    #[serde(rename = "PermissionRequest", default)]    pub permission_request: Vec<MatcherGroup>,    #[serde(rename = "PostToolUse", default)]    pub post_tool_use: Vec<MatcherGroup>,    #[serde(rename = "PreCompact", default)]    pub pre_compact: Vec<MatcherGroup>,    #[serde(rename = "PostCompact", default)]    pub post_compact: Vec<MatcherGroup>,    #[serde(rename = "SessionStart", default)]    pub session_start: Vec<MatcherGroup>,    #[serde(rename = "UserPromptSubmit", default)]    pub user_prompt_submit: Vec<MatcherGroup>,    #[serde(rename = "SubagentStart", default)]    pub subagent_start: Vec<MatcherGroup>,    #[serde(rename = "SubagentStop", default)]    pub subagent_stop: Vec<MatcherGroup>,    #[serde(rename = "Stop", default)]    pub stop: Vec<MatcherGroup>,}impl HookEventsToml {    pub fn is_empty(&self) -> bool {        let Self {            pre_tool_use,            permission_request,            post_tool_use,            pre_compact,            post_compact,            session_start,            user_prompt_submit,            subagent_start,            subagent_stop,            stop,        } = 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()            && user_prompt_submit.is_empty()            && subagent_start.is_empty()            && subagent_stop.is_empty()            && stop.is_empty()    }    pub fn handler_count(&self) -> usize {        let Self {            pre_tool_use,            permission_request,            post_tool_use,            pre_compact,            post_compact,            session_start,            user_prompt_submit,            subagent_start,            subagent_stop,            stop,        } = self;        [            pre_tool_use,            permission_request,            post_tool_use,            pre_compact,            post_compact,            session_start,            user_prompt_submit,            subagent_start,            subagent_stop,            stop,        ]        .into_iter()        .flatten()        .map(|group| group.hooks.len())        .sum()    }    pub fn into_matcher_groups(self) -> [(HookEventName, Vec<MatcherGroup>); 10] {        [            (HookEventName::PreToolUse, self.pre_tool_use),            (HookEventName::PermissionRequest, self.permission_request),            (HookEventName::PostToolUse, self.post_tool_use),            (HookEventName::PreCompact, self.pre_compact),            (HookEventName::PostCompact, self.post_compact),            (HookEventName::SessionStart, self.session_start),            (HookEventName::UserPromptSubmit, self.user_prompt_submit),            (HookEventName::SubagentStart, self.subagent_start),            (HookEventName::SubagentStop, self.subagent_stop),            (HookEventName::Stop, self.stop),        ]    }}#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]pub struct MatcherGroup {    #[serde(default)]    pub matcher: Option<String>,    #[serde(default)]    pub hooks: Vec<HookHandlerConfig>,}#[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<String>,        #[serde(default, rename = "timeout")]        timeout_sec: Option<u64>,        #[serde(default)]        r#async: bool,        #[serde(default, rename = "statusMessage")]        status_message: Option<String>,    },    #[serde(rename = "prompt")]    Prompt {},    #[serde(rename = "agent")]    Agent {},}#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]pub struct ManagedHooksRequirementsToml {    pub managed_dir: Option<PathBuf>,    pub windows_managed_dir: Option<PathBuf>,    #[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;