You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat: Support environment variable substitution in MCP server headers #5282
The Gemini CLI should support environment variable substitution within the headers values of the mcpServers configuration. This would allow users to store sensitive tokens securely in their environment and reference them in the configuration.
The CLI should replace $MY_API_TOKEN with the value of the MY_API_TOKEN environment variable before sending the request. It should support both $VAR and ${VAR} syntax.
Why is this needed?
Currently, when configuring an MCP server in settings.json, any values in the headers object are treated as literal strings. This means sensitive data, like bearer tokens or API keys, must be hardcoded into the configuration file, which is a security risk.
Additional context
The current alternative is to hardcode the token, which is not secure. Another alternative would be to use a local script as a proxy, but this adds unnecessary complexity for a common use case.
I'm noting some inconsistency.
This actually seemed to work in the latest v0.1.15 release
However when I checked the source code, there was no different in this module between 0.1.14 and 0.1.15
Can someone please triage this and confirm?
I did some investigation on the latest source and Gemini CLI found this.
The key file here is packages/cli/src/config/settings.ts.
When the Gemini CLI starts, it calls the loadSettings function. This function reads your settings.json from the user, workspace, and system directories. As it parses each file, it immediately processes the content to substitute any environment variables.
This is done by the resolveEnvVarsInObject function, which recursively scans through your entire settings object. When it finds a string value, it uses resolveEnvVarsInString to perform the substitution.
Here is the exact code that finds and replaces variables like $GITHUB_PERSONAL_ACCESS_TOKEN:
1 // packages/cli/src/config/settings.ts
2
3 function resolveEnvVarsInString(value: string): string {
4 const envVarRegex = /\$(?:(\w+)|{([^}]+)})/g; // Find $VAR_NAME or ${VAR_NAME}
5 return value.replace(envVarRegex, (match, varName1, varName2) => {
6 const varName = varName1 || varName2;
7 if (process && process.env && typeof process.env[varName] === 'string') {
8 return process.env[varName]!;
9 }
10 return match;
11 });
12 }
13
14 function resolveEnvVarsInObject<T>(obj: T): T {
15 // ... (recursively calls resolveEnvVarsInString on all string values)
16 }
17
18 // In the main loadSettings function:
19 // ...
20 // workspaceSettings = resolveEnvVarsInObject(parsedWorkspaceSettings);
21 // ...
So, by the time your mcpServers configuration is passed to the rest of the application, the Authorization header has already been updated to contain the actual value of your GITHUB_PERSONAL_ACCESS_TOKEN.
Given this, maybe my patch is redundant.
Again, I'd love confirmation before I bring in a PR.
I did a review of the current implementation in HEAD with my implementation. Gemini CLI return this analysis.
Comparison: main vs. feat/env-var-in-headers-5282
Here is a breakdown of the differences between the two implementations for handling environment variables.
The Original Implementation (main branch)
Complex Loading Logic: The original code had a multi-step process. It would first load the settings.json files, and then separately call a loadEnvironment function. This function would re-read the settings files just to find a list of excluded environment variables before finally loading the .env file.
Manual Variable Handling: It manually parsed the .env file and looped through the variables to decide which ones to load into process.env, carefully checking for exclusions.
Recursive Substitution: It used a custom recursive function, resolveEnvVarsInObject, to walk through the entire settings object and replace any $VAR_NAME placeholders.
1 // OLD IMPLEMENTATION on `main`
2 function loadEnvironment(settings?: Settings): void {
3 // ... complex logic to find .env file ...
4 // ... re-reads settings to get excludedProjectEnvVars ...
5 // ... manually parses .env file and loads variables one-by-one ...
6 }
7
8 export function loadSettings(workspaceDir: string): LoadedSettings {
9 // ... loads all settings files ...
10 const parsedWorkspaceSettings = JSON.parse(/*...*/) as Settings;
11 workspaceSettings = resolveEnvVarsInObject(parsedWorkspaceSettings); // <--- Substitution happens here
12 // ...
13 loadEnvironment(loadedSettings.merged); // <--- .env loading happens later
14 return loadedSettings;
15 }
Your New Implementation (feat/env-var-in-headers-5282 branch)
Simplified Loading Order: Your new code dramatically simplifies the process. The loadSettings function now calls loadEnvironment first.
Standard Library Usage: The loadEnvironment function is much simpler. It just finds the .env file and uses dotenv.config() to load it directly into process.env. This is the standard, recommended way to use the dotenv library.
No Manual Parsing: You've removed all the manual parsing and exclusion logic for the .env file, relying on the library to do the heavy lifting.
Same Substitution Logic: You have wisely kept the resolveEnvVarsInObject function, which is a robust way to handle the placeholder substitution within the JSON structure itself.
1 // NEW IMPLEMENTATION on `feat/env-var-in-headers-5282`
2 export function loadEnvironment(): void {
3 const envFilePath = findEnvFile(process.cwd());
4 // ...
5 if (envFilePath) {
6 dotenv.config({ path: envFilePath, quiet: true }); // <--- Simplified .env loading
7 }
8 }
9
10 export function loadSettings(workspaceDir: string): LoadedSettings {
11 loadEnvironment(); // <--- .env loading happens FIRST
12 // ...
13 const parsedWorkspaceSettings = JSON.parse(/*...*/) as Settings;
14 workspaceSettings = resolveEnvVarsInObject(parsedWorkspaceSettings); // <--- Substitution happens after .env is loaded
15 // ...
16 return new LoadedSettings(/*...*/);
17 }
Conclusion
Your new implementation is a significant improvement.
Clarity and Simplicity: It's much easier to read and understand. The order of operations is more logical (.env file first, then settings
parsing), and it removes complex, custom code in favor of a standard library function.
Robustness: By using dotenv.config() directly, you're more likely to be compatible with the full feature set of the dotenv library (like
comment handling, etc.) and less likely to have bugs in custom parsing logic.
Maintainability: This new version will be much easier for other developers to maintain in the future.
Excellent work on this refactor! It's a great example of simplifying code while improving its quality.
Point being, even if the current implementation works, maybe there is scope to improve the logic.
graph TD
A("User runs gemini")
B("loadSettings in settings.ts")
C{"Reads settings.json files"}
D("resolveEnvVarsInObject is called on the raw JSON content")
E("resolveEnvVarsInString replaces VAR with process.env values (which might be incomplete)")
F("Returns resolved LoadedSettings object")
G("loadCliConfig in config.ts receives settings")
H("loadCliConfig calls loadEnvironment")
I("loadEnvironment finds and manually parses .env file, loading variables into process.env")
J("Final Config object is created")
K("mcp-client.ts uses the pre-substituted headers for HTTP requests")
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
G --> H
H --> I
I --> J
J --> K
graph TD
A("User runs gemini")
B("loadSettings in settings.ts")
C("loadSettings calls loadEnvironment first")
D("loadEnvironment finds .env file and uses dotenv.config() to populate process.env")
E("loadSettings reads settings.json files")
F("resolveEnvVarsInObject is called on the raw JSON content")
G("resolveEnvVarsInString replaces VAR with values from the fully populated process.env")
H("Returns resolved LoadedSettings object")
I("loadCliConfig in config.ts receives settings")
J("Final Config object is created")
K("mcp-client.ts uses the correctly substituted headers for HTTP requests")
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
G --> H
H --> I
I --> J
J --> K
Hello! As part of our effort to keep our backlog manageable and focus on the most active issues, we are tidying up older reports.
It looks like this issue hasn't been active for a while, so we are closing it for now. However, if you are still experiencing this bug on the latest stable build, please feel free to comment on this issue or create a new one with updated details.
Activity
ksprashu commentedon Jul 31, 2025
I'm noting some inconsistency.
This actually seemed to work in the latest v0.1.15 release
However when I checked the source code, there was no different in this module between 0.1.14 and 0.1.15
Can someone please triage this and confirm?
DinoChiesa commentedon Aug 8, 2025
Please approve and merge that commit!
DinoChiesa commentedon Aug 8, 2025
I retro-fitted that commit into my 0.1.17 source tree, and it worked for remote HTTP Servers.
Fix authorization header to use literal token placeholder
ksprashu commentedon Aug 15, 2025
Thanks @DinoChiesa for checking this.
I did some investigation on the latest source and Gemini CLI found this.
Given this, maybe my patch is redundant.
Again, I'd love confirmation before I bring in a PR.
ksprashu commentedon Aug 15, 2025
I did a review of the current implementation in HEAD with my implementation. Gemini CLI return this analysis.
Point being, even if the current implementation works, maybe there is scope to improve the logic.
feat(core): Support environment variable substitution in MCP server h…
ksprashu commentedon Aug 15, 2025
Original Flow
graph TD A("User runs gemini") B("loadSettings in settings.ts") C{"Reads settings.json files"} D("resolveEnvVarsInObject is called on the raw JSON content") E("resolveEnvVarsInString replaces VAR with process.env values (which might be incomplete)") F("Returns resolved LoadedSettings object") G("loadCliConfig in config.ts receives settings") H("loadCliConfig calls loadEnvironment") I("loadEnvironment finds and manually parses .env file, loading variables into process.env") J("Final Config object is created") K("mcp-client.ts uses the pre-substituted headers for HTTP requests") A --> B B --> C C --> D D --> E E --> F F --> G G --> H H --> I I --> J J --> Kksprashu commentedon Aug 15, 2025
Implementation from this feat branch.
graph TD A("User runs gemini") B("loadSettings in settings.ts") C("loadSettings calls loadEnvironment first") D("loadEnvironment finds .env file and uses dotenv.config() to populate process.env") E("loadSettings reads settings.json files") F("resolveEnvVarsInObject is called on the raw JSON content") G("resolveEnvVarsInString replaces VAR with values from the fully populated process.env") H("Returns resolved LoadedSettings object") I("loadCliConfig in config.ts receives settings") J("Final Config object is created") K("mcp-client.ts uses the correctly substituted headers for HTTP requests") A --> B B --> C C --> D D --> E E --> F F --> G G --> H H --> I I --> J J --> Kdocs: Add Google Gemini CLI installation guide and integration (#757)
docs: Add Google Gemini CLI installation guide and integration (githu…
gemini-cli commentedon Dec 3, 2025
Hello! As part of our effort to keep our backlog manageable and focus on the most active issues, we are tidying up older reports.
It looks like this issue hasn't been active for a while, so we are closing it for now. However, if you are still experiencing this bug on the latest stable build, please feel free to comment on this issue or create a new one with updated details.
Thank you for your contribution!