From 8e9ce3f4c35edd27c2618d953dce57faff58a002 Mon Sep 17 00:00:00 2001 From: christine betts Date: Tue, 24 Feb 2026 13:13:21 -0500 Subject: [PATCH] Fix extension env dir loading issue (#20198) --- .../extensions/extensionSettings.test.ts | 43 +++++++++++++++++++ .../config/extensions/extensionSettings.ts | 27 ++++++++++-- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/config/extensions/extensionSettings.test.ts b/packages/cli/src/config/extensions/extensionSettings.test.ts index ef066977a1..bdbbdb2401 100644 --- a/packages/cli/src/config/extensions/extensionSettings.test.ts +++ b/packages/cli/src/config/extensions/extensionSettings.test.ts @@ -590,6 +590,29 @@ describe('extensionSettings', () => { SENSITIVE_VAR: 'workspace-secret', }); }); + + it('should ignore .env if it is a directory', async () => { + const workspaceEnvPath = path.join( + tempWorkspaceDir, + EXTENSION_SETTINGS_FILENAME, + ); + fs.mkdirSync(workspaceEnvPath); + const workspaceKeychain = new KeychainTokenStorage( + `Gemini CLI Extensions test-ext 12345 ${tempWorkspaceDir}`, + ); + await workspaceKeychain.setSecret('SENSITIVE_VAR', 'workspace-secret'); + + const contents = await getScopedEnvContents( + config, + extensionId, + ExtensionSettingScope.WORKSPACE, + tempWorkspaceDir, + ); + + expect(contents).toEqual({ + SENSITIVE_VAR: 'workspace-secret', + }); + }); }); describe('getEnvContents (merged)', () => { @@ -696,6 +719,26 @@ describe('extensionSettings', () => { expect(actualContent).toContain('VAR1=new-workspace-value'); }); + it('should throw an error when trying to write to a workspace with a .env directory', async () => { + const workspaceEnvPath = path.join(tempWorkspaceDir, '.env'); + fs.mkdirSync(workspaceEnvPath); + + mockRequestSetting.mockResolvedValue('new-workspace-value'); + + await expect( + updateSetting( + config, + '12345', + 'VAR1', + mockRequestSetting, + ExtensionSettingScope.WORKSPACE, + tempWorkspaceDir, + ), + ).rejects.toThrow( + /Cannot write extension settings to .* because it is a directory./, + ); + }); + it('should update a sensitive setting in USER scope', async () => { mockRequestSetting.mockResolvedValue('new-value2'); diff --git a/packages/cli/src/config/extensions/extensionSettings.ts b/packages/cli/src/config/extensions/extensionSettings.ts index 06e4f49db4..700d854e20 100644 --- a/packages/cli/src/config/extensions/extensionSettings.ts +++ b/packages/cli/src/config/extensions/extensionSettings.ts @@ -124,6 +124,15 @@ export async function maybePromptForSettings( const envContent = formatEnvContent(nonSensitiveSettings); + if (fsSync.existsSync(envFilePath)) { + const stat = fsSync.statSync(envFilePath); + if (stat.isDirectory()) { + throw new Error( + `Cannot write extension settings to ${envFilePath} because it is a directory.`, + ); + } + } + await fs.writeFile(envFilePath, envContent); } @@ -173,8 +182,11 @@ export async function getScopedEnvContents( const envFilePath = getEnvFilePath(extensionName, scope, workspaceDir); let customEnv: Record = {}; if (fsSync.existsSync(envFilePath)) { - const envFile = fsSync.readFileSync(envFilePath, 'utf-8'); - customEnv = dotenv.parse(envFile); + const stat = fsSync.statSync(envFilePath); + if (!stat.isDirectory()) { + const envFile = fsSync.readFileSync(envFilePath, 'utf-8'); + customEnv = dotenv.parse(envFile); + } } if (extensionConfig.settings) { @@ -260,6 +272,12 @@ export async function updateSetting( const envFilePath = getEnvFilePath(extensionName, scope, workspaceDir); let envContent = ''; if (fsSync.existsSync(envFilePath)) { + const stat = fsSync.statSync(envFilePath); + if (stat.isDirectory()) { + throw new Error( + `Cannot write extension settings to ${envFilePath} because it is a directory.`, + ); + } envContent = await fs.readFile(envFilePath, 'utf-8'); } @@ -324,7 +342,10 @@ async function clearSettings( keychain: KeychainTokenStorage, ) { if (fsSync.existsSync(envFilePath)) { - await fs.writeFile(envFilePath, ''); + const stat = fsSync.statSync(envFilePath); + if (!stat.isDirectory()) { + await fs.writeFile(envFilePath, ''); + } } if (!(await keychain.isAvailable())) { return;