[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
Unverified
parent 18f1a08bc9
commit 73bbb07ba8
38 changed files with 1877 additions and 55 deletions
+32
View File
@@ -1,6 +1,8 @@
use std::future::Future;
use std::sync::Arc;
use codex_hooks::PreToolUseOutcome;
use codex_hooks::PreToolUseRequest;
use codex_hooks::SessionStartOutcome;
use codex_hooks::UserPromptSubmitOutcome;
use codex_hooks::UserPromptSubmitRequest;
@@ -109,6 +111,36 @@ pub(crate) async fn run_pending_session_start_hooks(
.await
}
pub(crate) async fn run_pre_tool_use_hooks(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,
tool_use_id: String,
command: String,
) -> Option<String> {
let request = PreToolUseRequest {
session_id: sess.conversation_id,
turn_id: turn_context.sub_id.clone(),
cwd: turn_context.cwd.clone(),
transcript_path: sess.hook_transcript_path().await,
model: turn_context.model_info.slug.clone(),
permission_mode: hook_permission_mode(turn_context),
tool_name: "Bash".to_string(),
tool_use_id,
command,
};
let preview_runs = sess.hooks().preview_pre_tool_use(&request);
emit_hook_started_events(sess, turn_context, preview_runs).await;
let PreToolUseOutcome {
hook_events,
should_block,
block_reason,
} = sess.hooks().run_pre_tool_use(request).await;
emit_hook_completed_events(sess, turn_context, hook_events).await;
if should_block { block_reason } else { None }
}
pub(crate) async fn run_user_prompt_submit_hooks(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,