fix(patch): cherry-pick 58df1c6 to release/v0.30.0-pr-20374 [CONFLICTS] (#20567)

Co-authored-by: christine betts <chrstn@uw.edu>
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
This commit is contained in:
gemini-cli-robot
2026-02-27 12:55:17 -05:00
committed by GitHub
parent 176b2baeef
commit 0fc15382ae
7 changed files with 458 additions and 15 deletions
+62 -2
View File
@@ -163,7 +163,8 @@ Each server configuration supports the following properties:
- **`args`** (string[]): Command-line arguments for Stdio transport - **`args`** (string[]): Command-line arguments for Stdio transport
- **`headers`** (object): Custom HTTP headers when using `url` or `httpUrl` - **`headers`** (object): Custom HTTP headers when using `url` or `httpUrl`
- **`env`** (object): Environment variables for the server process. Values can - **`env`** (object): Environment variables for the server process. Values can
reference environment variables using `$VAR_NAME` or `${VAR_NAME}` syntax reference environment variables using `$VAR_NAME` or `${VAR_NAME}` syntax (all
platforms), or `%VAR_NAME%` (Windows only).
- **`cwd`** (string): Working directory for Stdio transport - **`cwd`** (string): Working directory for Stdio transport
- **`timeout`** (number): Request timeout in milliseconds (default: 600,000ms = - **`timeout`** (number): Request timeout in milliseconds (default: 600,000ms =
10 minutes) 10 minutes)
@@ -184,6 +185,63 @@ Each server configuration supports the following properties:
Service Account to impersonate. Used with Service Account to impersonate. Used with
`authProviderType: 'service_account_impersonation'`. `authProviderType: 'service_account_impersonation'`.
### Environment variable expansion
Gemini CLI automatically expands environment variables in the `env` block of
your MCP server configuration. This allows you to securely reference variables
defined in your shell or environment without hardcoding sensitive information
directly in your `settings.json` file.
The expansion utility supports:
- **POSIX/Bash syntax:** `$VARIABLE_NAME` or `${VARIABLE_NAME}` (supported on
all platforms)
- **Windows syntax:** `%VARIABLE_NAME%` (supported only when running on Windows)
If a variable is not defined in the current environment, it resolves to an empty
string.
**Example:**
```json
"env": {
"API_KEY": "$MY_EXTERNAL_TOKEN",
"LOG_LEVEL": "$LOG_LEVEL",
"TEMP_DIR": "%TEMP%"
}
```
### Security and environment sanitization
To protect your credentials, Gemini CLI performs environment sanitization when
spawning MCP server processes.
#### Automatic redaction
By default, the CLI redacts sensitive environment variables from the base
environment (inherited from the host process) to prevent unintended exposure to
third-party MCP servers. This includes:
- Core project keys: `GEMINI_API_KEY`, `GOOGLE_API_KEY`, etc.
- Variables matching sensitive patterns: `*TOKEN*`, `*SECRET*`, `*PASSWORD*`,
`*KEY*`, `*AUTH*`, `*CREDENTIAL*`.
- Certificates and private key patterns.
#### Explicit overrides
If an environment variable must be passed to an MCP server, you must explicitly
state it in the `env` property of the server configuration in `settings.json`.
Explicitly defined variables (including those from extensions) are trusted and
are **not** subjected to the automatic redaction process.
This follows the security principle that if a variable is explicitly configured
by the user for a specific server, it constitutes informed consent to share that
specific data with that server.
> **Note:** Even when explicitly defined, you should avoid hardcoding secrets.
> Instead, use environment variable expansion (e.g., `"MY_KEY": "$MY_KEY"`) to
> securely pull the value from your host environment at runtime.
### OAuth support for remote MCP servers ### OAuth support for remote MCP servers
The Gemini CLI supports OAuth 2.0 authentication for remote MCP servers using The Gemini CLI supports OAuth 2.0 authentication for remote MCP servers using
@@ -738,7 +796,9 @@ The MCP integration tracks several states:
- **Trust settings:** The `trust` option bypasses all confirmation dialogs. Use - **Trust settings:** The `trust` option bypasses all confirmation dialogs. Use
cautiously and only for servers you completely control cautiously and only for servers you completely control
- **Access tokens:** Be security-aware when configuring environment variables - **Access tokens:** Be security-aware when configuring environment variables
containing API keys or tokens containing API keys or tokens. See
[Security and environment sanitization](#security-and-environment-sanitization)
for details on how Gemini CLI protects your credentials.
- **Sandbox compatibility:** When using sandboxing, ensure MCP servers are - **Sandbox compatibility:** When using sandboxing, ensure MCP servers are
available within the sandbox environment available within the sandbox environment
- **Private data:** Using broadly scoped personal access tokens can lead to - **Private data:** Using broadly scoped personal access tokens can lead to
+32 -3
View File
@@ -7475,9 +7475,36 @@
} }
}, },
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "17.1.0", "version": "17.2.4",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.1.0.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.4.tgz",
"integrity": "sha512-tG9VUTJTuju6GcXgbdsOuRhupE8cb4mRgY5JLRCh4MtGoVo3/gfGUtOMwmProM6d0ba2mCFvv+WrpYJV6qgJXQ==", "integrity": "sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dotenv-expand": {
"version": "12.0.3",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz",
"integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==",
"license": "BSD-2-Clause",
"dependencies": {
"dotenv": "^16.4.5"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dotenv-expand/node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@@ -17444,6 +17471,8 @@
"ajv-formats": "^3.0.0", "ajv-formats": "^3.0.0",
"chardet": "^2.1.0", "chardet": "^2.1.0",
"diff": "^8.0.3", "diff": "^8.0.3",
"dotenv": "^17.2.4",
"dotenv-expand": "^12.0.3",
"fast-levenshtein": "^2.0.6", "fast-levenshtein": "^2.0.6",
"fdir": "^6.4.6", "fdir": "^6.4.6",
"fzf": "^0.5.2", "fzf": "^0.5.2",
+2
View File
@@ -53,6 +53,8 @@
"ajv-formats": "^3.0.0", "ajv-formats": "^3.0.0",
"chardet": "^2.1.0", "chardet": "^2.1.0",
"diff": "^8.0.3", "diff": "^8.0.3",
"dotenv": "^17.2.4",
"dotenv-expand": "^12.0.3",
"fast-levenshtein": "^2.0.6", "fast-levenshtein": "^2.0.6",
"fdir": "^6.4.6", "fdir": "^6.4.6",
"fzf": "^0.5.2", "fzf": "^0.5.2",
+108
View File
@@ -1696,6 +1696,114 @@ describe('mcp-client', () => {
expect(callArgs.env!['GEMINI_CLI_EXT_VAR']).toBeUndefined(); expect(callArgs.env!['GEMINI_CLI_EXT_VAR']).toBeUndefined();
}); });
it('should include extension settings with defined values in environment', async () => {
const mockedTransport = vi
.spyOn(SdkClientStdioLib, 'StdioClientTransport')
.mockReturnValue({} as SdkClientStdioLib.StdioClientTransport);
await createTransport(
'test-server',
{
command: 'test-command',
extension: {
name: 'test-ext',
resolvedSettings: [
{
envVar: 'GEMINI_CLI_EXT_VAR',
value: 'defined-value',
sensitive: false,
name: 'ext-setting',
},
],
version: '',
isActive: false,
path: '',
contextFiles: [],
id: '',
},
},
false,
EMPTY_CONFIG,
);
const callArgs = mockedTransport.mock.calls[0][0];
expect(callArgs.env).toBeDefined();
expect(callArgs.env!['GEMINI_CLI_EXT_VAR']).toBe('defined-value');
});
it('should resolve environment variables in mcpServerConfig.env using extension settings', async () => {
const mockedTransport = vi
.spyOn(SdkClientStdioLib, 'StdioClientTransport')
.mockReturnValue({} as SdkClientStdioLib.StdioClientTransport);
await createTransport(
'test-server',
{
command: 'test-command',
env: {
RESOLVED_VAR: '$GEMINI_CLI_EXT_VAR',
},
extension: {
name: 'test-ext',
resolvedSettings: [
{
envVar: 'GEMINI_CLI_EXT_VAR',
value: 'ext-value',
sensitive: false,
name: 'ext-setting',
},
],
version: '',
isActive: false,
path: '',
contextFiles: [],
id: '',
},
},
false,
EMPTY_CONFIG,
);
const callArgs = mockedTransport.mock.calls[0][0];
expect(callArgs.env).toBeDefined();
expect(callArgs.env!['GEMINI_CLI_EXT_VAR']).toBe('ext-value');
expect(callArgs.env!['RESOLVED_VAR']).toBe('ext-value');
});
it('should expand environment variables in mcpServerConfig.env and not redact them', async () => {
const mockedTransport = vi
.spyOn(SdkClientStdioLib, 'StdioClientTransport')
.mockReturnValue({} as SdkClientStdioLib.StdioClientTransport);
const originalEnv = process.env;
process.env = {
...originalEnv,
GEMINI_TEST_VAR: 'expanded-value',
};
try {
await createTransport(
'test-server',
{
command: 'test-command',
env: {
TEST_EXPANDED: 'Value is $GEMINI_TEST_VAR',
SECRET_KEY: 'intentional-secret-123',
},
},
false,
EMPTY_CONFIG,
);
const callArgs = mockedTransport.mock.calls[0][0];
expect(callArgs.env).toBeDefined();
expect(callArgs.env!['TEST_EXPANDED']).toBe('Value is expanded-value');
expect(callArgs.env!['SECRET_KEY']).toBe('intentional-secret-123');
} finally {
process.env = originalEnv;
}
});
describe('useGoogleCredentialProvider', () => { describe('useGoogleCredentialProvider', () => {
beforeEach(() => { beforeEach(() => {
// Mock GoogleAuth client // Mock GoogleAuth client
+83 -10
View File
@@ -34,7 +34,11 @@ import {
} from '@modelcontextprotocol/sdk/types.js'; } from '@modelcontextprotocol/sdk/types.js';
import { ApprovalMode, PolicyDecision } from '../policy/types.js'; import { ApprovalMode, PolicyDecision } from '../policy/types.js';
import { parse } from 'shell-quote'; import { parse } from 'shell-quote';
import type { Config, MCPServerConfig } from '../config/config.js'; import type {
Config,
MCPServerConfig,
GeminiCLIExtension,
} from '../config/config.js';
import { AuthProviderType } from '../config/config.js'; import { AuthProviderType } from '../config/config.js';
import { GoogleCredentialProvider } from '../mcp/google-auth-provider.js'; import { GoogleCredentialProvider } from '../mcp/google-auth-provider.js';
import { ServiceAccountImpersonationProvider } from '../mcp/sa-impersonation-provider.js'; import { ServiceAccountImpersonationProvider } from '../mcp/sa-impersonation-provider.js';
@@ -67,6 +71,7 @@ import {
sanitizeEnvironment, sanitizeEnvironment,
type EnvironmentSanitizationConfig, type EnvironmentSanitizationConfig,
} from '../services/environmentSanitization.js'; } from '../services/environmentSanitization.js';
import { expandEnvVars } from '../utils/envExpansion.js';
import { import {
GEMINI_CLI_IDENTIFICATION_ENV_VAR, GEMINI_CLI_IDENTIFICATION_ENV_VAR,
GEMINI_CLI_IDENTIFICATION_ENV_VAR_VALUE, GEMINI_CLI_IDENTIFICATION_ENV_VAR_VALUE,
@@ -727,14 +732,31 @@ async function handleAutomaticOAuth(
* *
* @param mcpServerConfig The MCP server configuration * @param mcpServerConfig The MCP server configuration
* @param headers Additional headers * @param headers Additional headers
* @param sanitizationConfig Configuration for environment sanitization
*/ */
function createTransportRequestInit( function createTransportRequestInit(
mcpServerConfig: MCPServerConfig, mcpServerConfig: MCPServerConfig,
headers: Record<string, string>, headers: Record<string, string>,
sanitizationConfig: EnvironmentSanitizationConfig,
): RequestInit { ): RequestInit {
const extensionEnv = getExtensionEnvironment(mcpServerConfig.extension);
const expansionEnv = { ...process.env, ...extensionEnv };
const sanitizedEnv = sanitizeEnvironment(expansionEnv, {
...sanitizationConfig,
enableEnvironmentVariableRedaction: true,
});
const expandedHeaders: Record<string, string> = {};
if (mcpServerConfig.headers) {
for (const [key, value] of Object.entries(mcpServerConfig.headers)) {
expandedHeaders[key] = expandEnvVars(value, sanitizedEnv);
}
}
return { return {
headers: { headers: {
...mcpServerConfig.headers, ...expandedHeaders,
...headers, ...headers,
}, },
}; };
@@ -768,12 +790,14 @@ function createAuthProvider(
* @param mcpServerName The name of the MCP server * @param mcpServerName The name of the MCP server
* @param mcpServerConfig The MCP server configuration * @param mcpServerConfig The MCP server configuration
* @param accessToken The OAuth access token * @param accessToken The OAuth access token
* @param sanitizationConfig Configuration for environment sanitization
* @returns The transport with OAuth token, or null if creation fails * @returns The transport with OAuth token, or null if creation fails
*/ */
async function createTransportWithOAuth( async function createTransportWithOAuth(
mcpServerName: string, mcpServerName: string,
mcpServerConfig: MCPServerConfig, mcpServerConfig: MCPServerConfig,
accessToken: string, accessToken: string,
sanitizationConfig: EnvironmentSanitizationConfig,
): Promise<StreamableHTTPClientTransport | SSEClientTransport | null> { ): Promise<StreamableHTTPClientTransport | SSEClientTransport | null> {
try { try {
const headers: Record<string, string> = { const headers: Record<string, string> = {
@@ -782,7 +806,11 @@ async function createTransportWithOAuth(
const transportOptions: const transportOptions:
| StreamableHTTPClientTransportOptions | StreamableHTTPClientTransportOptions
| SSEClientTransportOptions = { | SSEClientTransportOptions = {
requestInit: createTransportRequestInit(mcpServerConfig, headers), requestInit: createTransportRequestInit(
mcpServerConfig,
headers,
sanitizationConfig,
),
}; };
return createUrlTransport(mcpServerName, mcpServerConfig, transportOptions); return createUrlTransport(mcpServerName, mcpServerConfig, transportOptions);
@@ -1366,6 +1394,7 @@ async function showAuthRequiredMessage(serverName: string): Promise<never> {
* @param config The MCP server configuration * @param config The MCP server configuration
* @param accessToken The OAuth access token to use * @param accessToken The OAuth access token to use
* @param httpReturned404 Whether the HTTP transport returned 404 (indicating SSE-only server) * @param httpReturned404 Whether the HTTP transport returned 404 (indicating SSE-only server)
* @param sanitizationConfig Configuration for environment sanitization
*/ */
async function retryWithOAuth( async function retryWithOAuth(
client: Client, client: Client,
@@ -1373,6 +1402,7 @@ async function retryWithOAuth(
config: MCPServerConfig, config: MCPServerConfig,
accessToken: string, accessToken: string,
httpReturned404: boolean, httpReturned404: boolean,
sanitizationConfig: EnvironmentSanitizationConfig,
): Promise<void> { ): Promise<void> {
if (httpReturned404) { if (httpReturned404) {
// HTTP returned 404, only try SSE // HTTP returned 404, only try SSE
@@ -1393,6 +1423,7 @@ async function retryWithOAuth(
serverName, serverName,
config, config,
accessToken, accessToken,
sanitizationConfig,
); );
if (!httpTransport) { if (!httpTransport) {
throw new Error( throw new Error(
@@ -1672,6 +1703,7 @@ export async function connectToMcpServer(
mcpServerConfig, mcpServerConfig,
accessToken, accessToken,
httpReturned404, httpReturned404,
sanitizationConfig,
); );
return mcpClient; return mcpClient;
} else { } else {
@@ -1743,6 +1775,7 @@ export async function connectToMcpServer(
mcpServerName, mcpServerName,
mcpServerConfig, mcpServerConfig,
accessToken, accessToken,
sanitizationConfig,
); );
if (!oauthTransport) { if (!oauthTransport) {
throw new Error( throw new Error(
@@ -1890,7 +1923,11 @@ export async function createTransport(
const transportOptions: const transportOptions:
| StreamableHTTPClientTransportOptions | StreamableHTTPClientTransportOptions
| SSEClientTransportOptions = { | SSEClientTransportOptions = {
requestInit: createTransportRequestInit(mcpServerConfig, headers), requestInit: createTransportRequestInit(
mcpServerConfig,
headers,
sanitizationConfig,
),
authProvider, authProvider,
}; };
@@ -1898,15 +1935,37 @@ export async function createTransport(
} }
if (mcpServerConfig.command) { if (mcpServerConfig.command) {
const extensionEnv = getExtensionEnvironment(mcpServerConfig.extension);
const expansionEnv = { ...process.env, ...extensionEnv };
// 1. Sanitize the base process environment to prevent unintended leaks of system-wide secrets.
const sanitizedEnv = sanitizeEnvironment(expansionEnv, {
...sanitizationConfig,
enableEnvironmentVariableRedaction: true,
});
const finalEnv: Record<string, string> = {
[GEMINI_CLI_IDENTIFICATION_ENV_VAR]:
GEMINI_CLI_IDENTIFICATION_ENV_VAR_VALUE,
...extensionEnv,
};
for (const [key, value] of Object.entries(sanitizedEnv)) {
if (value !== undefined) {
finalEnv[key] = value;
}
}
// Expand and merge explicit environment variables from the MCP configuration.
if (mcpServerConfig.env) {
for (const [key, value] of Object.entries(mcpServerConfig.env)) {
finalEnv[key] = expandEnvVars(value, expansionEnv);
}
}
let transport: Transport = new StdioClientTransport({ let transport: Transport = new StdioClientTransport({
command: mcpServerConfig.command, command: mcpServerConfig.command,
args: mcpServerConfig.args || [], args: mcpServerConfig.args || [],
env: { env: finalEnv,
...sanitizeEnvironment(process.env, sanitizationConfig),
...(mcpServerConfig.env || {}),
[GEMINI_CLI_IDENTIFICATION_ENV_VAR]:
GEMINI_CLI_IDENTIFICATION_ENV_VAR_VALUE,
} as Record<string, string>,
cwd: mcpServerConfig.cwd, cwd: mcpServerConfig.cwd,
stderr: 'pipe', stderr: 'pipe',
}); });
@@ -1955,6 +2014,20 @@ interface NamedTool {
name?: string; name?: string;
} }
function getExtensionEnvironment(
extension?: GeminiCLIExtension,
): Record<string, string> {
const env: Record<string, string> = {};
if (extension?.resolvedSettings) {
for (const setting of extension.resolvedSettings) {
if (setting.value !== undefined) {
env[setting.envVar] = setting.value;
}
}
}
return env;
}
/** Visible for testing */ /** Visible for testing */
export function isEnabled( export function isEnabled(
funcDecl: NamedTool, funcDecl: NamedTool,
@@ -0,0 +1,117 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { expandEnvVars } from './envExpansion.js';
describe('expandEnvVars', () => {
const defaultEnv = {
USER: 'morty',
HOME: '/home/morty',
TEMP: 'C:\\Temp',
EMPTY: '',
};
describe('POSIX behavior (non-Windows)', () => {
beforeEach(() => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin');
});
afterEach(() => {
vi.restoreAllMocks();
});
it.each([
['$VAR (POSIX)', 'Hello $USER', defaultEnv, 'Hello morty'],
[
'${VAR} (POSIX)',
'Welcome to ${HOME}',
defaultEnv,
'Welcome to /home/morty',
],
[
'should NOT expand %VAR% on non-Windows',
'Data in %TEMP%',
defaultEnv,
'Data in %TEMP%',
],
[
'mixed formats (only POSIX expanded)',
'$USER lives in ${HOME} on %TEMP%',
defaultEnv,
'morty lives in /home/morty on %TEMP%',
],
[
'missing variables (POSIX only)',
'Missing $UNDEFINED and ${NONE} and %MISSING%',
defaultEnv,
'Missing and and %MISSING%',
],
[
'empty or undefined values',
'Value is "$EMPTY"',
defaultEnv,
'Value is ""',
],
[
'original string if no variables',
'No vars here',
defaultEnv,
'No vars here',
],
['literal values like "1234"', '1234', defaultEnv, '1234'],
['empty input string', '', defaultEnv, ''],
[
'complex paths',
'${HOME}/bin:$PATH',
{ ...defaultEnv, PATH: '/usr/bin' },
'/home/morty/bin:/usr/bin',
],
])('should handle %s', (_, input, env, expected) => {
expect(expandEnvVars(input, env)).toBe(expected);
});
});
describe('Windows behavior', () => {
beforeEach(() => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32');
});
afterEach(() => {
vi.restoreAllMocks();
});
it.each([
['$VAR (POSIX)', 'Hello $USER', defaultEnv, 'Hello morty'],
[
'${VAR} (POSIX)',
'Welcome to ${HOME}',
defaultEnv,
'Welcome to /home/morty',
],
[
'should expand %VAR% on Windows',
'Data in %TEMP%',
defaultEnv,
'Data in C:\\Temp',
],
[
'mixed formats (both expanded)',
'$USER lives in ${HOME} on %TEMP%',
defaultEnv,
'morty lives in /home/morty on C:\\Temp',
],
[
'missing variables (all expanded to empty)',
'Missing $UNDEFINED and ${NONE} and %MISSING%',
defaultEnv,
'Missing and and ',
],
])('should handle %s', (_, input, env, expected) => {
expect(expandEnvVars(input, env)).toBe(expected);
});
});
});
+54
View File
@@ -0,0 +1,54 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { expand } from 'dotenv-expand';
/**
* Expands environment variables in a string using the provided environment record.
* Uses the standard `dotenv-expand` library to handle expansion consistently with
* other tools.
*
* Supports POSIX/Bash syntax ($VAR, ${VAR}).
* Note: Windows syntax (%VAR%) is not natively supported by dotenv-expand.
*
* @param str - The string containing environment variable placeholders.
* @param env - A record of environment variable names and their values.
* @returns The string with environment variables expanded. Missing variables resolve to an empty string.
*/
export function expandEnvVars(
str: string,
env: Record<string, string | undefined>,
): string {
if (!str) return str;
// 1. Pre-process Windows-style variables (%VAR%) since dotenv-expand only handles POSIX ($VAR).
// We only do this on Windows to limit the blast radius and avoid conflicts with other
// systems where % might be a literal character (e.g. in URLs or shell commands).
const isWindows = process.platform === 'win32';
const processedStr = isWindows
? str.replace(/%(\w+)%/g, (_, name) => env[name] ?? '')
: str;
// 2. Use dotenv-expand for POSIX/Bash syntax ($VAR, ${VAR}).
// dotenv-expand is designed to process an object of key-value pairs (like a .env file).
// To expand a single string, we wrap it in an object with a temporary key.
const dummyKey = '__GCLI_EXPAND_TARGET__';
// Filter out undefined values to satisfy the Record<string, string> requirement safely
const processEnv: Record<string, string> = {};
for (const [key, value] of Object.entries(env)) {
if (value !== undefined) {
processEnv[key] = value;
}
}
const result = expand({
parsed: { [dummyKey]: processedStr },
processEnv,
});
return result.parsed?.[dummyKey] ?? '';
}