feat(agent): gate destructive PostHog MCP exec sub-tools#1991
Merged
Conversation
Contributor
Prompt To Fix All With AIFix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
packages/agent/src/adapters/claude/permissions/posthog-exec-gate.test.ts:54-84
**Prefer parameterised tests**
Multiple assertions are grouped into a single `it` block here (and throughout this file), which makes it hard to see which specific input fails. The same pattern appears in `isPostHogExecTool` and `extractPostHogSubTool` tests. Using `it.each` surfaces the failing case in the test name without any extra effort:
```typescript
it.each([
["experiment-update", true],
["feature-flag-delete", true],
["notebooks-destroy", true],
["experiment-partial-update", true],
["update-something", true],
["delete", true],
["experiment-get", false],
["feature-flag-list", false],
["experiment-create", false],
["insights-pause", false],
["get-updated-events", false],
["deleter-test", false],
])("isPostHogDestructiveSubTool(%s) === %s", (input, expected) => {
expect(isPostHogDestructiveSubTool(input)).toBe(expected);
});
```
The same refactor applies to the `isPostHogExecTool` and `extractPostHogSubTool` describes.
### Issue 2 of 3
packages/agent/src/adapters/claude/hooks.test.ts:230-313
**Prefer parameterised tests**
The four `createPreToolUseHook` tests share the same shape — build a settings stub, create the hook, call it with an input, assert on the result — but are written as separate `test()` blocks. An `it.each` table would cover all cases with one declaration and surface the failing case in the test name:
```typescript
it.each([
[
"destructive PostHog exec sub-tool is deferred via ask",
"mcp__posthog__exec",
{ command: 'call dashboard-update {"id": 1, "name": "x"}' },
{ hookSpecificOutput: { permissionDecision: "ask" } },
],
[
"partial-update is also deferred",
"mcp__posthog__exec",
{ command: 'call cohorts-partial-update {"id": 1}' },
{ hookSpecificOutput: { permissionDecision: "ask" } },
],
[
"non-destructive PostHog sub-tool is allowed",
"mcp__posthog__exec",
{ command: 'call experiment-get {"id": 1}' },
{ hookSpecificOutput: { permissionDecision: "allow" } },
],
])("%s", async (_, toolName, toolInput, expected) => { … });
```
### Issue 3 of 3
packages/agent/src/adapters/claude/permissions/permission-handlers.ts:557-565
**OnceAndOnlyOnce: denial path duplicated**
The block at the bottom of `handlePostHogExecApprovalFlow` is byte-for-byte identical to the tail of `handleMcpApprovalFlow`. Extracting a shared helper would satisfy the simplicity rule and make future changes to the denial message or `interrupt` logic apply to both flows automatically:
```typescript
async function buildDenialResult(
context: ToolHandlerContext,
response: { _meta?: unknown },
): Promise<ToolPermissionResult> {
const feedback = (response._meta?.customInput as string | undefined)?.trim();
const message = feedback
? `User refused permission to run tool with feedback: ${feedback}`
: "User refused permission to run tool";
await emitToolDenial(context, message);
return { behavior: "deny", message, interrupt: !feedback };
}
```
Both `handleMcpApprovalFlow` and `handlePostHogExecApprovalFlow` can then delegate to this helper.
Reviews (1): Last reviewed commit: "fix: remove logs" | Re-trigger Greptile |
tatoalo
approved these changes
May 5, 2026
Contributor
tatoalo
left a comment
There was a problem hiding this comment.
ooof, this is great, thanks!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The PostHog MCP exposes a single
execdispatcher (mcp__posthog__exec) that runs subcommands likecall dashboard-update {...}. Once a user approvesmcp__posthog__execonce (e.g. via "always allow"), every subsequentcallflows through silently — including destructive writes (update,partial-update,delete,destroy). This violates least-privilege expectations: an approval intended for read traffic ends up authorizing arbitrary live-data mutations.Changes
posthog-exec-gate.tsmatches PostHogexectools, extracts the sub-tool from thecommandstring, and flags the destructive subset.canUseToolre-gates destructive sub-tools at sub-tool granularity. Approvals can be persisted per sub-tool (posthogApprovedExecToolsin.claude/settings.local.json) viaSettingsManager.addPostHogExecApproval.auto/bypassPermissionsmodes still skip the gate.PreToolUseHooknow returnspermissionDecision: "ask"when an existing settings allow rule would otherwise short-circuit a destructive PostHog sub-tool, forcing the SDK to invokecanUseTool. Returning{ continue: true }was insufficient — the SDK falls back to its default permission flow which re-checks the same allow rule.How did you test this code?
posthog-exec-gate.test.ts(regex coverage),permission-handlers.test.ts(gate flow),settings.test.ts(approval persistence),hooks.test.ts(deferral behavior).mcp__posthog__execallow rule in~/.claude/settings.local.json:call dashboard-update {...}now triggers an approval prompt;call experiment-get {...}does not.Publish to changelog?
no