Skip to content

Commit 473168f

Browse files
committed
fix(agent): default Claude PostHog exec guard
1 parent c2ca91a commit 473168f

2 files changed

Lines changed: 116 additions & 7 deletions

File tree

packages/agent/src/adapters/claude/claude-agent.resume-model.test.ts

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { mkdtempSync, rmSync } from "node:fs";
1+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
22
import * as os from "node:os";
33
import * as path from "node:path";
44
import type { AgentSideConnection } from "@agentclientprotocol/sdk";
5+
import type { HookInput, Options } from "@anthropic-ai/claude-agent-sdk";
56
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
67
import { DEFAULT_GATEWAY_MODEL } from "../../gateway-models";
78

@@ -38,11 +39,13 @@ function makeQueryHandle(): SdkQueryHandle {
3839
}
3940

4041
const createdQueries: SdkQueryHandle[] = [];
42+
const createdQueryOptions: Options[] = [];
4143

4244
vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
43-
query: vi.fn(() => {
45+
query: vi.fn(({ options }: { options: Options }) => {
4446
const handle = makeQueryHandle();
4547
createdQueries.push(handle);
48+
createdQueryOptions.push(options);
4649
return handle;
4750
}),
4851
getSessionMessages: vi.fn().mockResolvedValue([]),
@@ -87,6 +90,14 @@ const cwd = mkdtempSync(path.join(os.tmpdir(), "claude-agent-test-cwd-"));
8790
const configDir = mkdtempSync(
8891
path.join(os.tmpdir(), "claude-agent-test-config-"),
8992
);
93+
const permissionCwd = mkdtempSync(
94+
path.join(os.tmpdir(), "claude-agent-permission-test-cwd-"),
95+
);
96+
mkdirSync(path.join(permissionCwd, ".claude"), { recursive: true });
97+
writeFileSync(
98+
path.join(permissionCwd, ".claude", "settings.json"),
99+
JSON.stringify({ permissions: { allow: ["mcp__posthog__exec"] } }),
100+
);
90101
const savedEnv = {
91102
ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL,
92103
CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR,
@@ -95,6 +106,7 @@ const savedEnv = {
95106
afterAll(() => {
96107
rmSync(cwd, { recursive: true, force: true });
97108
rmSync(configDir, { recursive: true, force: true });
109+
rmSync(permissionCwd, { recursive: true, force: true });
98110
process.env.ANTHROPIC_BASE_URL = savedEnv.ANTHROPIC_BASE_URL;
99111
process.env.CLAUDE_CONFIG_DIR = savedEnv.CLAUDE_CONFIG_DIR;
100112
if (savedEnv.ANTHROPIC_BASE_URL === undefined) {
@@ -105,9 +117,10 @@ afterAll(() => {
105117
}
106118
});
107119

108-
describe("ClaudeAcpAgent session model on resume", () => {
120+
describe("ClaudeAcpAgent session creation", () => {
109121
beforeEach(() => {
110122
createdQueries.length = 0;
123+
createdQueryOptions.length = 0;
111124
nextInitPromise = Promise.resolve({
112125
result: "success",
113126
commands: [],
@@ -119,6 +132,98 @@ describe("ClaudeAcpAgent session model on resume", () => {
119132
process.env.CLAUDE_CONFIG_DIR = configDir;
120133
});
121134

135+
async function runPostHogExecPreToolUse(
136+
options: Options,
137+
subTool: string,
138+
): Promise<string | undefined> {
139+
const input = {
140+
session_id: "permission-session",
141+
transcript_path: "/tmp/transcript",
142+
cwd: permissionCwd,
143+
hook_event_name: "PreToolUse",
144+
tool_name: "mcp__posthog__exec",
145+
tool_use_id: "toolu_permission",
146+
tool_input: { command: `call ${subTool} {}` },
147+
} as HookInput;
148+
149+
for (const hook of (options.hooks?.PreToolUse ?? []).flatMap(
150+
(entry) => entry.hooks ?? [],
151+
)) {
152+
const result = await hook(input, undefined, {
153+
signal: new AbortController().signal,
154+
});
155+
const decision = (
156+
result as {
157+
hookSpecificOutput?: { permissionDecision?: string };
158+
}
159+
).hookSpecificOutput?.permissionDecision;
160+
if (decision) return decision;
161+
}
162+
163+
return undefined;
164+
}
165+
166+
it.each(["new", "resume", "load"] as const)(
167+
"uses the default PostHog exec permission regex for local %s sessions when metadata omits it",
168+
async (sessionKind) => {
169+
const agent = makeAgent();
170+
const sessionIds = {
171+
new: "0197a000-0000-7000-8000-000000000101",
172+
resume: "0197a000-0000-7000-8000-000000000102",
173+
load: "0197a000-0000-7000-8000-000000000103",
174+
};
175+
const params = {
176+
sessionId: sessionIds[sessionKind],
177+
cwd: permissionCwd,
178+
mcpServers: [],
179+
_meta: { taskRunId: `run-permission-${sessionKind}` },
180+
};
181+
182+
if (sessionKind === "new") {
183+
await agent.newSession(params);
184+
} else if (sessionKind === "resume") {
185+
await agent.resumeSession(params);
186+
} else {
187+
await agent.loadSession(params);
188+
}
189+
190+
expect(createdQueryOptions).toHaveLength(1);
191+
await expect(
192+
runPostHogExecPreToolUse(
193+
createdQueryOptions[0] as Options,
194+
"dashboard-update",
195+
),
196+
).resolves.toBe("ask");
197+
},
198+
);
199+
200+
it("uses an explicit PostHog exec permission regex instead of the default", async () => {
201+
const agent = makeAgent();
202+
203+
await agent.newSession({
204+
cwd: permissionCwd,
205+
mcpServers: [],
206+
_meta: {
207+
taskRunId: "run-permission-custom",
208+
posthogExecPermissionRegex: "(^|-)archive(-|$)",
209+
},
210+
});
211+
212+
expect(createdQueryOptions).toHaveLength(1);
213+
await expect(
214+
runPostHogExecPreToolUse(
215+
createdQueryOptions[0] as Options,
216+
"dashboard-update",
217+
),
218+
).resolves.toBe("allow");
219+
await expect(
220+
runPostHogExecPreToolUse(
221+
createdQueryOptions[0] as Options,
222+
"dashboard-archive",
223+
),
224+
).resolves.toBe("ask");
225+
});
226+
122227
// The SDK does not carry the model across resume — without an explicit
123228
// setModel the resumed session silently runs the SDK default (opus).
124229
it.each([

packages/agent/src/adapters/claude/claude-agent.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ import {
5757
type Enrichment,
5858
type FileEnrichmentDeps,
5959
} from "../../enrichment/file-enricher";
60-
import { compilePostHogExecPermissionRegex } from "../../posthog-exec-permission";
60+
import {
61+
compilePostHogExecPermissionRegex,
62+
DEFAULT_POSTHOG_EXEC_PERMISSION_REGEX_SOURCE,
63+
} from "../../posthog-exec-permission";
6164
import {
6265
classifyPostHogExecCall,
6366
isUnclassifiedPostHogSubTool,
@@ -1951,9 +1954,10 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
19511954
CODE_EXECUTION_MODES.includes(meta.permissionMode as CodeExecutionMode)
19521955
? (meta.permissionMode as CodeExecutionMode)
19531956
: "default";
1954-
const posthogExecPermissionRegex = meta?.posthogExecPermissionRegex
1955-
? compilePostHogExecPermissionRegex(meta.posthogExecPermissionRegex)
1956-
: undefined;
1957+
const posthogExecPermissionRegex = compilePostHogExecPermissionRegex(
1958+
meta?.posthogExecPermissionRegex ??
1959+
DEFAULT_POSTHOG_EXEC_PERMISSION_REGEX_SOURCE,
1960+
);
19571961

19581962
const taskState: TaskState = new Map();
19591963
const options = buildSessionOptions({

0 commit comments

Comments
 (0)