From 2bc55f2aa9f6f9b1f2e9ae0983f20402110388e3 Mon Sep 17 00:00:00 2001 From: Georgiy Tarasov Date: Thu, 16 Jul 2026 14:55:14 +0000 Subject: [PATCH] fix(agent): prompt for destructive PostHog exec sub-tools in auto mode Auto mode only promises hands-off file edits and shell commands, but the PostHog exec destructive gate short-circuited to silent allow in auto mode, so update/delete/destroy sub-tools were the only exec calls that never produced a permission_request. Non-destructive exec calls already prompt in auto mode (and web clients auto-approve them client-side), which made the gate exactly backwards on the cloud surface: safe calls asked, destructive ones ran silently. Keep the bypass for bypassPermissions only; persisted allow_always sub-tool approvals still skip the prompt. Co-Authored-By: Claude Opus 4.8 --- .../permissions/permission-handlers.test.ts | 17 ++++++++++++++--- .../claude/permissions/permission-handlers.ts | 8 ++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/agent/src/adapters/claude/permissions/permission-handlers.test.ts b/packages/agent/src/adapters/claude/permissions/permission-handlers.test.ts index 60a200126c..221fafc866 100644 --- a/packages/agent/src/adapters/claude/permissions/permission-handlers.test.ts +++ b/packages/agent/src/adapters/claude/permissions/permission-handlers.test.ts @@ -200,7 +200,7 @@ describe("canUseTool MCP approval enforcement", () => { expect(result.behavior).toBe("allow"); }); - it("bypasses the PostHog exec gate in auto mode", async () => { + it("prompts through the PostHog exec gate in auto mode", async () => { setMcpToolApprovalStates({ mcp__posthog__exec: "approved" }); const hasApproval = vi.fn().mockReturnValue(false); const addApproval = vi.fn().mockResolvedValue(undefined); @@ -215,12 +215,23 @@ describe("canUseTool MCP approval enforcement", () => { addPostHogExecApproval: addApproval, }, }, + client: createClient({ + outcome: { outcome: "selected", optionId: "allow" }, + }), }); const result = await canUseTool(context); + // Auto mode only auto-approves file edits and shell commands; destructive + // PostHog sub-tools must still reach the client as a permission prompt. expect(result.behavior).toBe("allow"); - expect(context.client.requestPermission).not.toHaveBeenCalled(); - expect(hasApproval).not.toHaveBeenCalled(); + expect(hasApproval).toHaveBeenCalledWith("experiment-update"); + expect(context.client.requestPermission).toHaveBeenCalledWith( + expect.objectContaining({ + toolCall: expect.objectContaining({ + title: "The agent wants to run `experiment-update` on PostHog", + }), + }), + ); expect(addApproval).not.toHaveBeenCalled(); }); diff --git a/packages/agent/src/adapters/claude/permissions/permission-handlers.ts b/packages/agent/src/adapters/claude/permissions/permission-handlers.ts index 979f62140e..cfc8ccd3e1 100644 --- a/packages/agent/src/adapters/claude/permissions/permission-handlers.ts +++ b/packages/agent/src/adapters/claude/permissions/permission-handlers.ts @@ -778,10 +778,10 @@ export async function canUseTool( if (isPostHogExecTool(toolName)) { const subTool = extractPostHogSubTool(toolInput); if (subTool && isPostHogDestructiveSubTool(subTool)) { - if ( - session.permissionMode === "auto" || - session.permissionMode === "bypassPermissions" - ) { + // "auto" deliberately does NOT skip this gate: the mode only promises + // hands-off file edits and shell commands, and destructive PostHog + // sub-tools must still surface a permission prompt to the client. + if (session.permissionMode === "bypassPermissions") { return { behavior: "allow", updatedInput: toolInput as Record,