Skip to content

fix(cli): load environment variables before resolving settings placeholders - #28597

Closed
WolfGreyDev wants to merge 1 commit into
google-gemini:mainfrom
WolfGreyDev:fix/settings-env-race-condition
Closed

WolfGreyDev wants to merge 1 commit into
google-gemini:mainfrom
WolfGreyDev:fix/settings-env-race-condition

Conversation

@WolfGreyDev

@WolfGreyDev WolfGreyDev commented Jul 30, 2026 •

Copy link
Copy Markdown

Summary

This PR resolves a load-order race condition in the settings lifecycle. Previously, settings files (system, user, workspace) were parsed, immediately expanded against process.env, and validated in a single monolithic step inside the load helper upon startup. However, the local .env files (which populate process.env from the project workspace) were only loaded after this step had already finished. This caused settings mapped to environment variables defined only in .env (such as $GITHUB_MCP_PAT) to remain unexpanded, leading to downstream authorization failures (e.g., a 400 Bad Format Authorization Header when communicating with GitHub Copilot's MCP server).

Details

The settings lifecycle inside _doLoadSettings has been refactored to decouple loading from variable expansion and validation:

  • Parse Raw Files: The load helper now strictly reads files from disk and parses the JSON content to produce raw, unexpanded configurations.
  • Initialize Environment: loadEnvironment executes using these raw settings to load and inject .env file variables into process.env.
  • Resolve & Validate: A new resolveAndValidate helper executes after environment variables have been fully initialized. This helper expands environment variable placeholders (now with fully loaded .env variables) and validates their final shapes against Zod schemas.

Before the Fix (Race Condition Bug)

Upon CLI initialization, the internal load() helper executes for all settings files:

  1. Settings files are parsed and immediately expanded against process.env.
  2. The placeholder $GITHUB_MCP_PAT is evaluated against process.env. Because the local workspace .env file has not been loaded yet, the variable is not found.
  3. The placeholder remains unexpanded:
    "headers": {
      "Authorization": "Bearer $GITHUB_MCP_PAT"
    }
  4. Only after this initial settings evaluation is completed does loadEnvironment() run to parse the .env file and populate process.env.
  5. The CLI attempts to establish a connection to the GitHub Copilot MCP server using the literal string Bearer $GITHUB_MCP_PAT, resulting in a 400 Bad Format Authorization Header error.

After the Fix (Decoupled Pipeline)

The settings loading sequence is refactored into a decoupled, multi-stage pipeline:

  1. Load Raw Files: The CLI loads and parses the settings files but defers environment variable resolution.
  2. Initialize Environment: loadEnvironment() runs using the raw configurations. It parses the local .env file and populates the process environment:
    process.env['GITHUB_MCP_PAT'] = 'ghp_secretTokenVal123'
  3. Resolve & Validate: The resolveAndValidate() helper executes. It resolves settings placeholders now that GITHUB_MCP_PAT is fully initialized in the environment.
  4. The header expands cleanly to:
    "headers": {
      "Authorization": "Bearer ghp_secretTokenVal123"
    }
  5. The CLI connects to the GitHub Copilot MCP server, authorizes successfully, and launches without error.

Related Issues

Fixes #28684

How to Validate

This change has been covered with both unit and integration-level tests:

  1. Unit tests:
    Run the modified settings unit test suite:

    npx vitest run packages/cli/src/config/settings.test.ts

    Expected: All 115 unit tests pass cleanly.

  2. Integration tests:
    Run the newly added integration test covering E2E .env resolution:

    npx vitest run integration-tests/settings.test.ts

    Expected: The test passes cleanly in a sandboxed, isolated environment.

Pre-Merge Checklist

  • Updated relevant documentation and README (if needed)
  • Added/updated tests (if needed)
  • Noted breaking changes (if any)
  • Validated on required platforms/methods:
    • MacOS
      • npm run
      • npx
      • Docker
      • Podman
      • Seatbelt
    • Windows
      • npm run
      • npx
      • Docker
    • Linux
      • npm run
      • npx
      • Docker

@WolfGreyDev
WolfGreyDev requested a review from a team as a code owner July 30, 2026 10:06
@google-cla

google-cla Bot commented Jul 30, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@github-actions github-actions Bot added the size/l A large sized PR label Jul 30, 2026
@github-actions

github-actions Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

📊 PR Size: size/L

  • Lines changed: 410
  • Additions: +315
  • Deletions: -95
  • Files changed: 3

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a race condition in the CLI settings initialization where environment variables defined in local .env files were not available during the initial settings expansion phase. By decoupling the loading, resolution, and validation steps, the CLI now correctly populates the environment before attempting to resolve placeholders in settings files, ensuring proper authorization and configuration.

Highlights

  • Decoupled Settings Lifecycle: Refactored the settings loading process into a multi-stage pipeline that separates raw file parsing from environment variable resolution and validation.
  • Race Condition Resolution: Ensured that local .env files are fully loaded into process.env before settings placeholders are resolved, preventing expansion failures.
  • Enhanced Testing: Added a new integration test to verify E2E environment variable resolution and updated existing unit tests to cover .env file integration.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the settings loading process to ensure environment variables from .env files are loaded before individual settings scopes are resolved and validated. It also adds unit and integration tests to verify this behavior. The feedback points out that the new unit test directly modifies process.env without guaranteeing cleanup in case of test failure, which violates repository testing conventions and could lead to test leakage.

Comment on lines +1381 to +1419
it('should resolve environment variables loaded from .env files', () => {
delete process.env['TEST_VAR_FROM_DOT_ENV'];

const workspaceEnvPath = path.join(MOCK_WORKSPACE_DIR, '.env');
const userSettingsContent: TestSettings = {
apiKey: '$TEST_VAR_FROM_DOT_ENV',
};

(mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) => {
const normalized = normalizePath(p);
return (
normalized === normalizePath(USER_SETTINGS_PATH) ||
normalized === normalizePath(workspaceEnvPath)
);
});

(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
const normalized = normalizePath(p);
if (normalized === normalizePath(USER_SETTINGS_PATH)) {
return JSON.stringify(userSettingsContent);
}
if (normalized === normalizePath(workspaceEnvPath)) {
return 'TEST_VAR_FROM_DOT_ENV=resolved_from_dot_env';
}
return '{}';
},
);

const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect((settings.user.settings as TestSettings)['apiKey']).toBe(
'resolved_from_dot_env',
);
expect((settings.merged as TestSettings)['apiKey']).toBe(
'resolved_from_dot_env',
);

delete process.env['TEST_VAR_FROM_DOT_ENV'];
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This test directly modifies process.env by deleting a property at the beginning and end of the test case. This pattern is susceptible to test leakage: if an assertion fails, the cleanup code at the end of the it block will not run, and the modified environment variable will persist for subsequent tests.

This violates the repository's testing convention, which states to avoid direct modification of process.env to prevent test leakage.

While testing loadEnvironment's side effects on process.env requires some direct interaction, it should be done safely. Please wrap the test's logic in a try...finally block to guarantee that the environment is cleaned up, even if assertions fail.

    it('should resolve environment variables loaded from .env files', () => {
      const envVar = 'TEST_VAR_FROM_DOT_ENV';
      // Ensure the environment is clean before the test.
      delete process.env[envVar];

      try {
        const workspaceEnvPath = path.join(MOCK_WORKSPACE_DIR, '.env');
        const userSettingsContent: TestSettings = {
          apiKey: `$${envVar}`,
        };

        (mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) => {
          const normalized = normalizePath(p);
          return (
            normalized === normalizePath(USER_SETTINGS_PATH) ||
            normalized === normalizePath(workspaceEnvPath)
          );
        });

        (fs.readFileSync as Mock).mockImplementation(
          (p: fs.PathOrFileDescriptor) => {
            const normalized = normalizePath(p);
            if (normalized === normalizePath(USER_SETTINGS_PATH)) {
              return JSON.stringify(userSettingsContent);
            }
            if (normalized === normalizePath(workspaceEnvPath)) {
              return `${envVar}=resolved_from_dot_env`;
            }
            return '{}';
          },
        );

        const settings = loadSettings(MOCK_WORKSPACE_DIR);
        expect((settings.user.settings as TestSettings)['apiKey']).toBe(
          'resolved_from_dot_env',
        );
        expect((settings.merged as TestSettings)['apiKey']).toBe(
          'resolved_from_dot_env',
        );
      } finally {
        // Ensure cleanup happens even if the test fails.
        delete process.env[envVar];
      }
    });
