Files
codex/codex-rs/hooks/src/engine/mod.rs
T
Andrei EternalandMichael Bolin 2b2de3f38b codex: support hooks in config.toml and requirements.toml (#18893)
## Summary

Support the existing hooks schema in inline TOML so hooks can be
configured from both `config.toml` and enterprise-managed
`requirements.toml` without requiring a separate `hooks.json` payload.

This gives enterprise admins a way to ship managed hook policy through
the existing requirements channel while still leaving script delivery to
MDM or other device-management tooling, and it keeps `hooks.json`
working unchanged for existing users.

This also lays the groundwork for follow-on managed filtering work such
as #15937, while continuing to respect project trust gating from #14718.
It does **not** implement `allow_managed_hooks_only` itself.

NOTE: yes, it's a bit unfortunate that the toml isn't formatted as
closely as normal to our default styling. This is because we're trying
to stay compatible with the spec for plugins/hooks that we'll need to
support & the main usecase here is embedding into requirements.toml

## What changed

- moved the shared hook serde model out of `codex-rs/hooks` into
`codex-rs/config` so the same schema can power `hooks.json`, inline
`config.toml` hooks, and managed `requirements.toml` hooks
- added `hooks` support to both `ConfigToml` and
`ConfigRequirementsToml`, including requirements-side `managed_dir` /
`windows_managed_dir`
- treated requirements-managed hooks as one constrained value via
`Constrained`, so managed hook policy is merged atomically and cannot
drift across requirement sources
- updated hook discovery to load requirements-managed hooks first, then
per-layer `hooks.json`, then per-layer inline TOML hooks, with a warning
when a single layer defines both representations
- threaded managed hook metadata through discovered handlers and exposed
requirements hooks in app-server responses, generated schemas, and
`/debug-config`
- added hook/config coverage in `codex-rs/config`, `codex-rs/hooks`,
`codex-rs/core/src/config_loader/tests.rs`, and
`codex-rs/core/tests/suite/hooks.rs`

## Testing

- `cargo test -p codex-config`
- `cargo test -p codex-hooks`
- `cargo test -p codex-app-server config_api`

## Documentation

Companion updates are needed in the developers website repo for:

- the hooks guide
- the config reference, sample, basic, and advanced pages
- the enterprise managed configuration guide

---------

Co-authored-by: Michael Bolin <mbolin@openai.com>
2026-04-22 21:20:09 -07:00

177 lines
5.6 KiB
Rust

pub(crate) mod command_runner;
pub(crate) mod discovery;
pub(crate) mod dispatcher;
pub(crate) mod output_parser;
pub(crate) mod schema_loader;
use codex_config::ConfigLayerStack;
use codex_protocol::protocol::HookRunSummary;
use codex_protocol::protocol::HookSource;
use codex_utils_absolute_path::AbsolutePathBuf;
use crate::events::permission_request::PermissionRequestOutcome;
use crate::events::permission_request::PermissionRequestRequest;
use crate::events::post_tool_use::PostToolUseOutcome;
use crate::events::post_tool_use::PostToolUseRequest;
use crate::events::pre_tool_use::PreToolUseOutcome;
use crate::events::pre_tool_use::PreToolUseRequest;
use crate::events::session_start::SessionStartOutcome;
use crate::events::session_start::SessionStartRequest;
use crate::events::stop::StopOutcome;
use crate::events::stop::StopRequest;
use crate::events::user_prompt_submit::UserPromptSubmitOutcome;
use crate::events::user_prompt_submit::UserPromptSubmitRequest;
#[derive(Debug, Clone)]
pub(crate) struct CommandShell {
pub program: String,
pub args: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ConfiguredHandler {
pub event_name: codex_protocol::protocol::HookEventName,
pub is_managed: bool,
pub matcher: Option<String>,
pub command: String,
pub timeout_sec: u64,
pub status_message: Option<String>,
pub source_path: AbsolutePathBuf,
pub source: HookSource,
pub display_order: i64,
}
impl ConfiguredHandler {
pub fn run_id(&self) -> String {
format!(
"{}:{}:{}",
self.event_name_label(),
self.display_order,
self.source_path.display()
)
}
fn event_name_label(&self) -> &'static str {
match self.event_name {
codex_protocol::protocol::HookEventName::PreToolUse => "pre-tool-use",
codex_protocol::protocol::HookEventName::PermissionRequest => "permission-request",
codex_protocol::protocol::HookEventName::PostToolUse => "post-tool-use",
codex_protocol::protocol::HookEventName::SessionStart => "session-start",
codex_protocol::protocol::HookEventName::UserPromptSubmit => "user-prompt-submit",
codex_protocol::protocol::HookEventName::Stop => "stop",
}
}
}
#[derive(Clone)]
pub(crate) struct ClaudeHooksEngine {
handlers: Vec<ConfiguredHandler>,
warnings: Vec<String>,
shell: CommandShell,
}
impl ClaudeHooksEngine {
pub(crate) fn new(
enabled: bool,
config_layer_stack: Option<&ConfigLayerStack>,
shell: CommandShell,
) -> Self {
if !enabled {
return Self {
handlers: Vec::new(),
warnings: Vec::new(),
shell,
};
}
let _ = schema_loader::generated_hook_schemas();
let discovered = discovery::discover_handlers(config_layer_stack);
Self {
handlers: discovered.handlers,
warnings: discovered.warnings,
shell,
}
}
pub(crate) fn warnings(&self) -> &[String] {
&self.warnings
}
pub(crate) fn preview_session_start(
&self,
request: &SessionStartRequest,
) -> Vec<HookRunSummary> {
crate::events::session_start::preview(&self.handlers, request)
}
pub(crate) fn preview_pre_tool_use(&self, request: &PreToolUseRequest) -> Vec<HookRunSummary> {
crate::events::pre_tool_use::preview(&self.handlers, request)
}
pub(crate) fn preview_permission_request(
&self,
request: &PermissionRequestRequest,
) -> Vec<HookRunSummary> {
crate::events::permission_request::preview(&self.handlers, request)
}
pub(crate) fn preview_post_tool_use(
&self,
request: &PostToolUseRequest,
) -> Vec<HookRunSummary> {
crate::events::post_tool_use::preview(&self.handlers, request)
}
pub(crate) async fn run_session_start(
&self,
request: SessionStartRequest,
turn_id: Option<String>,
) -> SessionStartOutcome {
crate::events::session_start::run(&self.handlers, &self.shell, request, turn_id).await
}
pub(crate) async fn run_pre_tool_use(&self, request: PreToolUseRequest) -> PreToolUseOutcome {
crate::events::pre_tool_use::run(&self.handlers, &self.shell, request).await
}
pub(crate) async fn run_permission_request(
&self,
request: PermissionRequestRequest,
) -> PermissionRequestOutcome {
crate::events::permission_request::run(&self.handlers, &self.shell, request).await
}
pub(crate) async fn run_post_tool_use(
&self,
request: PostToolUseRequest,
) -> PostToolUseOutcome {
crate::events::post_tool_use::run(&self.handlers, &self.shell, request).await
}
pub(crate) fn preview_user_prompt_submit(
&self,
request: &UserPromptSubmitRequest,
) -> Vec<HookRunSummary> {
crate::events::user_prompt_submit::preview(&self.handlers, request)
}
pub(crate) async fn run_user_prompt_submit(
&self,
request: UserPromptSubmitRequest,
) -> UserPromptSubmitOutcome {
crate::events::user_prompt_submit::run(&self.handlers, &self.shell, request).await
}
pub(crate) fn preview_stop(&self, request: &StopRequest) -> Vec<HookRunSummary> {
crate::events::stop::preview(&self.handlers, request)
}
pub(crate) async fn run_stop(&self, request: StopRequest) -> StopOutcome {
crate::events::stop::run(&self.handlers, &self.shell, request).await
}
}
#[cfg(test)]
#[path = "mod_tests.rs"]
mod tests;