@@ -76,6 +76,9 @@ describe('HookRunner', () => {
7676 sanitizationConfig : {
7777 enableEnvironmentVariableRedaction : true ,
7878 } ,
79+ storage : {
80+ getPlansDir : vi . fn ( ) . mockReturnValue ( '/test/project/plans' ) ,
81+ } ,
7982 } as unknown as Config ;
8083
8184 hookRunner = new HookRunner ( mockConfig ) ;
@@ -370,12 +373,51 @@ describe('HookRunner', () => {
370373 shell : false ,
371374 env : expect . objectContaining ( {
372375 GEMINI_PROJECT_DIR : '/test/project' ,
376+ GEMINI_PLANS_DIR : '/test/project/plans' ,
377+ GEMINI_CWD : '/test/project' ,
378+ GEMINI_SESSION_ID : 'test-session' ,
373379 CLAUDE_PROJECT_DIR : '/test/project' ,
374380 } ) ,
375381 } ) ,
376382 ) ;
377383 } ) ;
378384
385+ it ( 'should expand and escape GEMINI_PLANS_DIR in commands' , async ( ) => {
386+ const configWithEnvVar : HookConfig = {
387+ type : HookType . Command ,
388+ command : 'ls $GEMINI_PLANS_DIR' ,
389+ } ;
390+
391+ // Change plans dir to one with spaces
392+ vi . mocked ( mockConfig . storage . getPlansDir ) . mockReturnValue (
393+ '/test/project/plans with spaces' ,
394+ ) ;
395+
396+ mockSpawn . mockProcessOn . mockImplementation (
397+ ( event : string , callback : ( code : number ) => void ) => {
398+ if ( event === 'close' ) {
399+ setImmediate ( ( ) => callback ( 0 ) ) ;
400+ }
401+ } ,
402+ ) ;
403+
404+ await hookRunner . executeHook (
405+ configWithEnvVar ,
406+ HookEventName . BeforeTool ,
407+ mockInput ,
408+ ) ;
409+
410+ expect ( spawn ) . toHaveBeenCalledWith (
411+ expect . stringMatching ( / b a s h | p o w e r s h e l l / ) ,
412+ expect . arrayContaining ( [
413+ expect . stringMatching (
414+ / l s [ ' " ] \/ t e s t \/ p r o j e c t \/ p l a n s w i t h s p a c e s [ ' " ] / ,
415+ ) ,
416+ ] ) ,
417+ expect . any ( Object ) ,
418+ ) ;
419+ } ) ;
420+
379421 it ( 'should not allow command injection via GEMINI_PROJECT_DIR' , async ( ) => {
380422 const maliciousCwd = '/test/project; echo "pwned" > /tmp/pwned' ;
381423 const mockMaliciousInput : HookInput = {
0 commit comments