mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-14 23:31:13 -07:00
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:
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user