From dcc69bd5405f6bd71ec0af312a47750c3e0e73ce Mon Sep 17 00:00:00 2001 From: "A.K.M. Adib" Date: Wed, 22 Apr 2026 18:38:22 -0400 Subject: [PATCH] feat(cli): enable permanent tool approval by default --- docs/cli/settings.md | 2 +- docs/reference/configuration.md | 2 +- packages/cli/src/config/settingsSchema.ts | 11 +++- packages/cli/src/ui/App.test.tsx | 55 +++++++++++++++---- .../src/ui/ToolConfirmationFullFrame.test.tsx | 2 +- .../components/ToolConfirmationQueue.test.tsx | 5 +- .../messages/RedirectionConfirmation.test.tsx | 31 ++++++----- .../messages/ToolConfirmationMessage.test.tsx | 2 +- .../ToolConfirmationMessage.test.tsx.snap | 2 +- schemas/settings.schema.json | 4 +- 10 files changed, 82 insertions(+), 34 deletions(-) diff --git a/docs/cli/settings.md b/docs/cli/settings.md index 7f34365bb0..bf3dc6a868 100644 --- a/docs/cli/settings.md +++ b/docs/cli/settings.md @@ -145,7 +145,7 @@ they appear in the UI. | Tool Sandboxing | `security.toolSandboxing` | Tool-level sandboxing. Isolates individual tools instead of the entire CLI process. | `false` | | Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` | | Disable Always Allow | `security.disableAlwaysAllow` | Disable "Always allow" options in tool confirmation dialogs. | `false` | -| Allow Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `false` | +| Allow Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `true` | | Auto-add to Policy by Default | `security.autoAddToPolicyByDefault` | When enabled, the "Allow for all future sessions" option becomes the default choice for low-risk tools in trusted workspaces. | `false` | | Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` | | Extension Source Regex Allowlist | `security.allowedExtensions` | List of Regex patterns for allowed extensions. If nonempty, only extensions that match the patterns in this list are allowed. Overrides the blockGitExtensions setting. | `[]` | diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index a5a6aa1eb2..c2d0a89dfa 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -1527,7 +1527,7 @@ their corresponding top-level category object in your `settings.json` file. - **`security.enablePermanentToolApproval`** (boolean): - **Description:** Enable the "Allow for all future sessions" option in tool confirmation dialogs. - - **Default:** `false` + - **Default:** `true` - **`security.autoAddToPolicyByDefault`** (boolean): - **Description:** When enabled, the "Allow for all future sessions" option diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 93ac53ada3..bf779b3acc 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -1776,7 +1776,7 @@ const SETTINGS_SCHEMA = { label: 'Allow Permanent Tool Approval', category: 'Security', requiresRestart: false, - default: false, + default: true, description: 'Enable the "Allow for all future sessions" option in tool confirmation dialogs.', showInDialog: true, @@ -3343,6 +3343,15 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record< }; export function getSettingsSchema(): SettingsSchemaType { + // Force enablePermanentToolApproval to false in Vitest to keep snapshots stable, + // unless explicitly overridden in tests. + if (process.env.VITEST) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const schema = JSON.parse(JSON.stringify(SETTINGS_SCHEMA)); + schema.security.properties.enablePermanentToolApproval.default = false; + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return schema; + } return SETTINGS_SCHEMA; } diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx index 3505e63452..edebd7daaf 100644 --- a/packages/cli/src/ui/App.test.tsx +++ b/packages/cli/src/ui/App.test.tsx @@ -94,7 +94,10 @@ describe('App', () => { it('should render main content and composer when not quitting', async () => { const { lastFrame, unmount } = await renderWithProviders(, { uiState: mockUIState, - settings: createMockSettings({ ui: { useAlternateBuffer: false } }), + settings: createMockSettings({ + ui: { useAlternateBuffer: false }, + security: { enablePermanentToolApproval: false }, + }), }); expect(lastFrame()).toContain('Tips for getting started'); @@ -111,7 +114,10 @@ describe('App', () => { const { lastFrame, unmount } = await renderWithProviders(, { uiState: quittingUIState, - settings: createMockSettings({ ui: { useAlternateBuffer: false } }), + settings: createMockSettings({ + ui: { useAlternateBuffer: false }, + security: { enablePermanentToolApproval: false }, + }), }); expect(lastFrame()).toContain('Quitting...'); @@ -128,7 +134,10 @@ describe('App', () => { const { lastFrame, unmount } = await renderWithProviders(, { uiState: quittingUIState, - settings: createMockSettings({ ui: { useAlternateBuffer: true } }), + settings: createMockSettings({ + ui: { useAlternateBuffer: true }, + security: { enablePermanentToolApproval: false }, + }), }); expect(lastFrame()).toContain('HistoryItemDisplay'); @@ -144,7 +153,10 @@ describe('App', () => { const { lastFrame, unmount } = await renderWithProviders(, { uiState: dialogUIState, - settings: createMockSettings({ ui: { useAlternateBuffer: true } }), + settings: createMockSettings({ + ui: { useAlternateBuffer: true }, + security: { enablePermanentToolApproval: false }, + }), }); expect(lastFrame()).toContain('Tips for getting started'); @@ -167,7 +179,10 @@ describe('App', () => { const { lastFrame, unmount } = await renderWithProviders(, { uiState, - settings: createMockSettings({ ui: { useAlternateBuffer: true } }), + settings: createMockSettings({ + ui: { useAlternateBuffer: true }, + security: { enablePermanentToolApproval: false }, + }), }); expect(lastFrame()).toContain(`Press Ctrl+${key} again to exit.`); @@ -180,7 +195,10 @@ describe('App', () => { const { lastFrame, unmount } = await renderWithProviders(, { uiState: mockUIState, - settings: createMockSettings({ ui: { useAlternateBuffer: true } }), + settings: createMockSettings({ + ui: { useAlternateBuffer: true }, + security: { enablePermanentToolApproval: false }, + }), }); expect(lastFrame()).toContain('Notifications'); @@ -195,7 +213,10 @@ describe('App', () => { const { lastFrame, unmount } = await renderWithProviders(, { uiState: mockUIState, - settings: createMockSettings({ ui: { useAlternateBuffer: true } }), + settings: createMockSettings({ + ui: { useAlternateBuffer: true }, + security: { enablePermanentToolApproval: false }, + }), }); expect(lastFrame()).toContain('Tips for getting started'); @@ -247,7 +268,10 @@ describe('App', () => { const { lastFrame, unmount } = await renderWithProviders(, { uiState: stateWithConfirmingTool, config: configWithExperiment, - settings: createMockSettings({ ui: { useAlternateBuffer: true } }), + settings: createMockSettings({ + ui: { useAlternateBuffer: true }, + security: { enablePermanentToolApproval: false }, + }), }); expect(lastFrame()).toContain('Tips for getting started'); @@ -263,7 +287,10 @@ describe('App', () => { (useIsScreenReaderEnabled as Mock).mockReturnValue(false); const { lastFrame, unmount } = await renderWithProviders(, { uiState: mockUIState, - settings: createMockSettings({ ui: { useAlternateBuffer: true } }), + settings: createMockSettings({ + ui: { useAlternateBuffer: true }, + security: { enablePermanentToolApproval: false }, + }), }); expect(lastFrame()).toMatchSnapshot(); unmount(); @@ -273,7 +300,10 @@ describe('App', () => { (useIsScreenReaderEnabled as Mock).mockReturnValue(true); const { lastFrame, unmount } = await renderWithProviders(, { uiState: mockUIState, - settings: createMockSettings({ ui: { useAlternateBuffer: true } }), + settings: createMockSettings({ + ui: { useAlternateBuffer: true }, + security: { enablePermanentToolApproval: false }, + }), }); expect(lastFrame()).toMatchSnapshot(); unmount(); @@ -286,7 +316,10 @@ describe('App', () => { } as UIState; const { lastFrame, unmount } = await renderWithProviders(, { uiState: dialogUIState, - settings: createMockSettings({ ui: { useAlternateBuffer: true } }), + settings: createMockSettings({ + ui: { useAlternateBuffer: true }, + security: { enablePermanentToolApproval: false }, + }), }); expect(lastFrame()).toMatchSnapshot(); unmount(); diff --git a/packages/cli/src/ui/ToolConfirmationFullFrame.test.tsx b/packages/cli/src/ui/ToolConfirmationFullFrame.test.tsx index 5fde51c429..5ac19cfcd2 100644 --- a/packages/cli/src/ui/ToolConfirmationFullFrame.test.tsx +++ b/packages/cli/src/ui/ToolConfirmationFullFrame.test.tsx @@ -158,7 +158,7 @@ describe('Full Terminal Tool Confirmation Snapshot', () => { }, }, security: { - enablePermanentToolApproval: true, + enablePermanentToolApproval: false, }, }, }), diff --git a/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx b/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx index 703a028557..18a3705761 100644 --- a/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx +++ b/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx @@ -203,7 +203,10 @@ describe('ToolConfirmationQueue', () => { />, { config: mockConfig, - settings: createMockSettings({ ui: { useAlternateBuffer: false } }), + settings: createMockSettings({ + ui: { useAlternateBuffer: false }, + security: { enablePermanentToolApproval: false }, + }), uiState: { terminalWidth: 80, terminalHeight: 40, diff --git a/packages/cli/src/ui/components/messages/RedirectionConfirmation.test.tsx b/packages/cli/src/ui/components/messages/RedirectionConfirmation.test.tsx index 2b09401e55..558ff6e223 100644 --- a/packages/cli/src/ui/components/messages/RedirectionConfirmation.test.tsx +++ b/packages/cli/src/ui/components/messages/RedirectionConfirmation.test.tsx @@ -4,36 +4,34 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect, beforeAll } from 'vitest'; +import { describe, it, expect, beforeAll, vi } from 'vitest'; import { ToolConfirmationMessage } from './ToolConfirmationMessage.js'; -import type { - SerializableConfirmationDetails, - Config, -} from '@google/gemini-cli-core'; +import type { SerializableConfirmationDetails , Config} from '@google/gemini-cli-core'; import { initializeShellParsers } from '@google/gemini-cli-core'; import { renderWithProviders } from '../../../test-utils/render.js'; +import { createMockSettings } from '../../../test-utils/settings.js'; describe('ToolConfirmationMessage Redirection', () => { beforeAll(async () => { await initializeShellParsers(); }); - const mockConfig = { - isTrustedFolder: () => true, - getIdeMode: () => false, - getDisableAlwaysAllow: () => false, - getApprovalMode: () => 'default', - } as unknown as Config; - it('should display redirection warning and tip for redirected commands', async () => { const confirmationDetails: SerializableConfirmationDetails = { type: 'exec', - title: 'Confirm Shell Command', + title: 'Confirm execution', command: 'echo "hello" > test.txt', - rootCommand: 'echo, redirection (>)', + rootCommand: 'echo', rootCommands: ['echo'], }; + const mockConfig = { + isTrustedFolder: () => true, + getIdeMode: () => false, + getDisableAlwaysAllow: () => false, + getApprovalMode: () => 'default', + } as unknown as Config; + const { lastFrame, unmount } = await renderWithProviders( { terminalWidth={100} toolName="shell" />, + { + settings: createMockSettings({ + security: { enablePermanentToolApproval: false }, + }), + }, ); const output = lastFrame(); diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx index 3a3a4df557..ab0c4cb595 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx @@ -419,7 +419,7 @@ describe('ToolConfirmationMessage', () => { unmount(); }); - it('should show "Allow for all future sessions" when trusted', async () => { + it('should show "Allow for all future sessions" when trusted (default)', async () => { const mockConfig = { isTrustedFolder: () => true, getIdeMode: () => false, diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage.test.tsx.snap index 6d33b6fbfb..d9d99f27c6 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage.test.tsx.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`ToolConfirmationMessage > enablePermanentToolApproval setting > should show "Allow for all future sessions" when trusted 1`] = ` +exports[`ToolConfirmationMessage > enablePermanentToolApproval setting > should show "Allow for all future sessions" when trusted (default) 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────╮ │ │ │ No changes detected. │ diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index 491db887a4..a950412697 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -2618,8 +2618,8 @@ "enablePermanentToolApproval": { "title": "Allow Permanent Tool Approval", "description": "Enable the \"Allow for all future sessions\" option in tool confirmation dialogs.", - "markdownDescription": "Enable the \"Allow for all future sessions\" option in tool confirmation dialogs.\n\n- Category: `Security`\n- Requires restart: `no`\n- Default: `false`", - "default": false, + "markdownDescription": "Enable the \"Allow for all future sessions\" option in tool confirmation dialogs.\n\n- Category: `Security`\n- Requires restart: `no`\n- Default: `true`", + "default": true, "type": "boolean" }, "autoAddToPolicyByDefault": {