From a7c91cededc6598c3c21e257b662ee30bb71bd01 Mon Sep 17 00:00:00 2001 From: Lukas Kosina Date: Wed, 4 Mar 2026 09:46:41 +0100 Subject: [PATCH 1/2] Fix too many notifications for Bash commands (#37) Remove Bash from INTERACTIVE_TOOLS so PreToolUse events for Bash commands set status to "running" instead of "waiting", preventing spurious "Waiting for input" notifications for auto-approved commands. Co-Authored-By: Claude Opus 4.6 --- src/state.test.ts | 4 ++-- src/state.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/state.test.ts b/src/state.test.ts index 507309d..7f9bcf3 100644 --- a/src/state.test.ts +++ b/src/state.test.ts @@ -164,7 +164,7 @@ describe("createStore", () => { assert.equal(session?.lastEvent, "Read"); }); - it("PreToolUse with Bash transitions to waiting", () => { + it("PreToolUse with Bash stays running", () => { const store = createStore(); store.handleEvent({ session_id: "s1", hook_event_name: "SessionStart" }); store.handleEvent({ @@ -176,7 +176,7 @@ describe("createStore", () => { hook_event_name: "PreToolUse", tool_name: "Bash", }); - assert.equal(session?.status, "waiting"); + assert.equal(session?.status, "running"); assert.equal(session?.lastEvent, "Bash"); }); diff --git a/src/state.ts b/src/state.ts index 379ca56..48e8a3a 100644 --- a/src/state.ts +++ b/src/state.ts @@ -33,7 +33,6 @@ const EVENT_TO_STATUS: Record = { const INTERACTIVE_TOOLS = new Set([ "ExitPlanMode", "AskUserQuestion", - "Bash", "Write", "Edit", "NotebookEdit", From 6600391b672661191095a1c6676815aa540a33d9 Mon Sep 17 00:00:00 2001 From: Lukas Kosina Date: Wed, 4 Mar 2026 12:37:40 +0100 Subject: [PATCH 2/2] Add PermissionRequest and PostToolUse hooks for accurate Bash approval status --- src/hooks.test.ts | 4 +- src/hooks.ts | 9 +++- src/state.test.ts | 104 ++++++++++++++++++++++++++++++++++++++++++++++ src/state.ts | 8 ++++ 4 files changed, 123 insertions(+), 2 deletions(-) diff --git a/src/hooks.test.ts b/src/hooks.test.ts index 73cb869..f7e5314 100644 --- a/src/hooks.test.ts +++ b/src/hooks.test.ts @@ -37,7 +37,7 @@ describe("installHooks", () => { assert.ok(settings.hooks); }); - it("creates all 5 hook events including PreToolUse", () => { + it("creates all 7 hook events including PreToolUse, PermissionRequest, and PostToolUse", () => { installHooks(8377, tmpDir); const settings = readSettings() as { hooks: Record }; assert.ok(settings.hooks.SessionStart); @@ -45,6 +45,8 @@ describe("installHooks", () => { assert.ok(settings.hooks.Stop); assert.ok(settings.hooks.SessionEnd); assert.ok(settings.hooks.PreToolUse); + assert.ok(settings.hooks.PermissionRequest); + assert.ok(settings.hooks.PostToolUse); }); it("PreToolUse hook captures all tools without matcher", () => { diff --git a/src/hooks.ts b/src/hooks.ts index ba793a5..1fc9855 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -6,7 +6,14 @@ const MARKER_QUICK = "__claude_code_dashboard_quick__"; const MARKER_INSTALL = "__claude_code_dashboard_install__"; const MARKER_LEGACY = "__claude_code_dashboard__"; -const HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "Stop", "SessionEnd"] as const; +const HOOK_EVENTS = [ + "SessionStart", + "UserPromptSubmit", + "Stop", + "SessionEnd", + "PermissionRequest", + "PostToolUse", +] as const; interface HookEntry { type: string; diff --git a/src/state.test.ts b/src/state.test.ts index 7f9bcf3..357012f 100644 --- a/src/state.test.ts +++ b/src/state.test.ts @@ -260,6 +260,110 @@ describe("createStore", () => { assert.equal(session?.status, "waiting"); }); + it("PermissionRequest transitions to waiting with tool name", () => { + const store = createStore(); + store.handleEvent({ session_id: "s1", hook_event_name: "SessionStart" }); + store.handleEvent({ session_id: "s1", hook_event_name: "UserPromptSubmit" }); + store.handleEvent({ session_id: "s1", hook_event_name: "PreToolUse", tool_name: "Bash" }); + assert.equal(store.getSession("s1")?.status, "running"); + const session = store.handleEvent({ + session_id: "s1", + hook_event_name: "PermissionRequest", + tool_name: "Bash", + }); + assert.equal(session?.status, "waiting"); + assert.equal(session?.lastEvent, "Bash"); + }); + + it("PostToolUse transitions to running with tool name", () => { + const store = createStore(); + store.handleEvent({ session_id: "s1", hook_event_name: "SessionStart" }); + store.handleEvent({ session_id: "s1", hook_event_name: "UserPromptSubmit" }); + store.handleEvent({ session_id: "s1", hook_event_name: "PreToolUse", tool_name: "Bash" }); + store.handleEvent({ + session_id: "s1", + hook_event_name: "PermissionRequest", + tool_name: "Bash", + }); + assert.equal(store.getSession("s1")?.status, "waiting"); + const session = store.handleEvent({ + session_id: "s1", + hook_event_name: "PostToolUse", + tool_name: "Bash", + }); + assert.equal(session?.status, "running"); + assert.equal(session?.lastEvent, "Bash"); + }); + + it("Full approval flow: PreToolUse → PermissionRequest → PostToolUse", () => { + const store = createStore(); + store.handleEvent({ session_id: "s1", hook_event_name: "SessionStart" }); + store.handleEvent({ session_id: "s1", hook_event_name: "UserPromptSubmit" }); + + const afterPre = store.handleEvent({ + session_id: "s1", + hook_event_name: "PreToolUse", + tool_name: "Bash", + }); + assert.equal(afterPre?.status, "running"); + + const afterPerm = store.handleEvent({ + session_id: "s1", + hook_event_name: "PermissionRequest", + tool_name: "Bash", + }); + assert.equal(afterPerm?.status, "waiting"); + + const afterPost = store.handleEvent({ + session_id: "s1", + hook_event_name: "PostToolUse", + tool_name: "Bash", + }); + assert.equal(afterPost?.status, "running"); + }); + + it("Auto-approved flow: PreToolUse → PostToolUse (no PermissionRequest)", () => { + const store = createStore(); + store.handleEvent({ session_id: "s1", hook_event_name: "SessionStart" }); + store.handleEvent({ session_id: "s1", hook_event_name: "UserPromptSubmit" }); + + const afterPre = store.handleEvent({ + session_id: "s1", + hook_event_name: "PreToolUse", + tool_name: "Bash", + }); + assert.equal(afterPre?.status, "running"); + + const afterPost = store.handleEvent({ + session_id: "s1", + hook_event_name: "PostToolUse", + tool_name: "Bash", + }); + assert.equal(afterPost?.status, "running"); + }); + + it("PermissionRequest without tool_name falls back to event name", () => { + const store = createStore(); + store.handleEvent({ session_id: "s1", hook_event_name: "SessionStart" }); + const session = store.handleEvent({ + session_id: "s1", + hook_event_name: "PermissionRequest", + }); + assert.equal(session?.status, "waiting"); + assert.equal(session?.lastEvent, "PermissionRequest"); + }); + + it("PostToolUse without tool_name falls back to event name", () => { + const store = createStore(); + store.handleEvent({ session_id: "s1", hook_event_name: "SessionStart" }); + const session = store.handleEvent({ + session_id: "s1", + hook_event_name: "PostToolUse", + }); + assert.equal(session?.status, "running"); + assert.equal(session?.lastEvent, "PostToolUse"); + }); + it("removeSession deletes an existing session", () => { const store = createStore(); store.handleEvent({ session_id: "s1", hook_event_name: "SessionStart" }); diff --git a/src/state.ts b/src/state.ts index 48e8a3a..53dddb1 100644 --- a/src/state.ts +++ b/src/state.ts @@ -61,6 +61,14 @@ export function createStore(): Store { const toolName = typeof payload.tool_name === "string" ? payload.tool_name : ""; displayEvent = toolName || hook_event_name; status = INTERACTIVE_TOOLS.has(toolName) ? "waiting" : "running"; + } else if (hook_event_name === "PermissionRequest") { + const toolName = typeof payload.tool_name === "string" ? payload.tool_name : ""; + displayEvent = toolName || hook_event_name; + status = "waiting"; + } else if (hook_event_name === "PostToolUse") { + const toolName = typeof payload.tool_name === "string" ? payload.tool_name : ""; + displayEvent = toolName || hook_event_name; + status = "running"; } else { const mapped = EVENT_TO_STATUS[hook_event_name]; if (!mapped) {