From c13d9df1e0d7e5127e7beacbad855fe4aac09c31 Mon Sep 17 00:00:00 2001 From: Murat Karayel Date: Thu, 13 Aug 2026 16:03:14 +0300 Subject: [PATCH 1/3] fix: skip permission notification when request is auto-approved --- src/index.ts | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index b204efa..46748ef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -239,6 +239,16 @@ function getSessionIDFromEvent(event: unknown): string | null { return getStringField(properties, "sessionID") } +function getPermissionIDFromEvent(event: unknown): string | null { + const properties = getNestedRecord(event, "properties") + const id = getStringField(properties, "id") + if (id) { + return id + } + const request = getNestedRecord(event, "properties", "request") + return getStringField(request, "id") +} + interface SessionLifecycleInfo { id: string | null title: string | null @@ -538,7 +548,27 @@ export const NotifierPlugin: Plugin = async ({ client, directory }) => { if ((event as any).type === "permission.asked") { const sessionID = getSessionIDFromEvent(event) if (!shouldSuppressPermissionAlert(sessionID)) { - await handleEventWithElapsedTime(client, config, "permission", projectName, event) + const permissionID = getPermissionIDFromEvent(event) + if (permissionID) { + // Auto-approved requests are resolved immediately, so wait briefly + // and only notify when the request is still pending. + await new Promise((resolve) => setTimeout(resolve, 300)) + let stillPending = false + try { + const inner = (client as any)?._client || (client as any)?.session?._client + const listResponse = await inner?.get({ url: "/permission" }) + const body = listResponse?.data ?? listResponse + const pendingList = Array.isArray(body) ? body : Array.isArray(body?.data) ? body.data : [] + stillPending = pendingList.some((p: { id?: string }) => p?.id === permissionID) + } catch { + stillPending = true + } + if (stillPending) { + await handleEventWithElapsedTime(client, config, "permission", projectName, event) + } + } else { + await handleEventWithElapsedTime(client, config, "permission", projectName, event) + } } } From d310871217f2250d2ddc2f3b40ce7ca02ee9fc1e Mon Sep 17 00:00:00 2001 From: Murat Karayel Date: Tue, 8 Sep 2026 03:48:20 +0300 Subject: [PATCH 2/3] fix: make auto-approve pending check fail-open --- src/index.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index 46748ef..04f8d64 100644 --- a/src/index.ts +++ b/src/index.ts @@ -553,13 +553,17 @@ export const NotifierPlugin: Plugin = async ({ client, directory }) => { // Auto-approved requests are resolved immediately, so wait briefly // and only notify when the request is still pending. await new Promise((resolve) => setTimeout(resolve, 300)) - let stillPending = false + let stillPending = true try { const inner = (client as any)?._client || (client as any)?.session?._client - const listResponse = await inner?.get({ url: "/permission" }) - const body = listResponse?.data ?? listResponse - const pendingList = Array.isArray(body) ? body : Array.isArray(body?.data) ? body.data : [] - stillPending = pendingList.some((p: { id?: string }) => p?.id === permissionID) + if (inner && typeof inner.get === "function") { + const listResponse = await inner.get({ url: "/permission" }) + const body = listResponse?.data ?? listResponse + const pendingList = Array.isArray(body) ? body : Array.isArray(body?.data) ? body.data : null + if (pendingList) { + stillPending = pendingList.some((p: { id?: string }) => p?.id === permissionID) + } + } } catch { stillPending = true } From be46edca1fc604df1cea986c51538971d03632ea Mon Sep 17 00:00:00 2001 From: Murat Karayel Date: Sun, 13 Sep 2026 18:50:35 +0300 Subject: [PATCH 3/3] fix: claim the permission dedupe window only when a notification fires The pending check now runs before the shared 1s dedupe window is claimed, so a silently skipped auto-approved request no longer mutes a real request that arrives within the same second. Verified with a fake client: auto-approved stays silent, pending notifies, every fail-open path notifies, a real request 250ms after an auto-approved one still notifies, and hook+event for the same request delivers exactly one notification. --- src/index.ts | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/src/index.ts b/src/index.ts index 04f8d64..d8750d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -547,33 +547,32 @@ export const NotifierPlugin: Plugin = async ({ client, directory }) => { if ((event as any).type === "permission.asked") { const sessionID = getSessionIDFromEvent(event) - if (!shouldSuppressPermissionAlert(sessionID)) { - const permissionID = getPermissionIDFromEvent(event) - if (permissionID) { - // Auto-approved requests are resolved immediately, so wait briefly - // and only notify when the request is still pending. - await new Promise((resolve) => setTimeout(resolve, 300)) - let stillPending = true - try { - const inner = (client as any)?._client || (client as any)?.session?._client - if (inner && typeof inner.get === "function") { - const listResponse = await inner.get({ url: "/permission" }) - const body = listResponse?.data ?? listResponse - const pendingList = Array.isArray(body) ? body : Array.isArray(body?.data) ? body.data : null - if (pendingList) { - stillPending = pendingList.some((p: { id?: string }) => p?.id === permissionID) - } + const permissionID = getPermissionIDFromEvent(event) + let stillPending = true + if (permissionID) { + // Auto-approved requests are resolved immediately, so wait briefly + // and only notify when the request is still pending. + await new Promise((resolve) => setTimeout(resolve, 300)) + try { + const inner = (client as any)?._client || (client as any)?.session?._client + if (inner && typeof inner.get === "function") { + const listResponse = await inner.get({ url: "/permission" }) + const body = listResponse?.data ?? listResponse + const pendingList = Array.isArray(body) ? body : Array.isArray(body?.data) ? body.data : null + if (pendingList) { + stillPending = pendingList.some((p: { id?: string }) => p?.id === permissionID) } - } catch { - stillPending = true } - if (stillPending) { - await handleEventWithElapsedTime(client, config, "permission", projectName, event) - } - } else { - await handleEventWithElapsedTime(client, config, "permission", projectName, event) + } catch { + stillPending = true } } + // Claim the shared dedupe window only when a notification is actually + // about to fire: a silently skipped auto-approved request must not mute a + // real one arriving within the same second. + if (stillPending && !shouldSuppressPermissionAlert(sessionID)) { + await handleEventWithElapsedTime(client, config, "permission", projectName, event) + } } if (event.type === "session.idle") {