References
  1. The testing conventions state to avoid modifying process.env directly to prevent test leakage. While this test needs to interact with process.env, it should do so in a way that guarantees cleanup, such as using a try...finally block. (link)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I set up the test using the manual process.env deletion pattern to stay consistent with the adjacent tests (like "should resolve environment variables in user settings"), which also use this exact manual mutation and deletion pattern.

However, I agree that manually mutating process.env in this way is fragile if assertions fail. If we want to make the environment cleanup robust, rather than adding a verbose manual try...finally block to this test, a much cleaner Vitest option would be using vi.stubEnv().

To keep the scope of this PR focused strictly on the settings load-order fix, I feel it's probably best to leave the test matching the adjacent test patterns for now. We can then open a separate refactoring PR to update the settings test suite to use vi.stubEnv systematically. Let me know if you would like me to refactor this specific test using your recommended try...finally block, or the vi.stubEnv approach, or if you agree with me, to keep the test unchanged for consistency with adjacent tests.

@gemini-cli gemini-cli Bot added the status/need-issue Pull requests that need to have an associated issue. label Jul 30, 2026
@WolfGreyDev

Copy link
Copy Markdown
Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request restructures the settings loading flow in packages/cli/src/config/settings.ts to ensure that environment variables defined in .env files are loaded before settings are expanded and validated. Previously, environment variables were resolved during the initial file load, which occurred before .env files were processed by loadEnvironment. The updated implementation loads raw settings first, performs the initial trust check, runs loadEnvironment to populate process.env from .env files, and then resolves and validates each settings scope. New unit and integration tests have been added to verify this behavior. There are no review comments, so I have no feedback to provide.

