[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
@@ -4,6 +4,8 @@ use serde_json::Value;
#[allow(dead_code)]
pub(crate) struct GeneratedHookSchemas {
pub pre_tool_use_command_input: Value,
pub pre_tool_use_command_output: Value,
pub session_start_command_input: Value,
pub session_start_command_output: Value,
pub user_prompt_submit_command_input: Value,
@@ -15,6 +17,14 @@ pub(crate) struct GeneratedHookSchemas {
pub(crate) fn generated_hook_schemas() -> &'static GeneratedHookSchemas {
static SCHEMAS: OnceLock<GeneratedHookSchemas> = OnceLock::new();
SCHEMAS.get_or_init(|| GeneratedHookSchemas {
pre_tool_use_command_input: parse_json_schema(
"pre-tool-use.command.input",
include_str!("../../schema/generated/pre-tool-use.command.input.schema.json"),
),
pre_tool_use_command_output: parse_json_schema(
"pre-tool-use.command.output",
include_str!("../../schema/generated/pre-tool-use.command.output.schema.json"),
),
session_start_command_input: parse_json_schema(
"session-start.command.input",
include_str!("../../schema/generated/session-start.command.input.schema.json"),
@@ -56,6 +66,8 @@ mod tests {
fn loads_generated_hook_schemas() {
let schemas = generated_hook_schemas();
assert_eq!(schemas.pre_tool_use_command_input["type"], "object");
assert_eq!(schemas.pre_tool_use_command_output["type"], "object");
assert_eq!(schemas.session_start_command_input["type"], "object");
assert_eq!(schemas.session_start_command_output["type"], "object");
assert_eq!(schemas.user_prompt_submit_command_input["type"], "object");