From 9026adce4d6ed52ed72fe91d01f63f7fb56fd994 Mon Sep 17 00:00:00 2001 From: galz10 Date: Wed, 11 Feb 2026 15:28:57 -0800 Subject: [PATCH 1/2] fix(core): improve headless mode detection for flags and query args --- packages/cli/src/config/config.ts | 9 ++-- .../cli/src/config/trustedFolders.test.ts | 8 +++ packages/cli/src/config/trustedFolders.ts | 21 ++++++-- packages/core/src/utils/headless.test.ts | 52 ++++++++++++++++--- packages/core/src/utils/headless.ts | 29 ++++++++--- 5 files changed, 99 insertions(+), 20 deletions(-) diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 87eb1e8fa7d..f164ce77d8d 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -445,7 +445,11 @@ export async function loadCliConfig( process.env['VITEST'] === 'true' ? false : (settings.security?.folderTrust?.enabled ?? false); - const trustedFolder = isWorkspaceTrusted(settings, cwd)?.isTrusted ?? false; + const trustedFolder = + isWorkspaceTrusted(settings, cwd, undefined, { + prompt: argv.prompt, + query: argv.query, + })?.isTrusted ?? false; // Set the context filename in the server's memoryTool module BEFORE loading memory // TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed @@ -602,8 +606,7 @@ export async function loadCliConfig( const interactive = !!argv.promptInteractive || !!argv.experimentalAcp || - (!isHeadlessMode({ prompt: argv.prompt }) && - !argv.query && + (!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) && !argv.isCommand); const allowedTools = argv.allowedTools || settings.tools?.allowed || []; diff --git a/packages/cli/src/config/trustedFolders.test.ts b/packages/cli/src/config/trustedFolders.test.ts index dff4610b907..892cd86e4b2 100644 --- a/packages/cli/src/config/trustedFolders.test.ts +++ b/packages/cli/src/config/trustedFolders.test.ts @@ -449,6 +449,14 @@ describe('Trusted Folders', () => { false, ); }); + + it('should return true for isPathTrusted when isHeadlessMode is true', async () => { + const geminiCore = await import('@google/gemini-cli-core'); + vi.spyOn(geminiCore, 'isHeadlessMode').mockReturnValue(true); + + const folders = loadTrustedFolders(); + expect(folders.isPathTrusted('/any-untrusted-path')).toBe(true); + }); }); describe('Trusted Folders Caching', () => { diff --git a/packages/cli/src/config/trustedFolders.ts b/packages/cli/src/config/trustedFolders.ts index 1f85684900c..761bc368d3f 100644 --- a/packages/cli/src/config/trustedFolders.ts +++ b/packages/cli/src/config/trustedFolders.ts @@ -17,6 +17,7 @@ import { homedir, isHeadlessMode, coreEvents, + type HeadlessModeOptions, } from '@google/gemini-cli-core'; import type { Settings } from './settings.js'; import stripJsonComments from 'strip-json-comments'; @@ -128,7 +129,11 @@ export class LoadedTrustedFolders { isPathTrusted( location: string, config?: Record, + headlessOptions?: HeadlessModeOptions, ): boolean | undefined { + if (isHeadlessMode(headlessOptions)) { + return true; + } const configToUse = config ?? this.user.config; // Resolve location to its realpath for canonical comparison @@ -333,6 +338,7 @@ export function isFolderTrustEnabled(settings: Settings): boolean { function getWorkspaceTrustFromLocalConfig( workspaceDir: string, trustConfig?: Record, + headlessOptions?: HeadlessModeOptions, ): TrustResult { const folders = loadTrustedFolders(); const configToUse = trustConfig ?? folders.user.config; @@ -346,7 +352,11 @@ function getWorkspaceTrustFromLocalConfig( ); } - const isTrusted = folders.isPathTrusted(workspaceDir, configToUse); + const isTrusted = folders.isPathTrusted( + workspaceDir, + configToUse, + headlessOptions, + ); return { isTrusted, source: isTrusted !== undefined ? 'file' : undefined, @@ -357,8 +367,9 @@ export function isWorkspaceTrusted( settings: Settings, workspaceDir: string = process.cwd(), trustConfig?: Record, + headlessOptions?: HeadlessModeOptions, ): TrustResult { - if (isHeadlessMode()) { + if (isHeadlessMode(headlessOptions)) { return { isTrusted: true, source: undefined }; } @@ -372,5 +383,9 @@ export function isWorkspaceTrusted( } // Fall back to the local user configuration - return getWorkspaceTrustFromLocalConfig(workspaceDir, trustConfig); + return getWorkspaceTrustFromLocalConfig( + workspaceDir, + trustConfig, + headlessOptions, + ); } diff --git a/packages/core/src/utils/headless.test.ts b/packages/core/src/utils/headless.test.ts index 89f42ffcd60..4708c79969d 100644 --- a/packages/core/src/utils/headless.test.ts +++ b/packages/core/src/utils/headless.test.ts @@ -99,16 +99,50 @@ describe('isHeadlessMode', () => { expect(isHeadlessMode({ prompt: true })).toBe(true); }); - it('should return false if query is provided but it is still a TTY', () => { - // Note: per current logic, query alone doesn't force headless if TTY - // This matches the existing behavior in packages/cli/src/config/config.ts - expect(isHeadlessMode({ query: 'test query' })).toBe(false); + it('should return true if query is provided', () => { + expect(isHeadlessMode({ query: 'test query' })).toBe(true); + }); + + it('should return true if -p or --prompt is in process.argv as a fallback', () => { + const originalArgv = process.argv; + process.argv = ['node', 'index.js', '-p', 'hello']; + try { + expect(isHeadlessMode()).toBe(true); + } finally { + process.argv = originalArgv; + } + + process.argv = ['node', 'index.js', '--prompt', 'hello']; + try { + expect(isHeadlessMode()).toBe(true); + } finally { + process.argv = originalArgv; + } + }); + + it('should return true if -y or --yolo is in process.argv as a fallback', () => { + const originalArgv = process.argv; + process.argv = ['node', 'index.js', '-y']; + try { + expect(isHeadlessMode()).toBe(true); + } finally { + process.argv = originalArgv; + } + + process.argv = ['node', 'index.js', '--yolo']; + try { + expect(isHeadlessMode()).toBe(true); + } finally { + process.argv = originalArgv; + } }); it('should handle undefined process.stdout gracefully', () => { const originalStdout = process.stdout; - // @ts-expect-error - testing edge case - delete process.stdout; + Object.defineProperty(process, 'stdout', { + value: undefined, + configurable: true, + }); try { expect(isHeadlessMode()).toBe(false); @@ -122,8 +156,10 @@ describe('isHeadlessMode', () => { it('should handle undefined process.stdin gracefully', () => { const originalStdin = process.stdin; - // @ts-expect-error - testing edge case - delete process.stdin; + Object.defineProperty(process, 'stdin', { + value: undefined, + configurable: true, + }); try { expect(isHeadlessMode()).toBe(false); diff --git a/packages/core/src/utils/headless.ts b/packages/core/src/utils/headless.ts index 27ea5f9cbfa..8a1be922aa9 100644 --- a/packages/core/src/utils/headless.ts +++ b/packages/core/src/utils/headless.ts @@ -29,17 +29,34 @@ export interface HeadlessModeOptions { */ export function isHeadlessMode(options?: HeadlessModeOptions): boolean { if (process.env['GEMINI_CLI_INTEGRATION_TEST'] === 'true') { - return ( + if ( !!options?.prompt || + !!options?.query || (!!process.stdin && !process.stdin.isTTY) || (!!process.stdout && !process.stdout.isTTY) + ) { + return true; + } + return process.argv.some( + (arg) => + arg === '-p' || arg === '--prompt' || arg === '-y' || arg === '--yolo', ); } - return ( - process.env['CI'] === 'true' || - process.env['GITHUB_ACTIONS'] === 'true' || - !!options?.prompt || + + const isCI = + process.env['CI'] === 'true' || process.env['GITHUB_ACTIONS'] === 'true'; + const isNotTTY = (!!process.stdin && !process.stdin.isTTY) || - (!!process.stdout && !process.stdout.isTTY) + (!!process.stdout && !process.stdout.isTTY); + + if (isCI || isNotTTY || !!options?.prompt || !!options?.query) { + return true; + } + + // Fallback: check process.argv for flags that imply headless or auto-approve mode. + const argv = process.argv; + return argv.some( + (arg) => + arg === '-p' || arg === '--prompt' || arg === '-y' || arg === '--yolo', ); } From 2c37147e663b2840aef1dcb6991d90616058b914 Mon Sep 17 00:00:00 2001 From: galz10 Date: Wed, 11 Feb 2026 15:50:18 -0800 Subject: [PATCH 2/2] chore: dedupped code --- packages/core/src/utils/headless.ts | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/packages/core/src/utils/headless.ts b/packages/core/src/utils/headless.ts index 8a1be922aa9..5a46b90d6d4 100644 --- a/packages/core/src/utils/headless.ts +++ b/packages/core/src/utils/headless.ts @@ -28,34 +28,24 @@ export interface HeadlessModeOptions { * @returns true if the environment is considered headless. */ export function isHeadlessMode(options?: HeadlessModeOptions): boolean { - if (process.env['GEMINI_CLI_INTEGRATION_TEST'] === 'true') { - if ( - !!options?.prompt || - !!options?.query || - (!!process.stdin && !process.stdin.isTTY) || - (!!process.stdout && !process.stdout.isTTY) - ) { + if (process.env['GEMINI_CLI_INTEGRATION_TEST'] !== 'true') { + const isCI = + process.env['CI'] === 'true' || process.env['GITHUB_ACTIONS'] === 'true'; + if (isCI) { return true; } - return process.argv.some( - (arg) => - arg === '-p' || arg === '--prompt' || arg === '-y' || arg === '--yolo', - ); } - const isCI = - process.env['CI'] === 'true' || process.env['GITHUB_ACTIONS'] === 'true'; const isNotTTY = (!!process.stdin && !process.stdin.isTTY) || (!!process.stdout && !process.stdout.isTTY); - if (isCI || isNotTTY || !!options?.prompt || !!options?.query) { + if (isNotTTY || !!options?.prompt || !!options?.query) { return true; } // Fallback: check process.argv for flags that imply headless or auto-approve mode. - const argv = process.argv; - return argv.some( + return process.argv.some( (arg) => arg === '-p' || arg === '--prompt' || arg === '-y' || arg === '--yolo', );