@WolfGreyDev

Copy link
Copy Markdown
Author

fixes #28684

@WolfGreyDev

Copy link
Copy Markdown
Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the settings loading process in packages/cli/src/config/settings.ts to defer environment variable expansion and validation until after .env files are loaded into process.env via loadEnvironment. This ensures that environment variables defined in .env files are correctly resolved. Unit and integration tests have been added to verify this behavior. Feedback points out a security and correctness risk where raw, unexpanded settings are passed to isWorkspaceTrusted and loadEnvironment. Environment variable placeholders (e.g., "$ENABLE_FOLDER_TRUST") would be evaluated as truthy raw strings, potentially bypassing security controls. It is recommended to resolve environment variables against the pre-existing process.env before performing these checks.

Comment thread packages/cli/src/config/settings.ts Outdated
Comment on lines 843 to 865
const initialTrustCheckSettings = customDeepMerge(
getMergeStrategyForPath,
getDefaultsFromSchema(),
systemDefaultSettings,
userSettings,
systemSettings,
systemDefaultsRawResult.rawSettings,
userRawResult.rawSettings,
systemRawResult.rawSettings,
);
const isTrusted =
isWorkspaceTrusted(initialTrustCheckSettings as Settings, workspaceDir)
.isTrusted ?? false;

// Create a temporary merged settings object to pass to loadEnvironment.
const tempMergedSettings = mergeSettings(
systemSettings,
systemDefaultSettings,
userSettings,
workspaceSettings,
systemRawResult.rawSettings,
systemDefaultsRawResult.rawSettings,
userRawResult.rawSettings,
workspaceRawResult.rawSettings,
isTrusted,
);

