mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-22 11:04:42 -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:
@@ -327,8 +327,12 @@ Storage whenever Gemini CLI exits Plan Mode to start the implementation.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Extract the plan path from the tool input JSON
|
# Extract the plan filename from the tool input JSON
|
||||||
plan_path=$(jq -r '.tool_input.plan_path // empty')
|
plan_filename=$(jq -r '.tool_input.plan_filename // empty')
|
||||||
|
plan_filename=$(basename -- "$plan_filename")
|
||||||
|
|
||||||
|
# Construct the absolute path using the GEMINI_PLANS_DIR environment variable
|
||||||
|
plan_path="$GEMINI_PLANS_DIR/$plan_filename"
|
||||||
|
|
||||||
if [ -f "$plan_path" ]; then
|
if [ -f "$plan_path" ]; then
|
||||||
# Generate a unique filename using a timestamp
|
# Generate a unique filename using a timestamp
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ multiple layers in the following order of precedence (highest to lowest):
|
|||||||
Hooks are executed with a sanitized environment.
|
Hooks are executed with a sanitized environment.
|
||||||
|
|
||||||
- `GEMINI_PROJECT_DIR`: The absolute path to the project root.
|
- `GEMINI_PROJECT_DIR`: The absolute path to the project root.
|
||||||
|
- `GEMINI_PLANS_DIR`: The absolute path to the plans directory.
|
||||||
- `GEMINI_SESSION_ID`: The unique ID for the current session.
|
- `GEMINI_SESSION_ID`: The unique ID for the current session.
|
||||||
- `GEMINI_CWD`: The current working directory.
|
- `GEMINI_CWD`: The current working directory.
|
||||||
- `CLAUDE_PROJECT_DIR`: (Alias) Provided for compatibility.
|
- `CLAUDE_PROJECT_DIR`: (Alias) Provided for compatibility.
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ describe('HookRunner', () => {
|
|||||||
sanitizationConfig: {
|
sanitizationConfig: {
|
||||||
enableEnvironmentVariableRedaction: true,
|
enableEnvironmentVariableRedaction: true,
|
||||||
},
|
},
|
||||||
|
storage: {
|
||||||
|
getPlansDir: vi.fn().mockReturnValue('/test/project/plans'),
|
||||||
|
},
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
hookRunner = new HookRunner(mockConfig);
|
hookRunner = new HookRunner(mockConfig);
|
||||||
@@ -370,12 +373,51 @@ describe('HookRunner', () => {
|
|||||||
shell: false,
|
shell: false,
|
||||||
env: expect.objectContaining({
|
env: expect.objectContaining({
|
||||||
GEMINI_PROJECT_DIR: '/test/project',
|
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',
|
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 () => {
|
it('should not allow command injection via GEMINI_PROJECT_DIR', async () => {
|
||||||
const maliciousCwd = '/test/project; echo "pwned" > /tmp/pwned';
|
const maliciousCwd = '/test/project; echo "pwned" > /tmp/pwned';
|
||||||
const mockMaliciousInput: HookInput = {
|
const mockMaliciousInput: HookInput = {
|
||||||
|
|||||||
@@ -348,6 +348,9 @@ export class HookRunner {
|
|||||||
const env = {
|
const env = {
|
||||||
...sanitizeEnvironment(process.env, this.config.sanitizationConfig),
|
...sanitizeEnvironment(process.env, this.config.sanitizationConfig),
|
||||||
GEMINI_PROJECT_DIR: input.cwd,
|
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
|
CLAUDE_PROJECT_DIR: input.cwd, // For compatibility
|
||||||
...hookConfig.env,
|
...hookConfig.env,
|
||||||
};
|
};
|
||||||
@@ -514,8 +517,17 @@ export class HookRunner {
|
|||||||
): string {
|
): string {
|
||||||
debugLogger.debug(`Expanding hook command: ${command} (cwd: ${input.cwd})`);
|
debugLogger.debug(`Expanding hook command: ${command} (cwd: ${input.cwd})`);
|
||||||
const escapedCwd = escapeShellArg(input.cwd, shellType);
|
const escapedCwd = escapeShellArg(input.cwd, shellType);
|
||||||
|
const escapedPlansDir = escapeShellArg(
|
||||||
|
this.config.storage.getPlansDir(),
|
||||||
|
shellType,
|
||||||
|
);
|
||||||
|
const escapedSessionId = escapeShellArg(input.session_id, shellType);
|
||||||
|
|
||||||
return command
|
return command
|
||||||
.replace(/\$GEMINI_PROJECT_DIR/g, () => escapedCwd)
|
.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
|
.replace(/\$CLAUDE_PROJECT_DIR/g, () => escapedCwd); // For compatibility
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user