fix(core): expose GEMINI_PLANS_DIR to hook environment (#25296)

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Adib234
2026-04-13 15:26:52 -04:00
committed by GitHub
parent 82e8d67a78
commit a4318f22ec
4 changed files with 61 additions and 2 deletions

View File

@@ -76,6 +76,9 @@ describe('HookRunner', () => {
sanitizationConfig: {
enableEnvironmentVariableRedaction: true,
},
storage: {
getPlansDir: vi.fn().mockReturnValue('/test/project/plans'),
},
} as unknown as Config;
hookRunner = new HookRunner(mockConfig);
@@ -370,12 +373,51 @@ describe('HookRunner', () => {
shell: false,
env: expect.objectContaining({
GEMINI_PROJECT_DIR: '/test/project',
GEMINI_PLANS_DIR: '/test/project/plans',
GEMINI_CWD: '/test/project',
GEMINI_SESSION_ID: 'test-session',
CLAUDE_PROJECT_DIR: '/test/project',
}),
}),
);
});
it('should expand and escape GEMINI_PLANS_DIR in commands', async () => {
const configWithEnvVar: HookConfig = {
type: HookType.Command,
command: 'ls $GEMINI_PLANS_DIR',
};
// Change plans dir to one with spaces
vi.mocked(mockConfig.storage.getPlansDir).mockReturnValue(
'/test/project/plans with spaces',
);
mockSpawn.mockProcessOn.mockImplementation(
(event: string, callback: (code: number) => void) => {
if (event === 'close') {
setImmediate(() => callback(0));
}
},
);
await hookRunner.executeHook(
configWithEnvVar,
HookEventName.BeforeTool,
mockInput,
);
expect(spawn).toHaveBeenCalledWith(
expect.stringMatching(/bash|powershell/),
expect.arrayContaining([
expect.stringMatching(
/ls ['"]\/test\/project\/plans with spaces['"]/,
),
]),
expect.any(Object),
);
});
it('should not allow command injection via GEMINI_PROJECT_DIR', async () => {
const maliciousCwd = '/test/project; echo "pwned" > /tmp/pwned';
const mockMaliciousInput: HookInput = {

View File

@@ -348,6 +348,9 @@ export class HookRunner {
const env = {
...sanitizeEnvironment(process.env, this.config.sanitizationConfig),
GEMINI_PROJECT_DIR: input.cwd,
GEMINI_PLANS_DIR: this.config.storage.getPlansDir(),
GEMINI_CWD: input.cwd,
GEMINI_SESSION_ID: input.session_id,
CLAUDE_PROJECT_DIR: input.cwd, // For compatibility
...hookConfig.env,
};
@@ -514,8 +517,17 @@ export class HookRunner {
): string {
debugLogger.debug(`Expanding hook command: ${command} (cwd: ${input.cwd})`);
const escapedCwd = escapeShellArg(input.cwd, shellType);
const escapedPlansDir = escapeShellArg(
this.config.storage.getPlansDir(),
shellType,
);
const escapedSessionId = escapeShellArg(input.session_id, shellType);
return command
.replace(/\$GEMINI_PROJECT_DIR/g, () => escapedCwd)
.replace(/\$GEMINI_CWD/g, () => escapedCwd)
.replace(/\$GEMINI_PLANS_DIR/g, () => escapedPlansDir)
.replace(/\$GEMINI_SESSION_ID/g, () => escapedSessionId)
.replace(/\$CLAUDE_PROJECT_DIR/g, () => escapedCwd); // For compatibility
}