Skip to content

Commit 537e56f

Browse files
authored
feat(plan): support configuring custom plans storage directory (#19577)
1 parent 2cba2ab commit 537e56f

24 files changed

Lines changed: 337 additions & 58 deletions

docs/cli/plan-mode.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,47 @@ To use a skill in Plan Mode, you can explicitly ask the agent to "use the
127127
[skill-name] skill to plan..." or the agent may autonomously activate it based
128128
on the task description.
129129

130+
### Custom Plan Directory and Policies
131+
132+
By default, planning artifacts are stored in a managed temporary directory
133+
outside your project: `~/.gemini/tmp/<project>/<session-id>/plans/`.
134+
135+
You can configure a custom directory for plans in your `settings.json`. For
136+
example, to store plans in a `.gemini/plans` directory within your project:
137+
138+
```json
139+
{
140+
"general": {
141+
"plan": {
142+
"directory": ".gemini/plans"
143+
}
144+
}
145+
}
146+
```
147+
148+
To maintain the safety of Plan Mode, user-configured paths for the plans
149+
directory are restricted to the project root. This ensures that custom planning
150+
locations defined within a project's workspace cannot be used to escape and
151+
overwrite sensitive files elsewhere. Any user-configured directory must reside
152+
within the project boundary.
153+
154+
Because Plan Mode is read-only by default, using a custom directory requires
155+
updating your [Policy Engine] configurations to allow `write_file` and `replace`
156+
in that specific location. For example, to allow writing to the `.gemini/plans`
157+
directory within your project, create a policy file at
158+
`~/.gemini/policies/plan-custom-directory.toml`:
159+
160+
```toml
161+
[[rule]]
162+
toolName = ["write_file", "replace"]
163+
decision = "allow"
164+
priority = 100
165+
modes = ["plan"]
166+
# Adjust the pattern to match your custom directory.
167+
# This example matches any .md file in a .gemini/plans directory within the project.
168+
argsPattern = "\"file_path\":\".*\\\\.gemini/plans/.*\\\\.md\""
169+
```
170+
130171
### Customizing Policies
131172

132173
Plan Mode is designed to be read-only by default to ensure safety during the

docs/cli/settings.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ they appear in the UI.
2828
| Default Approval Mode | `general.defaultApprovalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet. | `"default"` |
2929
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
3030
| Enable Notifications | `general.enableNotifications` | Enable run-event notifications for action-required prompts and session completion. Currently macOS only. | `false` |
31+
| Plan Directory | `general.plan.directory` | The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. | `undefined` |
3132
| Enable Prompt Completion | `general.enablePromptCompletion` | Enable AI-powered prompt completion suggestions while typing. | `false` |
3233
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
3334
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |

docs/get-started/configuration.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ their corresponding top-level category object in your `settings.json` file.
131131
- **Default:** `false`
132132
- **Requires restart:** Yes
133133

134+
- **`general.plan.directory`** (string):
135+
- **Description:** The directory where planning artifacts are stored. If not
136+
specified, defaults to the system temporary directory.
137+
- **Default:** `undefined`
138+
- **Requires restart:** Yes
139+
134140
- **`general.enablePromptCompletion`** (boolean):
135141
- **Description:** Enable AI-powered prompt completion suggestions while
136142
typing.

packages/cli/src/config/config.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ import {
2121
type MCPServerConfig,
2222
} from '@google/gemini-cli-core';
2323
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
24-
import { type Settings, createTestMergedSettings } from './settings.js';
24+
import {
25+
type Settings,
26+
type MergedSettings,
27+
createTestMergedSettings,
28+
} from './settings.js';
2529
import * as ServerConfig from '@google/gemini-cli-core';
2630

2731
import { isWorkspaceTrusted } from './trustedFolders.js';
@@ -2599,6 +2603,21 @@ describe('loadCliConfig approval mode', () => {
25992603
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
26002604
});
26012605

2606+
it('should pass planSettings.directory from settings to config', async () => {
2607+
process.argv = ['node', 'script.js'];
2608+
const settings = createTestMergedSettings({
2609+
general: {
2610+
plan: {
2611+
directory: '.custom-plans',
2612+
},
2613+
},
2614+
} as unknown as MergedSettings);
2615+
const argv = await parseArguments(settings);
2616+
const config = await loadCliConfig(settings, 'test-session', argv);
2617+
const plansDir = config.storage.getPlansDir();
2618+
expect(plansDir).toContain('.custom-plans');
2619+
});
2620+
26022621
// --- Untrusted Folder Scenarios ---
26032622
describe('when folder is NOT trusted', () => {
26042623
beforeEach(() => {

packages/cli/src/config/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,7 @@ export async function loadCliConfig(
814814
enableExtensionReloading: settings.experimental?.extensionReloading,
815815
enableAgents: settings.experimental?.enableAgents,
816816
plan: settings.experimental?.plan,
817+
planSettings: settings.general.plan,
817818
enableEventDrivenScheduler: true,
818819
skillsSupport: settings.skills?.enabled ?? true,
819820
disabledSkills: settings.skills?.disabled,

packages/cli/src/config/settingsSchema.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,16 @@ describe('SettingsSchema', () => {
107107
).toBe('boolean');
108108
});
109109

110+
it('should have plan nested properties', () => {
111+
expect(
112+
getSettingsSchema().general?.properties?.plan?.properties?.directory,
113+
).toBeDefined();
114+
expect(
115+
getSettingsSchema().general?.properties?.plan?.properties?.directory
116+
.type,
117+
).toBe('string');
118+
});
119+
110120
it('should have fileFiltering nested properties', () => {
111121
expect(
112122
getSettingsSchema().context.properties.fileFiltering.properties

packages/cli/src/config/settingsSchema.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,27 @@ const SETTINGS_SCHEMA = {
266266
},
267267
},
268268
},
269+
plan: {
270+
type: 'object',
271+
label: 'Plan',
272+
category: 'General',
273+
requiresRestart: true,
274+
default: {},
275+
description: 'Planning features configuration.',
276+
showInDialog: false,
277+
properties: {
278+
directory: {
279+
type: 'string',
280+
label: 'Plan Directory',
281+
category: 'General',
282+
requiresRestart: true,
283+
default: undefined as string | undefined,
284+
description:
285+
'The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory.',
286+
showInDialog: true,
287+
},
288+
},
289+
},
269290
enablePromptCompletion: {
270291
type: 'boolean',
271292
label: 'Enable Prompt Completion',
@@ -1313,6 +1334,7 @@ const SETTINGS_SCHEMA = {
13131334
},
13141335
},
13151336
},
1337+
13161338
useWriteTodos: {
13171339
type: 'boolean',
13181340
label: 'Use WriteTodos',

packages/cli/src/ui/commands/planCommand.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ describe('planCommand', () => {
5151
getApprovalMode: vi.fn(),
5252
getFileSystemService: vi.fn(),
5353
storage: {
54-
getProjectTempPlansDir: vi.fn().mockReturnValue('/mock/plans/dir'),
54+
getPlansDir: vi.fn().mockReturnValue('/mock/plans/dir'),
5555
},
5656
},
5757
},

packages/cli/src/ui/commands/planCommand.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export const planCommand: SlashCommand = {
4343
try {
4444
const content = await processSingleFileContent(
4545
approvedPlanPath,
46-
config.storage.getProjectTempPlansDir(),
46+
config.storage.getPlansDir(),
4747
config.getFileSystemService(),
4848
);
4949
const fileName = path.basename(approvedPlanPath);

packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ Implement a comprehensive authentication system with multiple providers.
154154
getIdeMode: () => false,
155155
isTrustedFolder: () => true,
156156
storage: {
157-
getProjectTempPlansDir: () => mockPlansDir,
157+
getPlansDir: () => mockPlansDir,
158158
},
159159
getFileSystemService: (): FileSystemService => ({
160160
readTextFile: vi.fn(),
@@ -429,7 +429,7 @@ Implement a comprehensive authentication system with multiple providers.
429429
getIdeMode: () => false,
430430
isTrustedFolder: () => true,
431431
storage: {
432-
getProjectTempPlansDir: () => mockPlansDir,
432+
getPlansDir: () => mockPlansDir,
433433
},
434434
getFileSystemService: (): FileSystemService => ({
435435
readTextFile: vi.fn(),

0 commit comments

Comments
 (0)