// loadEnvironment depends on settings so we have to create a temp version of
// the settings to avoid a cycle
// the settings to avoid a cycle. This loads environment variables from .env files!
loadEnvironment(tempMergedSettings, workspaceDir);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Passing raw, unexpanded settings to isWorkspaceTrusted and loadEnvironment introduces a security and correctness risk. If settings like security.folderTrust.enabled, tools.sandbox, or advanced.ignoreLocalEnv contain environment variable placeholders (e.g., "$ENABLE_FOLDER_TRUST" or "$SANDBOX"), they will be evaluated as raw strings. Since non-empty strings are truthy in JavaScript, these placeholders will be treated as true regardless of their actual resolved values, or they will fail to match expected string values (such as selectedAuthType matching AuthType.USE_VERTEX_AI).

To prevent this, we should resolve environment variables against the pre-existing process.env (before loading .env files) using resolveEnvVarsInObject before performing the trust check and loading the environment.

  const initialTrustCheckSettings = resolveEnvVarsInObject(
    customDeepMerge(
      getMergeStrategyForPath,
      getDefaultsFromSchema(),
      systemDefaultsRawResult.rawSettings,
      userRawResult.rawSettings,
      systemRawResult.rawSettings,
    ) as Settings,
  );
  const isTrusted =
    isWorkspaceTrusted(initialTrustCheckSettings, workspaceDir)
      .isTrusted ?? false;

  // Create a temporary merged settings object to pass to loadEnvironment.
  const tempMergedSettings = resolveEnvVarsInObject(
    mergeSettings(
      systemRawResult.rawSettings,
      systemDefaultsRawResult.rawSettings,
      userRawResult.rawSettings,
      workspaceRawResult.rawSettings,
      isTrusted,
    ),
  );

  // loadEnvironment depends on settings so we have to create a temp version of
  // the settings to avoid a cycle. This loads environment variables from .env files!
  loadEnvironment(tempMergedSettings, workspaceDir);
References
  1. Workspace-level configurations should be treated as untrusted by default. Security-sensitive settings, such as policy paths, must be loaded from trusted user-level configuration and should not be overridable by workspace settings unless trust is explicitly granted by the hosting environment.

@WolfGreyDev
WolfGreyDev force-pushed the fix/settings-env-race-condition branch 2 times, most recently from a346484 to 2e1d2d7 Compare August 4, 2026 11:22
@WolfGreyDev

Copy link
Copy Markdown
Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the settings loading pipeline in packages/cli/src/config/settings.ts to load environment variables from .env files before resolving and validating the various settings scopes, accompanied by new unit and integration tests. However, a high-severity load-order race condition was identified in loadEnvironment where setUpCloudShellEnvironment is called before the .env file is parsed, causing placeholders like selectedType to not be resolved in time and resulting in an incorrect override of GOOGLE_CLOUD_PROJECT in Cloud Shell. A fix is recommended to parse the .env file first and resolve placeholders before setting up the Cloud Shell environment.


// loadEnvironment depends on settings so we have to create a temp version of
// the settings to avoid a cycle. This loads environment variables from .env files!
loadEnvironment(tempMergedSettings, workspaceDir);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

🚨 High Severity: Load-Order Race Condition for .env Variables in Cloud Shell

There is a remaining race condition in loadEnvironment when resolving placeholders for selectedType (e.g., $MOCK_AUTH_TYPE) that are defined in the workspace .env file.

The Issue

  1. loadEnvironment(tempMergedSettings, workspaceDir) is called. At this point, tempMergedSettings.security.auth.selectedType is still the literal string "$MOCK_AUTH_TYPE" because the .env file has not been loaded into process.env yet.
  2. Inside loadEnvironment, setUpCloudShellEnvironment is called before the .env file is parsed and loaded into process.env.
  3. setUpCloudShellEnvironment checks if selectedAuthType === AuthType.USE_VERTEX_AI (i.e., "vertex-ai"). Since it is still "$MOCK_AUTH_TYPE", the check fails, and the Cloud Shell override is not skipped.
  4. GOOGLE_CLOUD_PROJECT is incorrectly overwritten to "cloudshell-gca".
  5. Only after this does loadEnvironment parse the .env file and load MOCK_AUTH_TYPE=vertex-ai into process.env. But by then, GOOGLE_CLOUD_PROJECT has already been overwritten.

