mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 01:51:34 -10:00
fix(hooks): treat bash, Bash and exec_shell as one tool in tool_name conditions
tool_category_for already classified the three spellings as the shell tool, but tool_name_matches_condition compared names literally, so a hook written with `exec_shell` (the documented example) never fired for `bash` calls and vice versa. Both now share is_shell_tool_name. HOOKS.md examples use `bash` with an alias note, and the configuration comment now says 15 event names, matching the events table. Checks: cargo test -p codewhale-tui --lib -- tool_name_ fleet::exact:: hooks::executor: 161 passed, 0 failed. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01TvKGZz4LrKfrQiAXip48JS
This commit is contained in:
1 parent
75cd6acb1c
commit
7e5793a29e
2 files changed
+40
-5
No files matched your search
@@ -2072,6 +2072,13 @@ impl HookExecutor {
|
||||
if tool_name == pattern {
|
||||
return true;
|
||||
}
|
||||
// The shell tool is spelled `bash` / `Bash` on the model surface, and
|
||||
// `exec_shell` is still stamped for the `shell_env` event and lives
|
||||
// on in older hook configs. Treat the three as one tool, matching
|
||||
// `tool_category_for`, so a condition written with any spelling fires.
|
||||
if is_shell_tool_name(tool_name) && is_shell_tool_name(pattern) {
|
||||
return true;
|
||||
}
|
||||
if let Some(rest) = pattern.strip_prefix("mcp_") {
|
||||
let documented = rest.strip_prefix('_');
|
||||
if documented.is_some() || pattern.contains('*') {
|
||||
@@ -2542,6 +2549,11 @@ fn is_mcp_server_tool(name: &str) -> bool {
|
||||
/// An unparseable or absent argument blob is treated as the tool's most
|
||||
/// dangerous action, because a gate that cannot see the action must not
|
||||
/// assume the harmless one.
|
||||
/// The spellings of the one shell tool (see `tool_category_for`).
|
||||
fn is_shell_tool_name(name: &str) -> bool {
|
||||
matches!(name, "bash" | "Bash" | "exec_shell")
|
||||
}
|
||||
|
||||
fn tool_category_for(tool_name: &str, tool_args: Option<&str>) -> &'static str {
|
||||
let action = tool_args
|
||||
.and_then(|raw| serde_json::from_str::<serde_json::Value>(raw).ok())
|
||||
@@ -2555,7 +2567,7 @@ fn tool_category_for(tool_name: &str, tool_args: Option<&str>) -> &'static str {
|
||||
match tool_name {
|
||||
// The shell surface. `exec_shell` is retired but kept here because
|
||||
// `shell.rs` still stamps it for the `shell_env` hook event.
|
||||
"bash" | "Bash" | "exec_shell" => "shell",
|
||||
name if is_shell_tool_name(name) => "shell",
|
||||
// The lowercase primitives ship without an action envelope.
|
||||
"read" | "todo_write" => "safe",
|
||||
"write" | "edit" => "file_write",
|
||||
@@ -4397,6 +4409,29 @@ exit 7
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_name_shell_spellings_match_each_other_in_both_directions() {
|
||||
let spellings = ["bash", "Bash", "exec_shell"];
|
||||
for tool in spellings {
|
||||
for pattern in spellings {
|
||||
assert!(
|
||||
HookExecutor::tool_name_matches_condition(tool, pattern),
|
||||
"tool {tool} should match condition {pattern}"
|
||||
);
|
||||
}
|
||||
}
|
||||
// The alias is exact: it does not widen to other shell-ish tools.
|
||||
assert!(!HookExecutor::tool_name_matches_condition(
|
||||
"task_shell_start",
|
||||
"bash"
|
||||
));
|
||||
assert!(!HookExecutor::tool_name_matches_condition(
|
||||
"bash",
|
||||
"read_file"
|
||||
));
|
||||
assert!(!HookExecutor::tool_name_matches_condition("BASH", "bash"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_name_glob_escapes_regex_metacharacters() {
|
||||
// Without escaping, `.` would match any character.
|
||||
|
||||
+4
-4
@@ -68,13 +68,13 @@ default_timeout_secs = 30 # see the timeout note below
|
||||
working_dir = "/path/to/dir" # default: the session workspace
|
||||
|
||||
[[hooks.hooks]]
|
||||
event = "tool_call_before" # required; one of the 11 names below
|
||||
event = "tool_call_before" # required; one of the 15 names below
|
||||
command = "~/.codewhale/hooks/gate.sh" # required; `sh -c` on Unix, `cmd /C` on Windows
|
||||
name = "gate" # optional label for /hooks and log lines
|
||||
timeout_secs = 30 # optional, default 30
|
||||
background = false # optional; foreground inside the hook worker
|
||||
continue_on_error = true # optional, default true
|
||||
condition = { type = "tool_name", name = "exec_shell" } # optional
|
||||
condition = { type = "tool_name", name = "bash" } # optional
|
||||
```
|
||||
|
||||
`timeout_secs` note, stated as implemented: when `[hooks].default_timeout_secs`
|
||||
@@ -173,7 +173,7 @@ configured instead (the backend owns its base environment, and your
|
||||
| Condition | Matches | Supported on |
|
||||
| --- | --- | --- |
|
||||
| `{ type = "always" }` | every invocation (also the default when omitted) | every event |
|
||||
| `{ type = "tool_name", name = "exec_shell" }` | exact tool name; `*` globs are supported, e.g. `mcp__*` | `tool_call_before`, `tool_call_after`, `shell_env`, `on_error` |
|
||||
| `{ type = "tool_name", name = "bash" }` | exact tool name; `*` globs are supported, e.g. `mcp__*`. The shell tool's spellings `bash`, `Bash`, and `exec_shell` are aliases: a condition naming any one matches all three | `tool_call_before`, `tool_call_after`, `shell_env`, `on_error` |
|
||||
| `{ type = "tool_category", category = "shell" }` | tool category | `tool_call_before`, `tool_call_after`, `shell_env`, `on_error` |
|
||||
| `{ type = "mode", mode = "plan" }` | the context's mode string, case-insensitive | every event **except** `shell_env` |
|
||||
| `{ type = "exit_code", code = 1 }` | the exit code the tool actually reported | `tool_call_after`, `on_error` |
|
||||
@@ -184,7 +184,7 @@ Three rules keep conditions from lying:
|
||||
|
||||
- **`exit_code` needs a real exit code.** It matches only when the event
|
||||
actually observed a process exit code — `tool_call_after`, or `on_error` for
|
||||
a tool failure, in both cases for a process-backed tool such as `exec_shell`.
|
||||
a tool failure, in both cases for a process-backed tool such as `bash`.
|
||||
A tool that reports no exit code never matches an `exit_code` condition; the
|
||||
condition is not satisfied by a default, a zero, or a success flag. The value
|
||||
is a 64-bit integer, so a Windows crash code such as `3221225477`
|
||||
|
||||
Reference in new issue
Block a user