Files
codex/codex-rs/hooks/src/engine/mod.rs
Andrei Eternal c4d9887f9a [hooks] add non-streaming (non-stdin style) shell-only PostToolUse support (#15531)
CHAINED PR - note that base is eternal/hooks-pretooluse-bash, not main
-- so the following PR should be first

Matching post-tool hook to the pre-tool functionality here:
https://github.com/openai/codex/pull/15211

So, PreToolUse calls for plain shell calls, allows blocking. This
PostToolUse call runs after the command executed

example run:

```
› as a test, run in parallel the following commands:
  - echo 'one'
  - echo '[block-pre-tool-use]'
  - echo '[block-post-tool-use]'


⚠ MCP startup incomplete (failed: notion, linear)

• Cruising through those three commands in parallel now, and I’ll share the exact outputs right after
  they land.

• Running PreToolUse hook: checking the observatory runes

• Running PreToolUse hook: checking the observatory runes

• Running PreToolUse hook: checking the observatory runes

PreToolUse hook (blocked)
  warning: wizard-tower PreToolUse demo blocked a Bash command on purpose.
  feedback: PreToolUse demo blocked the command. Remove [block-pre-tool-use] to continue.

PreToolUse hook (completed)
  warning: wizard-tower PreToolUse demo inspected Bash: echo 'one'

PreToolUse hook (completed)
  warning: wizard-tower PreToolUse demo inspected Bash: echo '[block-post-tool-use]'

• Ran echo '[block-post-tool-use]'
  └ [block-post-tool-use]

• Ran echo 'one'
  └ one

• Running PostToolUse hook: reading the observatory aftermath

• Running PostToolUse hook: reading the observatory aftermath

PostToolUse hook (blocked)
  warning: wizard-tower PostToolUse demo blocked the Bash follow-through on purpose.
  feedback: PostToolUse demo blocked the result after execution. Remove [block-post-tool-use] to
continue.

PostToolUse hook (completed)
  warning: wizard-tower PostToolUse demo inspected Bash: echo 'one'
  hook context: PostToolUse demo saw the Bash result. Response preview: one

──────────────────────────────────────────────────────────────────────────────────────────────────────

• Ran them in parallel. Little wave report:

  - echo 'one' completed and returned one.
  - echo '[block-pre-tool-use]' was blocked before execution by the PreToolUse hook.
  - echo '[block-post-tool-use]' ran, but its result was blocked afterward by the PostToolUse hook.

  The hook messages were:

  - PreToolUse demo blocked the command. Remove [block-pre-tool-use] to continue.
  - PostToolUse demo blocked the result after execution. Remove [block-post-tool-use] to continue.


› for the blocked messages (correctly blocked in this test) did the tool output land back into
  context?


• Yeah, from this run:

  - PreToolUse block: the command itself did not execute, so no command output landed in context. Only
    the block message did.
  - PostToolUse block: the command did execute, but the actual stdout did not come back into context.
    Only the post-tool block message did.

  So the blocked notifications landed in context, but the blocked command payloads themselves did not.
  Nice clean guardrail, aloha.
```
2026-03-25 19:18:03 -07:00

166 lines
5.1 KiB
Rust

pub(crate) mod command_runner;
pub(crate) mod config;
pub(crate) mod discovery;
pub(crate) mod dispatcher;
pub(crate) mod output_parser;
pub(crate) mod schema_loader;
use std::path::PathBuf;
use codex_config::ConfigLayerStack;
use codex_protocol::protocol::HookRunSummary;
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 matcher: Option<String>,
pub command: String,
pub timeout_sec: u64,
pub status_message: Option<String>,
pub source_path: PathBuf,
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::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,
};
}
if cfg!(windows) {
return Self {
handlers: Vec::new(),
warnings: vec![
"Disabled `codex_hooks` for this session because `hooks.json` lifecycle hooks are not supported on Windows yet."
.to_string(),
],
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_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_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
}
}