[hooks] add non-streaming (non-stdin style) shell-only PreToolUse support (#15211)

- add `PreToolUse` hook for bash-like tool execution only at first
- block shell execution before dispatch with deny-only hook behavior
- introduces common.rs matcher framework for matching when hooks are run

example run:

```
› run three parallel echo commands, and the second one should echo "[block-pre-tool-use]" as a test


• Running the three echo commands in parallel now and I’ll report the output directly.

• Running PreToolUse hook: name for demo pre tool use hook

• Running PreToolUse hook: name for demo pre tool use hook

• Running PreToolUse hook: name for demo pre tool use hook

PreToolUse hook (completed)
  warning: wizard-tower PreToolUse demo inspected Bash: echo "first parallel echo"
  
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 "third parallel echo"

• Ran echo "first parallel echo"
  └ first parallel echo

• Ran echo "third parallel echo"
  └ third parallel echo

• Three little waves went out in parallel.

  1. printed first parallel echo
  2. was blocked before execution because it contained the exact test string [block-pre-tool-use]
  3. printed third parallel echo

  There was also an unrelated macOS defaults warning around the successful commands, but the echoes
  themselves worked fine. If you want, I can rerun the second one with a slightly modified string so
  it passes cleanly.
```
This commit is contained in:
Andrei Eternal
2026-03-23 14:32:59 -07:00
committed by GitHub
parent 18f1a08bc9
commit 73bbb07ba8
38 changed files with 1877 additions and 55 deletions

View File

@@ -3,11 +3,12 @@ use std::path::Path;
use codex_config::ConfigLayerStack;
use codex_config::ConfigLayerStackOrdering;
use regex::Regex;
use super::ConfiguredHandler;
use super::config::HookHandlerConfig;
use super::config::HooksFile;
use crate::events::common::matcher_pattern_for_event;
use crate::events::common::validate_matcher_pattern;
pub(crate) struct DiscoveryResult {
pub handlers: Vec<ConfiguredHandler>,
@@ -69,6 +70,21 @@ pub(crate) fn discover_handlers(config_layer_stack: Option<&ConfigLayerStack>) -
}
};
for group in parsed.hooks.pre_tool_use {
append_group_handlers(
&mut handlers,
&mut warnings,
&mut display_order,
source_path.as_path(),
codex_protocol::protocol::HookEventName::PreToolUse,
matcher_pattern_for_event(
codex_protocol::protocol::HookEventName::PreToolUse,
group.matcher.as_deref(),
),
group.hooks,
);
}
for group in parsed.hooks.session_start {
append_group_handlers(
&mut handlers,
@@ -76,7 +92,7 @@ pub(crate) fn discover_handlers(config_layer_stack: Option<&ConfigLayerStack>) -
&mut display_order,
source_path.as_path(),
codex_protocol::protocol::HookEventName::SessionStart,
effective_matcher(
matcher_pattern_for_event(
codex_protocol::protocol::HookEventName::SessionStart,
group.matcher.as_deref(),
),
@@ -91,7 +107,7 @@ pub(crate) fn discover_handlers(config_layer_stack: Option<&ConfigLayerStack>) -
&mut display_order,
source_path.as_path(),
codex_protocol::protocol::HookEventName::UserPromptSubmit,
effective_matcher(
matcher_pattern_for_event(
codex_protocol::protocol::HookEventName::UserPromptSubmit,
group.matcher.as_deref(),
),
@@ -106,7 +122,7 @@ pub(crate) fn discover_handlers(config_layer_stack: Option<&ConfigLayerStack>) -
&mut display_order,
source_path.as_path(),
codex_protocol::protocol::HookEventName::Stop,
effective_matcher(
matcher_pattern_for_event(
codex_protocol::protocol::HookEventName::Stop,
group.matcher.as_deref(),
),
@@ -118,17 +134,6 @@ pub(crate) fn discover_handlers(config_layer_stack: Option<&ConfigLayerStack>) -
DiscoveryResult { handlers, warnings }
}
fn effective_matcher(
event_name: codex_protocol::protocol::HookEventName,
matcher: Option<&str>,
) -> Option<&str> {
match event_name {
codex_protocol::protocol::HookEventName::SessionStart => matcher,
codex_protocol::protocol::HookEventName::UserPromptSubmit
| codex_protocol::protocol::HookEventName::Stop => None,
}
}
fn append_group_handlers(
handlers: &mut Vec<ConfiguredHandler>,
warnings: &mut Vec<String>,
@@ -139,7 +144,7 @@ fn append_group_handlers(
group_handlers: Vec<HookHandlerConfig>,
) {
if let Some(matcher) = matcher
&& let Err(err) = Regex::new(matcher)
&& let Err(err) = validate_matcher_pattern(matcher)
{
warnings.push(format!(
"invalid matcher {matcher:?} in {}: {err}",
@@ -205,7 +210,7 @@ mod tests {
use super::ConfiguredHandler;
use super::HookHandlerConfig;
use super::append_group_handlers;
use super::effective_matcher;
use crate::events::common::matcher_pattern_for_event;
#[test]
fn user_prompt_submit_ignores_invalid_matcher_during_discovery() {
@@ -219,7 +224,7 @@ mod tests {
&mut display_order,
Path::new("/tmp/hooks.json"),
HookEventName::UserPromptSubmit,
effective_matcher(HookEventName::UserPromptSubmit, Some("[")),
matcher_pattern_for_event(HookEventName::UserPromptSubmit, Some("[")),
vec![HookHandlerConfig::Command {
command: "echo hello".to_string(),
timeout_sec: None,
@@ -242,4 +247,66 @@ mod tests {
}]
);
}
#[test]
fn pre_tool_use_keeps_valid_matcher_during_discovery() {
let mut handlers = Vec::new();
let mut warnings = Vec::new();
let mut display_order = 0;
append_group_handlers(
&mut handlers,
&mut warnings,
&mut display_order,
Path::new("/tmp/hooks.json"),
HookEventName::PreToolUse,
matcher_pattern_for_event(HookEventName::PreToolUse, Some("^Bash$")),
vec![HookHandlerConfig::Command {
command: "echo hello".to_string(),
timeout_sec: None,
r#async: false,
status_message: None,
}],
);
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(
handlers,
vec![ConfiguredHandler {
event_name: HookEventName::PreToolUse,
matcher: Some("^Bash$".to_string()),
command: "echo hello".to_string(),
timeout_sec: 600,
status_message: None,
source_path: PathBuf::from("/tmp/hooks.json"),
display_order: 0,
}]
);
}
#[test]
fn pre_tool_use_treats_star_matcher_as_match_all() {
let mut handlers = Vec::new();
let mut warnings = Vec::new();
let mut display_order = 0;
append_group_handlers(
&mut handlers,
&mut warnings,
&mut display_order,
Path::new("/tmp/hooks.json"),
HookEventName::PreToolUse,
matcher_pattern_for_event(HookEventName::PreToolUse, Some("*")),
vec![HookHandlerConfig::Command {
command: "echo hello".to_string(),
timeout_sec: None,
r#async: false,
status_message: None,
}],
);
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(handlers.len(), 1);
assert_eq!(handlers[0].matcher.as_deref(), Some("*"));
}
}