feat(cli): enhance folder trust with configuration discovery and security warnings (#19492)

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Gal Zahavi
2026-02-20 10:21:03 -08:00
committed by GitHub
parent 960ebf5ae3
commit 37b45909ef
14 changed files with 994 additions and 49 deletions
@@ -8,14 +8,25 @@ import { Box, Text } from 'ink';
import type React from 'react';
import { useEffect, useState, useCallback } from 'react';
import { theme } from '../semantic-colors.js';
import stripAnsi from 'strip-ansi';
import type { RadioSelectItem } from './shared/RadioButtonSelect.js';
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
import { MaxSizedBox } from './shared/MaxSizedBox.js';
import { Scrollable } from './shared/Scrollable.js';
import { useKeypress } from '../hooks/useKeypress.js';
import * as process from 'node:process';
import * as path from 'node:path';
import { relaunchApp } from '../../utils/processUtils.js';
import { runExitCleanup } from '../../utils/cleanup.js';
import { ExitCodes } from '@google/gemini-cli-core';
import {
ExitCodes,
type FolderDiscoveryResults,
} from '@google/gemini-cli-core';
import { useUIState } from '../contexts/UIStateContext.js';
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
import { OverflowProvider } from '../contexts/OverflowContext.js';
import { ShowMoreLines } from './ShowMoreLines.js';
import { StickyHeader } from './StickyHeader.js';
export enum FolderTrustChoice {
TRUST_FOLDER = 'trust_folder',
@@ -26,13 +37,19 @@ export enum FolderTrustChoice {
interface FolderTrustDialogProps {
onSelect: (choice: FolderTrustChoice) => void;
isRestarting?: boolean;
discoveryResults?: FolderDiscoveryResults | null;
}
export const FolderTrustDialog: React.FC<FolderTrustDialogProps> = ({
onSelect,
isRestarting,
discoveryResults,
}) => {
const [exiting, setExiting] = useState(false);
const { terminalHeight, terminalWidth, constrainHeight } = useUIState();
const isAlternateBuffer = useAlternateBuffer();
const isExpanded = !constrainHeight;
useEffect(() => {
let timer: ReturnType<typeof setTimeout>;
@@ -87,48 +104,214 @@ export const FolderTrustDialog: React.FC<FolderTrustDialogProps> = ({
},
];
return (
<Box flexDirection="column" width="100%">
<Box
flexDirection="column"
borderStyle="round"
borderColor={theme.status.warning}
padding={1}
marginLeft={1}
marginRight={1}
>
<Box flexDirection="column" marginBottom={1}>
<Text bold color={theme.text.primary}>
Do you trust this folder?
</Text>
<Text color={theme.text.primary}>
Trusting a folder allows Gemini to execute commands it suggests.
This is a security feature to prevent accidental execution in
untrusted directories.
</Text>
</Box>
const hasDiscovery =
discoveryResults &&
(discoveryResults.commands.length > 0 ||
discoveryResults.mcps.length > 0 ||
discoveryResults.hooks.length > 0 ||
discoveryResults.skills.length > 0 ||
discoveryResults.settings.length > 0);
<RadioButtonSelect
items={options}
onSelect={onSelect}
isFocused={!isRestarting}
/>
const hasWarnings =
discoveryResults && discoveryResults.securityWarnings.length > 0;
const hasErrors =
discoveryResults &&
discoveryResults.discoveryErrors &&
discoveryResults.discoveryErrors.length > 0;
const dialogWidth = terminalWidth - 2;
const borderColor = theme.status.warning;
// Header: 3 lines
// Options: options.length + 2 lines for margins
// Footer: 1 line
// Safety margin: 2 lines
const overhead = 3 + options.length + 2 + 1 + 2;
const scrollableHeight = Math.max(4, terminalHeight - overhead);
const groups = [
{ label: 'Commands', items: discoveryResults?.commands ?? [] },
{ label: 'MCP Servers', items: discoveryResults?.mcps ?? [] },
{ label: 'Hooks', items: discoveryResults?.hooks ?? [] },
{ label: 'Skills', items: discoveryResults?.skills ?? [] },
{ label: 'Setting overrides', items: discoveryResults?.settings ?? [] },
].filter((g) => g.items.length > 0);
const discoveryContent = (
<Box flexDirection="column">
<Box marginBottom={1}>
<Text color={theme.text.primary}>
Trusting a folder allows Gemini CLI to load its local configurations,
including custom commands, hooks, MCP servers, agent skills, and
settings. These configurations could execute code on your behalf or
change the behavior of the CLI.
</Text>
</Box>
{isRestarting && (
<Box marginLeft={1} marginTop={1}>
<Text color={theme.status.warning}>
Gemini CLI is restarting to apply the trust changes...
{hasErrors && (
<Box flexDirection="column" marginBottom={1}>
<Text color={theme.status.error} bold>
Discovery Errors:
</Text>
{discoveryResults.discoveryErrors.map((error, i) => (
<Box key={i} marginLeft={2}>
<Text color={theme.status.error}> {stripAnsi(error)}</Text>
</Box>
))}
</Box>
)}
{exiting && (
<Box marginLeft={1} marginTop={1}>
<Text color={theme.status.warning}>
A folder trust level must be selected to continue. Exiting since
escape was pressed.
{hasWarnings && (
<Box flexDirection="column" marginBottom={1}>
<Text color={theme.status.warning} bold>
Security Warnings:
</Text>
{discoveryResults.securityWarnings.map((warning, i) => (
<Box key={i} marginLeft={2}>
<Text color={theme.status.warning}> {stripAnsi(warning)}</Text>
</Box>
))}
</Box>
)}
{hasDiscovery && (
<Box flexDirection="column" marginBottom={1}>
<Text color={theme.text.primary} bold>
This folder contains:
</Text>
{groups.map((group) => (
<Box key={group.label} flexDirection="column" marginLeft={2}>
<Text color={theme.text.primary} bold>
{group.label} ({group.items.length}):
</Text>
{group.items.map((item, idx) => (
<Box key={idx} marginLeft={2}>
<Text color={theme.text.primary}>- {stripAnsi(item)}</Text>
</Box>
))}
</Box>
))}
</Box>
)}
</Box>
);
const title = (
<Text bold color={theme.text.primary}>
Do you trust the files in this folder?
</Text>
);
const selectOptions = (
<RadioButtonSelect
items={options}
onSelect={onSelect}
isFocused={!isRestarting}
/>
);
const renderContent = () => {
if (isAlternateBuffer) {
return (
<Box flexDirection="column" width={dialogWidth}>
<StickyHeader
width={dialogWidth}
isFirst={true}
borderColor={borderColor}
borderDimColor={false}
>
{title}
</StickyHeader>
<Box
flexDirection="column"
borderLeft={true}
borderRight={true}
borderColor={borderColor}
borderStyle="round"
borderTop={false}
borderBottom={false}
width={dialogWidth}
>
<Scrollable
hasFocus={!isRestarting}
height={scrollableHeight}
width={dialogWidth - 2}
>
<Box flexDirection="column" paddingX={1}>
{discoveryContent}
</Box>
</Scrollable>
<Box paddingX={1} marginY={1}>
{selectOptions}
</Box>
</Box>
<Box
height={0}
width={dialogWidth}
borderLeft={true}
borderRight={true}
borderTop={false}
borderBottom={true}
borderColor={borderColor}
borderStyle="round"
/>
</Box>
);
}
return (
<Box
flexDirection="column"
borderStyle="round"
borderColor={borderColor}
padding={1}
width="100%"
>
<Box marginBottom={1}>{title}</Box>
<MaxSizedBox
maxHeight={isExpanded ? undefined : Math.max(4, terminalHeight - 12)}
overflowDirection="bottom"
>
{discoveryContent}
</MaxSizedBox>
<Box marginTop={1}>{selectOptions}</Box>
</Box>
);
};
return (
<OverflowProvider>
<Box flexDirection="column" width="100%">
<Box flexDirection="column" marginLeft={1} marginRight={1}>
{renderContent()}
</Box>
<Box paddingX={2} marginBottom={1}>
<ShowMoreLines constrainHeight={constrainHeight} />
</Box>
{isRestarting && (
<Box marginLeft={1} marginTop={1}>
<Text color={theme.status.warning}>
Gemini CLI is restarting to apply the trust changes...
</Text>
</Box>
)}
{exiting && (
<Box marginLeft={1} marginTop={1}>
<Text color={theme.status.warning}>
A folder trust level must be selected to continue. Exiting since
escape was pressed.
</Text>
</Box>
)}
</Box>
</OverflowProvider>
);
};