feat(plan): document and validate Plan Mode policy overrides (#18825)

This commit is contained in:
Jerop Kipruto
2026-02-11 12:32:02 -05:00
committed by GitHub
parent 0080589939
commit 65d26e73a2
3 changed files with 166 additions and 3 deletions

View File

@@ -30,6 +30,7 @@ implementation strategy.
- [The Planning Workflow](#the-planning-workflow)
- [Exiting Plan Mode](#exiting-plan-mode)
- [Tool Restrictions](#tool-restrictions)
- [Customizing Policies](#customizing-policies)
## Starting in Plan Mode
@@ -98,6 +99,53 @@ These are the only allowed tools:
- **Planning (Write):** [`write_file`] and [`replace`] ONLY allowed for `.md`
files in the `~/.gemini/tmp/<project>/plans/` directory.
### Customizing Policies
Plan Mode is designed to be read-only by default to ensure safety during the
research phase. However, you may occasionally need to allow specific tools to
assist in your planning.
Because user policies (Tier 2) have a higher base priority than built-in
policies (Tier 1), you can override Plan Mode's default restrictions by creating
a rule in your `~/.gemini/policies/` directory.
#### Example: Allow `git status` and `git diff` in Plan Mode
This rule allows you to check the repository status and see changes while in
Plan Mode.
`~/.gemini/policies/git-research.toml`
```toml
[[rule]]
toolName = "run_shell_command"
commandPrefix = ["git status", "git diff"]
decision = "allow"
priority = 100
modes = ["plan"]
```
#### Example: Enable research sub-agents in Plan Mode
You can enable [experimental research sub-agents] like `codebase_investigator`
to help gather architecture details during the planning phase.
`~/.gemini/policies/research-subagents.toml`
```toml
[[rule]]
toolName = "codebase_investigator"
decision = "allow"
priority = 100
modes = ["plan"]
```
Tell the agent it can use these tools in your prompt, for example: _"You can
check ongoing changes in git."_
For more information on how the policy engine works, see the [Policy Engine
Guide].
[`list_directory`]: /docs/tools/file-system.md#1-list_directory-readfolder
[`read_file`]: /docs/tools/file-system.md#2-read_file-readfile
[`grep_search`]: /docs/tools/file-system.md#5-grep_search-searchtext
@@ -106,3 +154,5 @@ These are the only allowed tools:
[`google_web_search`]: /docs/tools/web-search.md
[`replace`]: /docs/tools/file-system.md#6-replace-edit
[MCP tools]: /docs/tools/mcp-server.md
[experimental research sub-agents]: /docs/core/subagents.md
[Policy Engine Guide]: /docs/core/policy-engine.md

View File

@@ -119,9 +119,17 @@ For example:
Approval modes allow the policy engine to apply different sets of rules based on
the CLI's operational mode. A rule can be associated with one or more modes
(e.g., `yolo`, `autoEdit`). The rule will only be active if the CLI is running
in one of its specified modes. If a rule has no modes specified, it is always
active.
(e.g., `yolo`, `autoEdit`, `plan`). The rule will only be active if the CLI is
running in one of its specified modes. If a rule has no modes specified, it is
always active.
- `default`: The standard interactive mode where most write tools require
confirmation.
- `autoEdit`: Optimized for automated code editing; some write tools may be
auto-approved.
- `plan`: A strict, read-only mode for research and design. See [Customizing
Plan Mode Policies].
- `yolo`: A mode where all tools are auto-approved (use with extreme caution).
## Rule matching
@@ -303,3 +311,5 @@ out-of-the-box experience.
- In **`yolo`** mode, a high-priority rule allows all tools.
- In **`autoEdit`** mode, rules allow certain write operations to happen without
prompting.
[Customizing Plan Mode Policies]: /docs/cli/plan-mode.md#customizing-policies

View File

@@ -951,4 +951,107 @@ name = "invalid-name"
vi.doUnmock('node:fs/promises');
});
it('should allow overriding Plan Mode deny with user policy', async () => {
const actualFs =
await vi.importActual<typeof import('node:fs/promises')>(
'node:fs/promises',
);
const mockReaddir = vi.fn(
async (
path: string | Buffer | URL,
options?: Parameters<typeof actualFs.readdir>[1],
) => {
const normalizedPath = nodePath.normalize(path.toString());
if (normalizedPath.includes(nodePath.normalize('.gemini/policies'))) {
return [
{
name: 'user-plan.toml',
isFile: () => true,
isDirectory: () => false,
},
] as unknown as Awaited<ReturnType<typeof actualFs.readdir>>;
}
return actualFs.readdir(
path,
options as Parameters<typeof actualFs.readdir>[1],
);
},
);
const mockReadFile = vi.fn(
async (
path: Parameters<typeof actualFs.readFile>[0],
options: Parameters<typeof actualFs.readFile>[1],
) => {
const normalizedPath = nodePath.normalize(path.toString());
if (normalizedPath.includes('user-plan.toml')) {
return `
[[rule]]
toolName = "run_shell_command"
commandPrefix = ["git status", "git diff"]
decision = "allow"
priority = 100
modes = ["plan"]
[[rule]]
toolName = "codebase_investigator"
decision = "allow"
priority = 100
modes = ["plan"]
`;
}
return actualFs.readFile(path, options);
},
);
vi.doMock('node:fs/promises', () => ({
...actualFs,
default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
readFile: mockReadFile,
readdir: mockReaddir,
}));
vi.resetModules();
const { createPolicyEngineConfig } = await import('./config.js');
const settings: PolicySettings = {};
const config = await createPolicyEngineConfig(
settings,
ApprovalMode.PLAN,
nodePath.join(__dirname, 'policies'),
);
const shellRules = config.rules?.filter(
(r) =>
r.toolName === 'run_shell_command' &&
r.decision === PolicyDecision.ALLOW &&
r.modes?.includes(ApprovalMode.PLAN) &&
r.argsPattern,
);
expect(shellRules).toHaveLength(2);
expect(
shellRules?.some((r) => r.argsPattern?.test('{"command":"git status"}')),
).toBe(true);
expect(
shellRules?.some((r) => r.argsPattern?.test('{"command":"git diff"}')),
).toBe(true);
expect(
shellRules?.every(
(r) => !r.argsPattern?.test('{"command":"git commit"}'),
),
).toBe(true);
const subagentRule = config.rules?.find(
(r) =>
r.toolName === 'codebase_investigator' &&
r.decision === PolicyDecision.ALLOW &&
r.modes?.includes(ApprovalMode.PLAN),
);
expect(subagentRule).toBeDefined();
expect(subagentRule?.priority).toBeCloseTo(2.1, 5);
vi.doUnmock('node:fs/promises');
});
});