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
+29 -12
View File
@@ -24,6 +24,7 @@ import {
FileDiscoveryService,
resolveTelemetrySettings,
FatalConfigError,
getErrorMessage,
getPty,
debugLogger,
loadServerHierarchicalMemory,
@@ -60,6 +61,7 @@ import {
import { loadSandboxConfig } from './sandboxConfig.js';
import { resolvePath } from '../utils/resolvePath.js';
import { isRecord } from '../utils/settingsUtils.js';
import { RESUME_LATEST } from '../utils/sessionUtils.js';
import { isWorkspaceTrusted } from './trustedFolders.js';
@@ -106,6 +108,7 @@ export interface CliArgs {
startupMessages?: string[];
rawOutput: boolean | undefined;
acceptRawOutputRisk: boolean | undefined;
skipTrust: boolean | undefined;
isCommand: boolean | undefined;
}
@@ -291,6 +294,11 @@ export async function parseArguments(
description:
'Execute the provided prompt and continue in interactive mode',
})
.option('skip-trust', {
type: 'boolean',
description: 'Trust the current workspace for this session.',
default: false,
})
.option('worktree', {
alias: 'w',
type: 'string',
@@ -459,9 +467,16 @@ export async function parseArguments(
yargsInstance.wrap(yargsInstance.terminalWidth());
let result;
try {
result = await yargsInstance.parse();
const parsed = await yargsInstance.parse();
if (!isRecord(parsed)) {
throw new Error('Failed to parse arguments');
}
result = parsed;
if (result['skip-trust']) {
process.env['GEMINI_CLI_TRUST_WORKSPACE'] = 'true';
}
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
const msg = getErrorMessage(e);
debugLogger.error(msg);
yargsInstance.showHelp();
await runExitCleanup();
@@ -475,11 +490,13 @@ export async function parseArguments(
}
// Normalize query args: handle both quoted "@path file" and unquoted @path file
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const queryArg = (result as { query?: string | string[] | undefined }).query;
const q: string | undefined = Array.isArray(queryArg)
? queryArg.join(' ')
: queryArg;
const queryArg = result['query'];
let q: string | undefined;
if (Array.isArray(queryArg)) {
q = queryArg.join(' ');
} else if (typeof queryArg === 'string') {
q = queryArg;
}
// -p/--prompt forces non-interactive mode; positional args default to interactive in TTY
if (q && !result['prompt']) {
@@ -494,8 +511,8 @@ export async function parseArguments(
}
// Keep CliArgs.query as a string for downstream typing
(result as Record<string, unknown>)['query'] = q || undefined;
(result as Record<string, unknown>)['startupMessages'] = startupMessages;
result['query'] = q || undefined;
result['startupMessages'] = startupMessages;
// The import format is now only controlled by settings.memoryImportFormat
// We no longer accept it as a CLI argument
@@ -547,7 +564,7 @@ export async function loadCliConfig(
? false
: (settings.security?.folderTrust?.enabled ?? false);
const trustedFolder =
isWorkspaceTrusted(settings, cwd, undefined, {
isWorkspaceTrusted(settings, cwd, {
prompt: argv.prompt,
query: argv.query,
})?.isTrusted ?? false;
@@ -593,7 +610,7 @@ export async function loadCliConfig(
return resolveToRealPath(trimmedPath) !== realCwd;
} catch (e) {
debugLogger.debug(
`[IDE] Skipping inaccessible workspace folder: ${trimmedPath} (${e instanceof Error ? e.message : String(e)})`,
`[IDE] Skipping inaccessible workspace folder: ${trimmedPath} (${getErrorMessage(e)})`,
);
return false;
}
@@ -1099,7 +1116,7 @@ async function resolveWorktreeSettings(
worktreeBaseSha = stdout.trim();
} catch (e: unknown) {
debugLogger.debug(
`Failed to resolve worktree base SHA at ${worktreePath}: ${e instanceof Error ? e.message : String(e)}`,
`Failed to resolve worktree base SHA at ${worktreePath}: ${getErrorMessage(e)}`,
);
}