feat: regex tool name matching for hooks (#660)

This commit is contained in:
jnjpng
2026-01-23 15:45:15 -08:00
committed by GitHub
parent ee8d3af099
commit 3e71a08156
2 changed files with 39 additions and 9 deletions

View File

@@ -147,8 +147,10 @@ export async function loadHooks(
* Check if a tool name matches a matcher pattern
* Patterns:
* - "*" or "": matches all tools
* - "ToolName": exact match
* - "Tool1|Tool2|Tool3": matches any of the listed tools
* - "ToolName": exact match (simple alphanumeric strings)
* - "Edit|Write": regex alternation, matches Edit or Write
* - "Notebook.*": regex pattern, matches Notebook, NotebookEdit, etc.
* - Any valid regex pattern is supported (case-sensitive)
*/
export function matchesTool(pattern: string, toolName: string): boolean {
// Empty or "*" matches everything
@@ -156,14 +158,14 @@ export function matchesTool(pattern: string, toolName: string): boolean {
return true;
}
// Check for pipe-separated list
if (pattern.includes("|")) {
const tools = pattern.split("|").map((t) => t.trim());
return tools.includes(toolName);
// Treat pattern as regex (anchored to match full tool name)
try {
const regex = new RegExp(`^(?:${pattern})$`);
return regex.test(toolName);
} catch {
// Invalid regex, fall back to exact match
return pattern === toolName;
}
// Exact match
return pattern === toolName;
}
/**

View File

@@ -168,6 +168,34 @@ describe("Hooks Loader", () => {
expect(matchesTool("Edit|Write", "Bash")).toBe(false);
expect(matchesTool("Edit|Write|Read", "Read")).toBe(true);
});
test("regex patterns work", () => {
// .* suffix pattern
expect(matchesTool("Notebook.*", "Notebook")).toBe(true);
expect(matchesTool("Notebook.*", "NotebookEdit")).toBe(true);
expect(matchesTool("Notebook.*", "NotebookRead")).toBe(true);
expect(matchesTool("Notebook.*", "Edit")).toBe(false);
// Prefix pattern
expect(matchesTool(".*Edit", "NotebookEdit")).toBe(true);
expect(matchesTool(".*Edit", "Edit")).toBe(true);
expect(matchesTool(".*Edit", "Write")).toBe(false);
// Character class
expect(matchesTool("Task|Bash", "Task")).toBe(true);
expect(matchesTool("Task|Bash", "Bash")).toBe(true);
// More complex patterns
expect(matchesTool("Web.*", "WebFetch")).toBe(true);
expect(matchesTool("Web.*", "WebSearch")).toBe(true);
expect(matchesTool("Web.*", "Bash")).toBe(false);
});
test("invalid regex falls back to exact match", () => {
// Unclosed bracket is invalid regex
expect(matchesTool("[invalid", "[invalid")).toBe(true);
expect(matchesTool("[invalid", "invalid")).toBe(false);
});
});
describe("getMatchingHooks", () => {