From 4bef31e0098f3b6905d04fb74e97ba9974adbda0 Mon Sep 17 00:00:00 2001 From: Aaron Ring Date: Sun, 25 Jan 2026 20:09:39 -0800 Subject: [PATCH] fix(exec-approvals): match simple command name patterns in allowlist Previously, matchAllowlist() skipped all patterns without path separators (like 'pwd', 'ls', 'date'), making it impossible to allowlist commands by their simple executable name. The bug was caused by 'if (!hasPath) continue;' which short-circuited before matching against executableName. Now simple patterns match against executableName, and path-based patterns fallback to rawExecutable when resolvedPath is undefined. --- src/infra/exec-approvals.test.ts | 8 ++++---- src/infra/exec-approvals.ts | 12 +++++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/infra/exec-approvals.test.ts b/src/infra/exec-approvals.test.ts index 57abeb695..4b47a80d8 100644 --- a/src/infra/exec-approvals.test.ts +++ b/src/infra/exec-approvals.test.ts @@ -33,7 +33,7 @@ function makeTempDir() { } describe("exec approvals allowlist matching", () => { - it("ignores basename-only patterns", () => { + it("matches basename-only patterns case-insensitively", () => { const resolution = { rawExecutable: "rg", resolvedPath: "/opt/homebrew/bin/rg", @@ -41,7 +41,7 @@ describe("exec approvals allowlist matching", () => { }; const entries: ExecAllowlistEntry[] = [{ pattern: "RG" }]; const match = matchAllowlist(entries, resolution); - expect(match).toBeNull(); + expect(match?.pattern).toBe("RG"); }); it("matches by resolved path with **", () => { @@ -66,7 +66,7 @@ describe("exec approvals allowlist matching", () => { expect(match).toBeNull(); }); - it("requires a resolved path", () => { + it("falls back to rawExecutable when resolvedPath is undefined", () => { const resolution = { rawExecutable: "bin/rg", resolvedPath: undefined, @@ -74,7 +74,7 @@ describe("exec approvals allowlist matching", () => { }; const entries: ExecAllowlistEntry[] = [{ pattern: "bin/rg" }]; const match = matchAllowlist(entries, resolution); - expect(match).toBeNull(); + expect(match?.pattern).toBe("bin/rg"); }); }); diff --git a/src/infra/exec-approvals.ts b/src/infra/exec-approvals.ts index 0830ed89a..c2d06fd99 100644 --- a/src/infra/exec-approvals.ts +++ b/src/infra/exec-approvals.ts @@ -508,14 +508,20 @@ export function matchAllowlist( entries: ExecAllowlistEntry[], resolution: CommandResolution | null, ): ExecAllowlistEntry | null { - if (!entries.length || !resolution?.resolvedPath) return null; + if (!entries.length || !resolution) return null; + const rawExecutable = resolution.rawExecutable; const resolvedPath = resolution.resolvedPath; + const executableName = resolution.executableName; for (const entry of entries) { const pattern = entry.pattern?.trim(); if (!pattern) continue; const hasPath = pattern.includes("/") || pattern.includes("\\") || pattern.includes("~"); - if (!hasPath) continue; - if (matchesPattern(pattern, resolvedPath)) return entry; + if (hasPath) { + const target = resolvedPath ?? rawExecutable; + if (target && matchesPattern(pattern, target)) return entry; + continue; + } + if (executableName && matchesPattern(pattern, executableName)) return entry; } return null; }