diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts
index f7a1bfa2..907af9e0 100644
--- a/packages/cli/src/config/config.test.ts
+++ b/packages/cli/src/config/config.test.ts
@@ -23,7 +23,7 @@ import * as ServerConfig from '@google/gemini-cli-core';
import { isWorkspaceTrusted } from './trustedFolders.js';
vi.mock('./trustedFolders.js', () => ({
- isWorkspaceTrusted: vi.fn(),
+ isWorkspaceTrusted: vi.fn().mockReturnValue(true), // Default to trusted
}));
vi.mock('fs', async (importOriginal) => {
@@ -1002,6 +1002,7 @@ describe('Approval mode tool exclusion logic', () => {
beforeEach(() => {
process.stdin.isTTY = false; // Ensure non-interactive mode
+ vi.mocked(isWorkspaceTrusted).mockReturnValue(true);
});
afterEach(() => {
@@ -1680,6 +1681,7 @@ describe('loadCliConfig tool exclusions', () => {
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
process.stdin.isTTY = true;
+ vi.mocked(isWorkspaceTrusted).mockReturnValue(true);
});
afterEach(() => {
@@ -1789,6 +1791,7 @@ describe('loadCliConfig approval mode', () => {
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
process.argv = ['node', 'script.js']; // Reset argv for each test
+ vi.mocked(isWorkspaceTrusted).mockReturnValue(true);
});
afterEach(() => {
@@ -1856,6 +1859,41 @@ describe('loadCliConfig approval mode', () => {
const config = await loadCliConfig({}, [], 'test-session', argv);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
});
+
+ // --- Untrusted Folder Scenarios ---
+ describe('when folder is NOT trusted', () => {
+ beforeEach(() => {
+ vi.mocked(isWorkspaceTrusted).mockReturnValue(false);
+ });
+
+ it('should override --approval-mode=yolo to DEFAULT', async () => {
+ process.argv = ['node', 'script.js', '--approval-mode', 'yolo'];
+ const argv = await parseArguments({} as Settings);
+ const config = await loadCliConfig({}, [], 'test-session', argv);
+ expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
+ });
+
+ it('should override --approval-mode=auto_edit to DEFAULT', async () => {
+ process.argv = ['node', 'script.js', '--approval-mode', 'auto_edit'];
+ const argv = await parseArguments({} as Settings);
+ const config = await loadCliConfig({}, [], 'test-session', argv);
+ expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
+ });
+
+ it('should override --yolo flag to DEFAULT', async () => {
+ process.argv = ['node', 'script.js', '--yolo'];
+ const argv = await parseArguments({} as Settings);
+ const config = await loadCliConfig({}, [], 'test-session', argv);
+ expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
+ });
+
+ it('should remain DEFAULT when --approval-mode=default', async () => {
+ process.argv = ['node', 'script.js', '--approval-mode', 'default'];
+ const argv = await parseArguments({} as Settings);
+ const config = await loadCliConfig({}, [], 'test-session', argv);
+ expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
+ });
+ });
});
describe('loadCliConfig trustedFolder', () => {
diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts
index 03ec676e..7a5de02e 100755
--- a/packages/cli/src/config/config.ts
+++ b/packages/cli/src/config/config.ts
@@ -406,6 +406,14 @@ export async function loadCliConfig(
argv.yolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT;
}
+ // Force approval mode to default if the folder is not trusted.
+ if (!trustedFolder && approvalMode !== ApprovalMode.DEFAULT) {
+ logger.warn(
+ `Approval mode overridden to "default" because the current folder is not trusted.`,
+ );
+ approvalMode = ApprovalMode.DEFAULT;
+ }
+
const interactive =
!!argv.promptInteractive || (process.stdin.isTTY && question.length === 0);
// In non-interactive mode, exclude tools that require a prompt.
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
index e65114f9..4561853b 100644
--- a/packages/cli/src/ui/App.tsx
+++ b/packages/cli/src/ui/App.tsx
@@ -627,7 +627,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
const { elapsedTime, currentLoadingPhrase } =
useLoadingIndicator(streamingState);
- const showAutoAcceptIndicator = useAutoAcceptIndicator({ config });
+ const showAutoAcceptIndicator = useAutoAcceptIndicator({ config, addItem });
const handleExit = useCallback(
(
diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx
index 6e54e7a4..94cffff3 100644
--- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx
+++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx
@@ -6,7 +6,10 @@
import { describe, it, expect, vi } from 'vitest';
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
-import type { ToolCallConfirmationDetails } from '@google/gemini-cli-core';
+import type {
+ ToolCallConfirmationDetails,
+ Config,
+} from '@google/gemini-cli-core';
import { renderWithProviders } from '../../../test-utils/render.js';
describe('ToolConfirmationMessage', () => {
@@ -55,4 +58,101 @@ describe('ToolConfirmationMessage', () => {
'- https://raw.githubusercontent.com/google/gemini-react/main/README.md',
);
});
+
+ describe('with folder trust', () => {
+ const editConfirmationDetails: ToolCallConfirmationDetails = {
+ type: 'edit',
+ title: 'Confirm Edit',
+ fileName: 'test.txt',
+ filePath: '/test.txt',
+ fileDiff: '...diff...',
+ originalContent: 'a',
+ newContent: 'b',
+ onConfirm: vi.fn(),
+ };
+
+ const execConfirmationDetails: ToolCallConfirmationDetails = {
+ type: 'exec',
+ title: 'Confirm Execution',
+ command: 'echo "hello"',
+ rootCommand: 'echo',
+ onConfirm: vi.fn(),
+ };
+
+ const infoConfirmationDetails: ToolCallConfirmationDetails = {
+ type: 'info',
+ title: 'Confirm Web Fetch',
+ prompt: 'https://example.com',
+ urls: ['https://example.com'],
+ onConfirm: vi.fn(),
+ };
+
+ const mcpConfirmationDetails: ToolCallConfirmationDetails = {
+ type: 'mcp',
+ title: 'Confirm MCP Tool',
+ serverName: 'test-server',
+ toolName: 'test-tool',
+ toolDisplayName: 'Test Tool',
+ onConfirm: vi.fn(),
+ };
+
+ describe.each([
+ {
+ description: 'for edit confirmations',
+ details: editConfirmationDetails,
+ alwaysAllowText: 'Yes, allow always',
+ },
+ {
+ description: 'for exec confirmations',
+ details: execConfirmationDetails,
+ alwaysAllowText: 'Yes, allow always',
+ },
+ {
+ description: 'for info confirmations',
+ details: infoConfirmationDetails,
+ alwaysAllowText: 'Yes, allow always',
+ },
+ {
+ description: 'for mcp confirmations',
+ details: mcpConfirmationDetails,
+ alwaysAllowText: 'always allow',
+ },
+ ])('$description', ({ details, alwaysAllowText }) => {
+ it('should show "allow always" when folder is trusted', () => {
+ const mockConfig = {
+ isTrustedFolder: () => true,
+ getIdeMode: () => false,
+ } as unknown as Config;
+
+ const { lastFrame } = renderWithProviders(
+ ,
+ );
+
+ expect(lastFrame()).toContain(alwaysAllowText);
+ });
+
+ it('should NOT show "allow always" when folder is untrusted', () => {
+ const mockConfig = {
+ isTrustedFolder: () => false,
+ getIdeMode: () => false,
+ } as unknown as Config;
+
+ const { lastFrame } = renderWithProviders(
+ ,
+ );
+
+ expect(lastFrame()).not.toContain(alwaysAllowText);
+ });
+ });
+ });
});
diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
index 6dee3d74..8cd7756e 100644
--- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
@@ -125,16 +125,16 @@ export const ToolConfirmationMessage: React.FC<
}
question = `Apply this change?`;
- options.push(
- {
- label: 'Yes, allow once',
- value: ToolConfirmationOutcome.ProceedOnce,
- },
- {
+ options.push({
+ label: 'Yes, allow once',
+ value: ToolConfirmationOutcome.ProceedOnce,
+ });
+ if (config?.isTrustedFolder()) {
+ options.push({
label: 'Yes, allow always',
value: ToolConfirmationOutcome.ProceedAlways,
- },
- );
+ });
+ }
if (config?.getIdeMode()) {
options.push({
label: 'No (esc)',
@@ -164,20 +164,20 @@ export const ToolConfirmationMessage: React.FC<
confirmationDetails as ToolExecuteConfirmationDetails;
question = `Allow execution of: '${executionProps.rootCommand}'?`;
- options.push(
- {
- label: `Yes, allow once`,
- value: ToolConfirmationOutcome.ProceedOnce,
- },
- {
+ options.push({
+ label: 'Yes, allow once',
+ value: ToolConfirmationOutcome.ProceedOnce,
+ });
+ if (config?.isTrustedFolder()) {
+ options.push({
label: `Yes, allow always ...`,
value: ToolConfirmationOutcome.ProceedAlways,
- },
- {
- label: 'No, suggest changes (esc)',
- value: ToolConfirmationOutcome.Cancel,
- },
- );
+ });
+ }
+ options.push({
+ label: 'No, suggest changes (esc)',
+ value: ToolConfirmationOutcome.Cancel,
+ });
let bodyContentHeight = availableBodyContentHeight();
if (bodyContentHeight !== undefined) {
@@ -204,20 +204,20 @@ export const ToolConfirmationMessage: React.FC<
!(infoProps.urls.length === 1 && infoProps.urls[0] === infoProps.prompt);
question = `Do you want to proceed?`;
- options.push(
- {
- label: 'Yes, allow once',
- value: ToolConfirmationOutcome.ProceedOnce,
- },
- {
+ options.push({
+ label: 'Yes, allow once',
+ value: ToolConfirmationOutcome.ProceedOnce,
+ });
+ if (config?.isTrustedFolder()) {
+ options.push({
label: 'Yes, allow always',
value: ToolConfirmationOutcome.ProceedAlways,
- },
- {
- label: 'No, suggest changes (esc)',
- value: ToolConfirmationOutcome.Cancel,
- },
- );
+ });
+ }
+ options.push({
+ label: 'No, suggest changes (esc)',
+ value: ToolConfirmationOutcome.Cancel,
+ });
bodyContent = (
@@ -249,24 +249,24 @@ export const ToolConfirmationMessage: React.FC<
);
question = `Allow execution of MCP tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"?`;
- options.push(
- {
- label: 'Yes, allow once',
- value: ToolConfirmationOutcome.ProceedOnce,
- },
- {
+ options.push({
+ label: 'Yes, allow once',
+ value: ToolConfirmationOutcome.ProceedOnce,
+ });
+ if (config?.isTrustedFolder()) {
+ options.push({
label: `Yes, always allow tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"`,
value: ToolConfirmationOutcome.ProceedAlwaysTool, // Cast until types are updated
- },
- {
+ });
+ options.push({
label: `Yes, always allow all tools from server "${mcpProps.serverName}"`,
value: ToolConfirmationOutcome.ProceedAlwaysServer,
- },
- {
- label: 'No, suggest changes (esc)',
- value: ToolConfirmationOutcome.Cancel,
- },
- );
+ });
+ }
+ options.push({
+ label: 'No, suggest changes (esc)',
+ value: ToolConfirmationOutcome.Cancel,
+ });
}
return (
diff --git a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts
index ab85dd83..9db68f1b 100644
--- a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts
+++ b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts
@@ -20,6 +20,7 @@ import type { Config as ActualConfigType } from '@google/gemini-cli-core';
import { Config, ApprovalMode } from '@google/gemini-cli-core';
import type { Key } from './useKeypress.js';
import { useKeypress } from './useKeypress.js';
+import { MessageType } from '../types.js';
vi.mock('./useKeypress.js');
@@ -36,6 +37,7 @@ vi.mock('@google/gemini-cli-core', async () => {
interface MockConfigInstanceShape {
getApprovalMode: Mock<() => ApprovalMode>;
setApprovalMode: Mock<(value: ApprovalMode) => void>;
+ isTrustedFolder: Mock<() => boolean>;
getCoreTools: Mock<() => string[]>;
getToolDiscoveryCommand: Mock<() => string | undefined>;
getTargetDir: Mock<() => string>;
@@ -74,6 +76,7 @@ describe('useAutoAcceptIndicator', () => {
setApprovalMode: instanceSetApprovalModeMock as Mock<
(value: ApprovalMode) => void
>,
+ isTrustedFolder: vi.fn().mockReturnValue(true) as Mock<() => boolean>,
getCoreTools: vi.fn().mockReturnValue([]) as Mock<() => string[]>,
getToolDiscoveryCommand: vi.fn().mockReturnValue(undefined) as Mock<
() => string | undefined
@@ -124,6 +127,7 @@ describe('useAutoAcceptIndicator', () => {
const { result } = renderHook(() =>
useAutoAcceptIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
+ addItem: vi.fn(),
}),
);
expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
@@ -135,6 +139,7 @@ describe('useAutoAcceptIndicator', () => {
const { result } = renderHook(() =>
useAutoAcceptIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
+ addItem: vi.fn(),
}),
);
expect(result.current).toBe(ApprovalMode.DEFAULT);
@@ -146,6 +151,7 @@ describe('useAutoAcceptIndicator', () => {
const { result } = renderHook(() =>
useAutoAcceptIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
+ addItem: vi.fn(),
}),
);
expect(result.current).toBe(ApprovalMode.YOLO);
@@ -157,6 +163,7 @@ describe('useAutoAcceptIndicator', () => {
const { result } = renderHook(() =>
useAutoAcceptIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
+ addItem: vi.fn(),
}),
);
expect(result.current).toBe(ApprovalMode.DEFAULT);
@@ -224,6 +231,7 @@ describe('useAutoAcceptIndicator', () => {
renderHook(() =>
useAutoAcceptIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
+ addItem: vi.fn(),
}),
);
@@ -280,10 +288,12 @@ describe('useAutoAcceptIndicator', () => {
it('should update indicator when config value changes externally (useEffect dependency)', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
const { result, rerender } = renderHook(
- (props: { config: ActualConfigType }) => useAutoAcceptIndicator(props),
+ (props: { config: ActualConfigType; addItem: () => void }) =>
+ useAutoAcceptIndicator(props),
{
initialProps: {
config: mockConfigInstance as unknown as ActualConfigType,
+ addItem: vi.fn(),
},
},
);
@@ -291,8 +301,173 @@ describe('useAutoAcceptIndicator', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.AUTO_EDIT);
- rerender({ config: mockConfigInstance as unknown as ActualConfigType });
+ rerender({
+ config: mockConfigInstance as unknown as ActualConfigType,
+ addItem: vi.fn(),
+ });
expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
expect(mockConfigInstance.getApprovalMode).toHaveBeenCalledTimes(3);
});
+
+ describe('in untrusted folders', () => {
+ beforeEach(() => {
+ mockConfigInstance.isTrustedFolder.mockReturnValue(false);
+ });
+
+ it('should not enable YOLO mode when Ctrl+Y is pressed', () => {
+ mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
+ mockConfigInstance.setApprovalMode.mockImplementation(() => {
+ throw new Error(
+ 'Cannot enable privileged approval modes in an untrusted folder.',
+ );
+ });
+ const mockAddItem = vi.fn();
+ const { result } = renderHook(() =>
+ useAutoAcceptIndicator({
+ config: mockConfigInstance as unknown as ActualConfigType,
+ addItem: mockAddItem,
+ }),
+ );
+
+ expect(result.current).toBe(ApprovalMode.DEFAULT);
+
+ act(() => {
+ capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
+ });
+
+ // We expect setApprovalMode to be called, and the error to be caught.
+ expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
+ ApprovalMode.YOLO,
+ );
+ expect(mockAddItem).toHaveBeenCalled();
+ // Verify the underlying config value was not changed
+ expect(mockConfigInstance.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
+ });
+
+ it('should not enable AUTO_EDIT mode when Shift+Tab is pressed', () => {
+ mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
+ mockConfigInstance.setApprovalMode.mockImplementation(() => {
+ throw new Error(
+ 'Cannot enable privileged approval modes in an untrusted folder.',
+ );
+ });
+ const mockAddItem = vi.fn();
+ const { result } = renderHook(() =>
+ useAutoAcceptIndicator({
+ config: mockConfigInstance as unknown as ActualConfigType,
+ addItem: mockAddItem,
+ }),
+ );
+
+ expect(result.current).toBe(ApprovalMode.DEFAULT);
+
+ act(() => {
+ capturedUseKeypressHandler({
+ name: 'tab',
+ shift: true,
+ } as Key);
+ });
+
+ // We expect setApprovalMode to be called, and the error to be caught.
+ expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
+ ApprovalMode.AUTO_EDIT,
+ );
+ expect(mockAddItem).toHaveBeenCalled();
+ // Verify the underlying config value was not changed
+ expect(mockConfigInstance.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
+ });
+
+ it('should disable YOLO mode when Ctrl+Y is pressed', () => {
+ mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.YOLO);
+ const mockAddItem = vi.fn();
+ renderHook(() =>
+ useAutoAcceptIndicator({
+ config: mockConfigInstance as unknown as ActualConfigType,
+ addItem: mockAddItem,
+ }),
+ );
+
+ act(() => {
+ capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
+ });
+
+ expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
+ ApprovalMode.DEFAULT,
+ );
+ expect(mockConfigInstance.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
+ });
+
+ it('should disable AUTO_EDIT mode when Shift+Tab is pressed', () => {
+ mockConfigInstance.getApprovalMode.mockReturnValue(
+ ApprovalMode.AUTO_EDIT,
+ );
+ const mockAddItem = vi.fn();
+ renderHook(() =>
+ useAutoAcceptIndicator({
+ config: mockConfigInstance as unknown as ActualConfigType,
+ addItem: mockAddItem,
+ }),
+ );
+
+ act(() => {
+ capturedUseKeypressHandler({
+ name: 'tab',
+ shift: true,
+ } as Key);
+ });
+
+ expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
+ ApprovalMode.DEFAULT,
+ );
+ expect(mockConfigInstance.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
+ });
+
+ it('should show a warning when trying to enable privileged modes', () => {
+ // Mock the error thrown by setApprovalMode
+ const errorMessage =
+ 'Cannot enable privileged approval modes in an untrusted folder.';
+ mockConfigInstance.setApprovalMode.mockImplementation(() => {
+ throw new Error(errorMessage);
+ });
+
+ const mockAddItem = vi.fn();
+ renderHook(() =>
+ useAutoAcceptIndicator({
+ config: mockConfigInstance as unknown as ActualConfigType,
+ addItem: mockAddItem,
+ }),
+ );
+
+ // Try to enable YOLO mode
+ act(() => {
+ capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
+ });
+
+ expect(mockAddItem).toHaveBeenCalledWith(
+ {
+ type: MessageType.INFO,
+ text: errorMessage,
+ },
+ expect.any(Number),
+ );
+
+ // Try to enable AUTO_EDIT mode
+ act(() => {
+ capturedUseKeypressHandler({
+ name: 'tab',
+ shift: true,
+ } as Key);
+ });
+
+ expect(mockAddItem).toHaveBeenCalledWith(
+ {
+ type: MessageType.INFO,
+ text: errorMessage,
+ },
+ expect.any(Number),
+ );
+
+ expect(mockAddItem).toHaveBeenCalledTimes(2);
+ });
+ });
});
diff --git a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts
index 2cc16077..8766a2db 100644
--- a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts
+++ b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts
@@ -7,13 +7,17 @@
import { useState, useEffect } from 'react';
import { ApprovalMode, type Config } from '@google/gemini-cli-core';
import { useKeypress } from './useKeypress.js';
+import type { HistoryItemWithoutId } from '../types.js';
+import { MessageType } from '../types.js';
export interface UseAutoAcceptIndicatorArgs {
config: Config;
+ addItem: (item: HistoryItemWithoutId, timestamp: number) => void;
}
export function useAutoAcceptIndicator({
config,
+ addItem,
}: UseAutoAcceptIndicatorArgs): ApprovalMode {
const currentConfigValue = config.getApprovalMode();
const [showAutoAcceptIndicator, setShowAutoAcceptIndicator] =
@@ -40,9 +44,19 @@ export function useAutoAcceptIndicator({
}
if (nextApprovalMode) {
- config.setApprovalMode(nextApprovalMode);
- // Update local state immediately for responsiveness
- setShowAutoAcceptIndicator(nextApprovalMode);
+ try {
+ config.setApprovalMode(nextApprovalMode);
+ // Update local state immediately for responsiveness
+ setShowAutoAcceptIndicator(nextApprovalMode);
+ } catch (e) {
+ addItem(
+ {
+ type: MessageType.INFO,
+ text: (e as Error).message,
+ },
+ Date.now(),
+ );
+ }
}
},
{ isActive: true },
diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts
index e5d15d76..a37a9fe9 100644
--- a/packages/core/src/config/config.test.ts
+++ b/packages/core/src/config/config.test.ts
@@ -7,7 +7,7 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { Mock } from 'vitest';
import type { ConfigParameters, SandboxConfig } from './config.js';
-import { Config } from './config.js';
+import { Config, ApprovalMode } from './config.js';
import * as path from 'node:path';
import { setGeminiMdFilename as mockSetGeminiMdFilename } from '../tools/memoryTool.js';
import {
@@ -630,3 +630,59 @@ describe('Server Config (config.ts)', () => {
});
});
});
+
+describe('setApprovalMode with folder trust', () => {
+ it('should throw an error when setting YOLO mode in an untrusted folder', () => {
+ const config = new Config({
+ sessionId: 'test',
+ targetDir: '.',
+ debugMode: false,
+ model: 'test-model',
+ cwd: '.',
+ trustedFolder: false, // Untrusted
+ });
+ expect(() => config.setApprovalMode(ApprovalMode.YOLO)).toThrow(
+ 'Cannot enable privileged approval modes in an untrusted folder.',
+ );
+ });
+
+ it('should throw an error when setting AUTO_EDIT mode in an untrusted folder', () => {
+ const config = new Config({
+ sessionId: 'test',
+ targetDir: '.',
+ debugMode: false,
+ model: 'test-model',
+ cwd: '.',
+ trustedFolder: false, // Untrusted
+ });
+ expect(() => config.setApprovalMode(ApprovalMode.AUTO_EDIT)).toThrow(
+ 'Cannot enable privileged approval modes in an untrusted folder.',
+ );
+ });
+
+ it('should NOT throw an error when setting DEFAULT mode in an untrusted folder', () => {
+ const config = new Config({
+ sessionId: 'test',
+ targetDir: '.',
+ debugMode: false,
+ model: 'test-model',
+ cwd: '.',
+ trustedFolder: false, // Untrusted
+ });
+ expect(() => config.setApprovalMode(ApprovalMode.DEFAULT)).not.toThrow();
+ });
+
+ it('should NOT throw an error when setting any mode in a trusted folder', () => {
+ const config = new Config({
+ sessionId: 'test',
+ targetDir: '.',
+ debugMode: false,
+ model: 'test-model',
+ cwd: '.',
+ trustedFolder: true, // Trusted
+ });
+ expect(() => config.setApprovalMode(ApprovalMode.YOLO)).not.toThrow();
+ expect(() => config.setApprovalMode(ApprovalMode.AUTO_EDIT)).not.toThrow();
+ expect(() => config.setApprovalMode(ApprovalMode.DEFAULT)).not.toThrow();
+ });
+});
diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts
index 41759afc..4b2f508d 100644
--- a/packages/core/src/config/config.ts
+++ b/packages/core/src/config/config.ts
@@ -564,6 +564,11 @@ export class Config {
}
setApprovalMode(mode: ApprovalMode): void {
+ if (!this.isTrustedFolder() && mode !== ApprovalMode.DEFAULT) {
+ throw new Error(
+ 'Cannot enable privileged approval modes in an untrusted folder.',
+ );
+ }
this.approvalMode = mode;
}