1- import { mkdtempSync , rmSync } from "node:fs" ;
1+ import { mkdirSync , mkdtempSync , rmSync , writeFileSync } from "node:fs" ;
22import * as os from "node:os" ;
33import * as path from "node:path" ;
44import type { AgentSideConnection } from "@agentclientprotocol/sdk" ;
5+ import type { HookInput , Options } from "@anthropic-ai/claude-agent-sdk" ;
56import { afterAll , beforeEach , describe , expect , it , vi } from "vitest" ;
67import { DEFAULT_GATEWAY_MODEL } from "../../gateway-models" ;
78
@@ -38,11 +39,13 @@ function makeQueryHandle(): SdkQueryHandle {
3839}
3940
4041const createdQueries : SdkQueryHandle [ ] = [ ] ;
42+ const createdQueryOptions : Options [ ] = [ ] ;
4143
4244vi . 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-"));
8790const 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+ ) ;
90101const 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 = {
95106afterAll ( ( ) => {
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,119 @@ 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+
227+ it . each ( [
228+ { environment : "local" , expectedCloudMode : false } ,
229+ { environment : "cloud" , expectedCloudMode : true } ,
230+ ] as const ) (
231+ "records $environment sessions as cloudMode=$expectedCloudMode" ,
232+ async ( { environment, expectedCloudMode } ) => {
233+ const agent = makeAgent ( ) ;
234+
235+ await agent . newSession ( {
236+ cwd,
237+ mcpServers : [ ] ,
238+ _meta : { environment, taskRunId : `run-${ environment } ` } ,
239+ } ) ;
240+
241+ expect (
242+ ( agent as unknown as { session : { cloudMode : boolean } } ) . session
243+ . cloudMode ,
244+ ) . toBe ( expectedCloudMode ) ;
245+ } ,
246+ ) ;
247+
122248 // The SDK does not carry the model across resume — without an explicit
123249 // setModel the resumed session silently runs the SDK default (opus).
124250 it . each ( [
0 commit comments