feat(cli): secure .env loading and enforce workspace trust in headless mode (#25814)

Co-authored-by: galz10 <galzahavi@google.com>
Co-authored-by: davidapierce <davidapierce@google.com>
This commit is contained in:
Emily Hedlund
2026-04-23 09:09:14 -07:00
committed by GitHub
parent a007f64d20
commit dba9b9a0ff
27 changed files with 881 additions and 489 deletions
@@ -34,6 +34,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
...actual,
homedir: () => os.homedir(),
getCompatibilityWarnings: vi.fn().mockReturnValue([]),
isHeadlessMode: vi.fn().mockReturnValue(false),
WarningPriority: {
Low: 'low',
High: 'high',
@@ -143,6 +144,51 @@ describe('getUserStartupWarnings', () => {
});
});
describe('folder trust check', () => {
it('should throw FatalUntrustedWorkspaceError when untrusted in headless mode', async () => {
const { isHeadlessMode, FatalUntrustedWorkspaceError } = await import(
'@google/gemini-cli-core'
);
vi.mocked(isFolderTrustEnabled).mockReturnValue(true);
vi.mocked(isWorkspaceTrusted).mockImplementation(() => {
throw new FatalUntrustedWorkspaceError(
'Gemini CLI is not running in a trusted directory',
);
});
vi.mocked(isHeadlessMode).mockReturnValue(true);
await expect(
getUserStartupWarnings({}, testRootDir),
).rejects.toThrowError(FatalUntrustedWorkspaceError);
});
it('should not return a warning when trusted in headless mode', async () => {
const { isHeadlessMode } = await import('@google/gemini-cli-core');
vi.mocked(isFolderTrustEnabled).mockReturnValue(true);
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: true,
source: 'file',
});
vi.mocked(isHeadlessMode).mockReturnValue(true);
const warnings = await getUserStartupWarnings({}, testRootDir);
expect(warnings.find((w) => w.id === 'folder-trust')).toBeUndefined();
});
it('should not return a warning when untrusted in interactive mode', async () => {
const { isHeadlessMode } = await import('@google/gemini-cli-core');
vi.mocked(isFolderTrustEnabled).mockReturnValue(true);
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: false,
source: undefined,
});
vi.mocked(isHeadlessMode).mockReturnValue(false);
const warnings = await getUserStartupWarnings({}, testRootDir);
expect(warnings.find((w) => w.id === 'folder-trust')).toBeUndefined();
});
});
describe('compatibility warnings', () => {
it('should include compatibility warnings by default', async () => {
const compWarning = {
@@ -12,6 +12,8 @@ import {
getCompatibilityWarnings,
WarningPriority,
type StartupWarning,
isHeadlessMode,
FatalUntrustedWorkspaceError,
} from '@google/gemini-cli-core';
import type { Settings } from '../config/settingsSchema.js';
import {
@@ -79,10 +81,34 @@ const rootDirectoryCheck: WarningCheck = {
},
};
const folderTrustCheck: WarningCheck = {
id: 'folder-trust',
priority: WarningPriority.High,
check: async (workspaceRoot: string, settings: Settings) => {
if (!isFolderTrustEnabled(settings)) {
return null;
}
const { isTrusted } = isWorkspaceTrusted(settings, workspaceRoot);
if (isTrusted === true) {
return null;
}
if (isHeadlessMode()) {
throw new FatalUntrustedWorkspaceError(
'Gemini CLI is not running in a trusted directory. To proceed, either use `--skip-trust`, set the `GEMINI_CLI_TRUST_WORKSPACE=true` environment variable, or trust this directory in interactive mode.',
);
}
return null;
},
};
// All warning checks
const WARNING_CHECKS: readonly WarningCheck[] = [
homeDirectoryCheck,
rootDirectoryCheck,
folderTrustCheck,
];
export async function getUserStartupWarnings(