This breaks Vertex AI authentication in Cloud Shell for any user who defines their auth type placeholder in .env.

Why the Test Passed

The test should resolve placeholders in selected auth type during loadSettings and skip Cloud Shell override passed only because it used vi.stubEnv('MOCK_AUTH_TYPE', 'vertex-ai') to inject the variable into process.env before loadSettings was called, rather than simulating it being loaded from the .env file.

Recommended Fix

Inside loadEnvironment (around line 683), we should parse the .env file first, and use the parsed variables to resolve the placeholder in selectedAuthType before calling setUpCloudShellEnvironment.

For example, in loadEnvironment:

export function loadEnvironment(
  settings: Settings,
  workspaceDir: string,
  isWorkspaceTrustedFn = isWorkspaceTrusted,
): void {
  // ... (existing setup) ...

  const envFilePath = findEnvFile(workspaceDir, isTrusted, shouldIgnoreEnv);

  // 1. Parse the .env file first if it exists
  let parsedEnv: Record<string, string> = {};
  if (envFilePath) {
    try {
      const envFileContent = fs.readFileSync(envFilePath, 'utf-8');
      parsedEnv = dotenv.parse(envFileContent);
    } catch {
      // ignore
    }
  }

  // 2. Cloud Shell environment variable handling
  if (process.env['CLOUD_SHELL'] === 'true') {
    let selectedAuthType = settings.security?.auth?.selectedType;
    if (selectedAuthType) {
      // Resolve placeholders using both process.env and the parsed .env variables
      selectedAuthType = resolveEnvVarsInString(selectedAuthType, parsedEnv);
    }
    setUpCloudShellEnvironment(
      envFilePath,
      isTrusted,
      isSandboxed,
      selectedAuthType,
    );
  }

  // 3. Load environment variables into process.env (using the already parsed parsedEnv)
  if (envFilePath) {
    // ... (load parsedEnv into process.env as before) ...
  }
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/gemini review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The refactored settings loading pipeline effectively resolves the race condition identified in the initial review. By decoupling the raw file loading from the environment variable expansion and validation, you have ensured that .env variables are fully available in process.env before they are used to resolve placeholders in settings or trigger Cloud Shell environment overrides.

Key Improvements:

  • Load-Order Correctness: loadEnvironment now correctly parses the .env file and resolves the selectedAuthType before calling setUpCloudShellEnvironment.
  • Decoupled Pipeline: The new resolveAndValidate helper ensures that settings are only expanded and validated after the environment is fully initialized, preventing the previous issue where variables were missing during the initial startup phase.
  • Robust Testing: The added integration and unit tests correctly simulate the environment loading sequence, verifying that placeholders are resolved as expected in both standard and Cloud Shell scenarios.

This implementation is correct and addresses the high-severity issue identified.

@gemini-cli

gemini-cli Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Hi there! Thank you for your interest in contributing to Gemini CLI.

To ensure we maintain high code quality and focus on our prioritized roadmap, we only guarantee review and consideration of pull requests for issues that are explicitly labeled as 'help wanted'.

This PR will be closed in 7 days if it remains without that designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding.

@gemini-cli gemini-cli Bot added priority/p2 Important but can be addressed in a future release. area/core Issues related to User Interface, OS Support, Core Functionality and removed status/need-issue Pull requests that need to have an associated issue. labels Aug 7, 2026
@WolfGreyDev
WolfGreyDev force-pushed the fix/settings-env-race-condition branch from cb1ba4f to bc4d012 Compare August 7, 2026 18:56
@WolfGreyDev
WolfGreyDev force-pushed the fix/settings-env-race-condition branch from bc4d012 to 5f1a3a7 Compare August 7, 2026 21:30
@gemini-cli

gemini-cli Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

This pull request is being closed as it has been open for 14 days without a 'help wanted' designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core Issues related to User Interface, OS Support, Core Functionality priority/p2 Important but can be addressed in a future release. size/l A large sized PR status/pr-nudge-sent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Load-order race condition prevents .env variables from being resolved in settings

1 participant