From 622f8c33df42c0d67c33499bf67b77cf7751a518 Mon Sep 17 00:00:00 2001 From: galz10 Date: Mon, 4 May 2026 17:00:26 -0700 Subject: [PATCH 1/2] security: implement hard blocklist for workspace environment variables to prevent RCE --- package-lock.json | 35 ++++++++++++++++++++-- packages/cli/src/config/settings.test.ts | 38 ++++++++++++++++++++++++ packages/cli/src/config/settings.ts | 31 +++++++++++++++++-- 3 files changed, 99 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9ced540f9aa..951b500b3f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -449,7 +449,8 @@ "version": "2.11.0", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", - "license": "(Apache-2.0 AND BSD-3-Clause)" + "license": "(Apache-2.0 AND BSD-3-Clause)", + "peer": true }, "node_modules/@bundled-es-modules/cookie": { "version": "2.0.1", @@ -1535,6 +1536,7 @@ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" @@ -2212,6 +2214,7 @@ "integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.2", @@ -2392,6 +2395,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -2441,6 +2445,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -2815,6 +2820,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -2848,6 +2854,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz", "integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0" @@ -2902,6 +2909,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0", @@ -4139,6 +4147,7 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4412,6 +4421,7 @@ "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", @@ -5187,6 +5197,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7304,7 +7315,8 @@ "version": "0.0.1581282", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/dezalgo": { "version": "1.0.4", @@ -7889,6 +7901,7 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -8499,6 +8512,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -9765,6 +9779,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -10024,6 +10039,7 @@ "resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.6.9.tgz", "integrity": "sha512-RL9sSiLQZECnjbmBwjIHOp8yVGdWF7C/uifg7ISv/e+F3nLNsfl7FdUFQs8iZARFMJAYxMFpxW6OW+HSt9drwQ==", "license": "MIT", + "peer": true, "dependencies": { "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.3", @@ -13799,6 +13815,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13809,6 +13826,7 @@ "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -15961,6 +15979,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -16183,7 +16202,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsx": { "version": "4.20.3", @@ -16191,6 +16211,7 @@ "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -16356,6 +16377,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16423,6 +16445,7 @@ "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/types": "8.35.0", @@ -16842,6 +16865,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -17412,6 +17436,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -17424,6 +17449,7 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -18062,6 +18088,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -18498,6 +18525,7 @@ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" @@ -18616,6 +18644,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts index eb7e991e6be..3508e3620ef 100644 --- a/packages/cli/src/config/settings.test.ts +++ b/packages/cli/src/config/settings.test.ts @@ -2165,6 +2165,44 @@ describe('Settings Loading and Merging', () => { process.argv = originalArgv; } }); + + it('always blocks restricted variables from workspace .env files', () => { + const workspaceEnvPath = path.resolve( + path.join(MOCK_WORKSPACE_DIR, '.env'), + ); + + vi.spyOn(trustedFolders, 'isWorkspaceTrusted').mockReturnValue({ + isTrusted: true, // Even if trusted! + source: 'file', + }); + + (mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) => path.resolve(p.toString()) === workspaceEnvPath); + + (fs.readFileSync as Mock).mockImplementation( + (p: fs.PathOrFileDescriptor) => { + if (path.resolve(p.toString()) === workspaceEnvPath) { + return 'GEMINI_CLI_IDE_SERVER_STDIO_COMMAND=malicious\nNODE_OPTIONS=--inspect\nSAFE_VAR=ok'; + } + return ''; + }, + ); + + delete process.env['GEMINI_CLI_IDE_SERVER_STDIO_COMMAND']; + delete process.env['NODE_OPTIONS']; + delete process.env['SAFE_VAR']; + + loadEnvironment(createTestMergedSettings(), MOCK_WORKSPACE_DIR); + + expect( + process.env['GEMINI_CLI_IDE_SERVER_STDIO_COMMAND'], + ).toBeUndefined(); + expect(process.env['NODE_OPTIONS']).toBeUndefined(); + expect(process.env['SAFE_VAR']).toBe('ok'); + expect(mockCoreEvents.emitFeedback).toHaveBeenCalledWith( + 'warning', + expect.stringContaining('Security Warning'), + ); + }); }); describe('migrateDeprecatedSettings', () => { diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index 5a52e5af3c7..0abc6c27180 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -86,6 +86,23 @@ export const DEFAULT_EXCLUDED_ENV_VARS = [ 'GEMINI_CLI_IDE_SERVER_STDIO_ARGS', ]; +/** + * These variables are NEVER allowed to be overridden by a workspace-level .env file + * due to security risks (e.g., RCE). + */ +export const RESTRICTED_WORKSPACE_ENV_VARS = new Set([ + 'GEMINI_CLI_IDE_SERVER_STDIO_COMMAND', + 'GEMINI_CLI_IDE_SERVER_STDIO_ARGS', + 'NODE_OPTIONS', + 'NODE_PATH', + 'PAGER', + 'EDITOR', + 'VISUAL', + 'SHELL', + 'LD_PRELOAD', + 'DYLD_INSERT_LIBRARIES', +]); + const AUTH_ENV_VAR_WHITELIST = [ 'GEMINI_API_KEY', 'GOOGLE_API_KEY', @@ -647,8 +664,18 @@ export function loadEnvironment( } // If it's a project .env file, skip loading excluded variables. - if (isProjectEnvFile && excludedVars.includes(key)) { - continue; + if (isProjectEnvFile) { + if (RESTRICTED_WORKSPACE_ENV_VARS.has(key)) { + coreEvents.emitFeedback( + 'warning', + `Security Warning: Workspace .env file attempted to set restricted environment variable "${key}". This was blocked for your safety.`, + ); + continue; + } + + if (excludedVars.includes(key)) { + continue; + } } // Load variable only if it's not already set in the environment. From b76023a8a1ad7b1bfff79ee1a488d8c3f2b43225 Mon Sep 17 00:00:00 2001 From: galz10 Date: Thu, 7 May 2026 12:32:46 -0700 Subject: [PATCH 2/2] security: harden environment variable blocklist and improve workspace detection --- packages/cli/src/config/settings.test.ts | 97 +++++++++++++++++++++++- packages/cli/src/config/settings.ts | 18 ++++- 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts index 3508e3620ef..28fffa75e42 100644 --- a/packages/cli/src/config/settings.test.ts +++ b/packages/cli/src/config/settings.test.ts @@ -2176,7 +2176,9 @@ describe('Settings Loading and Merging', () => { source: 'file', }); - (mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) => path.resolve(p.toString()) === workspaceEnvPath); + (mockFsExistsSync as Mock).mockImplementation( + (p: fs.PathLike) => path.resolve(p.toString()) === workspaceEnvPath, + ); (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { @@ -2203,6 +2205,99 @@ describe('Settings Loading and Merging', () => { expect.stringContaining('Security Warning'), ); }); + + it('blocks restricted variables case-insensitively', () => { + const workspaceEnvPath = path.resolve( + path.join(MOCK_WORKSPACE_DIR, '.env'), + ); + + (mockFsExistsSync as Mock).mockImplementation( + (p: fs.PathLike) => path.resolve(p.toString()) === workspaceEnvPath, + ); + + (fs.readFileSync as Mock).mockImplementation( + (p: fs.PathOrFileDescriptor) => { + if (path.resolve(p.toString()) === workspaceEnvPath) { + return 'node_options=--inspect\nShell=/bin/evil\npath=/tmp/bin'; + } + return ''; + }, + ); + + delete process.env['node_options']; + delete process.env['Shell']; + delete process.env['path']; + + loadEnvironment(createTestMergedSettings(), MOCK_WORKSPACE_DIR); + + expect(process.env['node_options']).toBeUndefined(); + expect(process.env['Shell']).toBeUndefined(); + expect(process.env['path']).toBeUndefined(); + expect(mockCoreEvents.emitFeedback).toHaveBeenCalledWith( + 'warning', + expect.stringContaining('Security Warning'), + ); + }); + + it('identifies project .env files robustly even if path contains .gemini', () => { + const trickyWorkspaceDir = path.resolve( + '/mock/home/user/my.gemini.tools', + ); + const trickyEnvPath = path.join(trickyWorkspaceDir, '.env'); + + (mockFsExistsSync as Mock).mockImplementation( + (p: fs.PathLike) => path.resolve(p.toString()) === trickyEnvPath, + ); + + (fs.readFileSync as Mock).mockImplementation( + (p: fs.PathOrFileDescriptor) => { + if (path.resolve(p.toString()) === trickyEnvPath) { + return 'NODE_OPTIONS=--inspect'; + } + return ''; + }, + ); + + delete process.env['NODE_OPTIONS']; + + loadEnvironment(createTestMergedSettings(), trickyWorkspaceDir); + + // It SHOULD be blocked because it's not the global .gemini/.env + expect(process.env['NODE_OPTIONS']).toBeUndefined(); + expect(mockCoreEvents.emitFeedback).toHaveBeenCalledWith( + 'warning', + expect.stringContaining('Security Warning'), + ); + }); + + it('blocks newly added dangerous variables (PATH, LD_LIBRARY_PATH, etc.)', () => { + const workspaceEnvPath = path.resolve( + path.join(MOCK_WORKSPACE_DIR, '.env'), + ); + + (mockFsExistsSync as Mock).mockImplementation( + (p: fs.PathLike) => path.resolve(p.toString()) === workspaceEnvPath, + ); + + (fs.readFileSync as Mock).mockImplementation( + (p: fs.PathOrFileDescriptor) => { + if (path.resolve(p.toString()) === workspaceEnvPath) { + return 'PATH=/tmp/evil\nLD_LIBRARY_PATH=/tmp/evil\nDYLD_LIBRARY_PATH=/tmp/evil'; + } + return ''; + }, + ); + + delete process.env['PATH']; + delete process.env['LD_LIBRARY_PATH']; + delete process.env['DYLD_LIBRARY_PATH']; + + loadEnvironment(createTestMergedSettings(), MOCK_WORKSPACE_DIR); + + expect(process.env['PATH']).toBeUndefined(); + expect(process.env['LD_LIBRARY_PATH']).toBeUndefined(); + expect(process.env['DYLD_LIBRARY_PATH']).toBeUndefined(); + }); }); describe('migrateDeprecatedSettings', () => { diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index 0abc6c27180..7fdd2ec44b0 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -100,7 +100,10 @@ export const RESTRICTED_WORKSPACE_ENV_VARS = new Set([ 'VISUAL', 'SHELL', 'LD_PRELOAD', + 'LD_LIBRARY_PATH', 'DYLD_INSERT_LIBRARIES', + 'DYLD_LIBRARY_PATH', + 'PATH', ]); const AUTH_ENV_VAR_WHITELIST = [ @@ -649,7 +652,15 @@ export function loadEnvironment( const excludedVars = settings?.advanced?.excludedEnvVars || DEFAULT_EXCLUDED_ENV_VARS; - const isProjectEnvFile = !envFilePath.includes(GEMINI_DIR); + + // Robust check for project vs global environment files. + // Global files (in the user's home directory) are exempt from the blocklist. + const globalGeminiEnv = path.join(homedir(), GEMINI_DIR, '.env'); + const globalHomeEnv = path.join(homedir(), '.env'); + const isGlobalEnvFile = + path.resolve(envFilePath) === path.resolve(globalGeminiEnv) || + path.resolve(envFilePath) === path.resolve(globalHomeEnv); + const isProjectEnvFile = !isGlobalEnvFile; for (const key in parsedEnv) { if (Object.hasOwn(parsedEnv, key)) { @@ -663,9 +674,10 @@ export function loadEnvironment( value = sanitizeEnvVar(value); } - // If it's a project .env file, skip loading excluded variables. + // If it's a project .env file, skip loading excluded variables and restricted ones. if (isProjectEnvFile) { - if (RESTRICTED_WORKSPACE_ENV_VARS.has(key)) { + const normalizedKey = key.toUpperCase(); + if (RESTRICTED_WORKSPACE_ENV_VARS.has(normalizedKey)) { coreEvents.emitFeedback( 'warning', `Security Warning: Workspace .env file attempted to set restricted environment variable "${key}". This was blocked for your safety.`,