fix: exclude DEBUG and DEBUG_MODE from project .env files by default (#5289)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
parent
bedcbb9feb
commit
03ed37d0dc
|
@ -242,6 +242,8 @@ To hit a breakpoint inside the sandbox container run:
|
|||
DEBUG=1 gemini
|
||||
```
|
||||
|
||||
**Note:** If you have `DEBUG=true` in a project's `.env` file, it won't affect gemini-cli due to automatic exclusion. Use `.gemini/.env` files for gemini-cli specific debug settings.
|
||||
|
||||
### React DevTools
|
||||
|
||||
To debug the CLI's React-based UI, you can use React DevTools. Ink, the library used for the CLI's interface, is compatible with React DevTools version 4.x.
|
||||
|
|
|
@ -91,6 +91,8 @@ The Gemini CLI requires you to authenticate with Google's AI services. On initia
|
|||
|
||||
You can create a **`.gemini/.env`** file in your project directory or in your home directory. Creating a plain **`.env`** file also works, but `.gemini/.env` is recommended to keep Gemini variables isolated from other tools.
|
||||
|
||||
**Important:** Some environment variables (like `DEBUG` and `DEBUG_MODE`) are automatically excluded from project `.env` files to prevent interference with gemini-cli behavior. Use `.gemini/.env` files for gemini-cli specific variables.
|
||||
|
||||
Gemini CLI automatically loads environment variables from the **first** `.env` file it finds, using the following search order:
|
||||
|
||||
1. Starting in the **current directory** and moving upward toward `/`, for each directory it checks:
|
||||
|
|
|
@ -240,6 +240,14 @@ In addition to a project settings file, a project's `.gemini` directory can cont
|
|||
}
|
||||
```
|
||||
|
||||
- **`excludedProjectEnvVars`** (array of strings):
|
||||
- **Description:** Specifies environment variables that should be excluded from being loaded from project `.env` files. This prevents project-specific environment variables (like `DEBUG=true`) from interfering with gemini-cli behavior. Variables from `.gemini/.env` files are never excluded.
|
||||
- **Default:** `["DEBUG", "DEBUG_MODE"]`
|
||||
- **Example:**
|
||||
```json
|
||||
"excludedProjectEnvVars": ["DEBUG", "DEBUG_MODE", "NODE_ENV"]
|
||||
```
|
||||
|
||||
### Example `settings.json`:
|
||||
|
||||
```json
|
||||
|
@ -271,7 +279,8 @@ In addition to a project settings file, a project's `.gemini` directory can cont
|
|||
"run_shell_command": {
|
||||
"tokenBudget": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
"excludedProjectEnvVars": ["DEBUG", "DEBUG_MODE", "NODE_ENV"]
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -293,6 +302,8 @@ The CLI automatically loads environment variables from an `.env` file. The loadi
|
|||
2. If not found, it searches upwards in parent directories until it finds an `.env` file or reaches the project root (identified by a `.git` folder) or the home directory.
|
||||
3. If still not found, it looks for `~/.env` (in the user's home directory).
|
||||
|
||||
**Environment Variable Exclusion:** Some environment variables (like `DEBUG` and `DEBUG_MODE`) are automatically excluded from being loaded from project `.env` files to prevent interference with gemini-cli behavior. Variables from `.gemini/.env` files are never excluded. You can customize this behavior using the `excludedProjectEnvVars` setting in your `settings.json` file.
|
||||
|
||||
- **`GEMINI_API_KEY`** (Required):
|
||||
- Your API key for the Gemini API.
|
||||
- **Crucial for operation.** The CLI will not function without it.
|
||||
|
@ -332,6 +343,7 @@ The CLI automatically loads environment variables from an `.env` file. The loadi
|
|||
- `<profile_name>`: Uses a custom profile. To define a custom profile, create a file named `sandbox-macos-<profile_name>.sb` in your project's `.gemini/` directory (e.g., `my-project/.gemini/sandbox-macos-custom.sb`).
|
||||
- **`DEBUG` or `DEBUG_MODE`** (often used by underlying libraries or the CLI itself):
|
||||
- Set to `true` or `1` to enable verbose debug logging, which can be helpful for troubleshooting.
|
||||
- **Note:** These variables are automatically excluded from project `.env` files by default to prevent interference with gemini-cli behavior. Use `.gemini/.env` files if you need to set these for gemini-cli specifically.
|
||||
- **`NO_COLOR`**:
|
||||
- Set to any value to disable all color output in the CLI.
|
||||
- **`CLI_TITLE`**:
|
||||
|
|
|
@ -129,6 +129,8 @@ export SANDBOX_SET_UID_GID=false # Disable UID/GID mapping
|
|||
DEBUG=1 gemini -s -p "debug command"
|
||||
```
|
||||
|
||||
**Note:** If you have `DEBUG=true` in a project's `.env` file, it won't affect gemini-cli due to automatic exclusion. Use `.gemini/.env` files for gemini-cli specific debug settings.
|
||||
|
||||
### Inspect sandbox
|
||||
|
||||
```bash
|
||||
|
|
|
@ -53,6 +53,11 @@ This guide provides solutions to common issues and debugging tips.
|
|||
- **Cause:** The `is-in-ci` package checks for the presence of `CI`, `CONTINUOUS_INTEGRATION`, or any environment variable with a `CI_` prefix. When any of these are found, it signals that the environment is non-interactive, which prevents the CLI from starting in its interactive mode.
|
||||
- **Solution:** If the `CI_` prefixed variable is not needed for the CLI to function, you can temporarily unset it for the command. e.g., `env -u CI_TOKEN gemini`
|
||||
|
||||
- **DEBUG mode not working from project .env file**
|
||||
- **Issue:** Setting `DEBUG=true` in a project's `.env` file doesn't enable debug mode for gemini-cli.
|
||||
- **Cause:** The `DEBUG` and `DEBUG_MODE` variables are automatically excluded from project `.env` files to prevent interference with gemini-cli behavior.
|
||||
- **Solution:** Use a `.gemini/.env` file instead, or configure the `excludedProjectEnvVars` setting in your `settings.json` to exclude fewer variables.
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
- **CLI debugging:**
|
||||
|
|
|
@ -334,6 +334,86 @@ describe('Settings Loading and Merging', () => {
|
|||
expect(settings.merged.contextFileName).toBe('PROJECT_SPECIFIC.md');
|
||||
});
|
||||
|
||||
it('should handle excludedProjectEnvVars correctly when only in user settings', () => {
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === USER_SETTINGS_PATH,
|
||||
);
|
||||
const userSettingsContent = {
|
||||
excludedProjectEnvVars: ['DEBUG', 'NODE_ENV', 'CUSTOM_VAR'],
|
||||
};
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
return '';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.excludedProjectEnvVars).toEqual([
|
||||
'DEBUG',
|
||||
'NODE_ENV',
|
||||
'CUSTOM_VAR',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle excludedProjectEnvVars correctly when only in workspace settings', () => {
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH,
|
||||
);
|
||||
const workspaceSettingsContent = {
|
||||
excludedProjectEnvVars: ['WORKSPACE_DEBUG', 'WORKSPACE_VAR'],
|
||||
};
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
|
||||
return JSON.stringify(workspaceSettingsContent);
|
||||
return '';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.excludedProjectEnvVars).toEqual([
|
||||
'WORKSPACE_DEBUG',
|
||||
'WORKSPACE_VAR',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should merge excludedProjectEnvVars with workspace taking precedence over user', () => {
|
||||
(mockFsExistsSync as Mock).mockReturnValue(true);
|
||||
const userSettingsContent = {
|
||||
excludedProjectEnvVars: ['DEBUG', 'NODE_ENV', 'USER_VAR'],
|
||||
};
|
||||
const workspaceSettingsContent = {
|
||||
excludedProjectEnvVars: ['WORKSPACE_DEBUG', 'WORKSPACE_VAR'],
|
||||
};
|
||||
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
|
||||
return JSON.stringify(workspaceSettingsContent);
|
||||
return '';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.user.settings.excludedProjectEnvVars).toEqual([
|
||||
'DEBUG',
|
||||
'NODE_ENV',
|
||||
'USER_VAR',
|
||||
]);
|
||||
expect(settings.workspace.settings.excludedProjectEnvVars).toEqual([
|
||||
'WORKSPACE_DEBUG',
|
||||
'WORKSPACE_VAR',
|
||||
]);
|
||||
expect(settings.merged.excludedProjectEnvVars).toEqual([
|
||||
'WORKSPACE_DEBUG',
|
||||
'WORKSPACE_VAR',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should default contextFileName to undefined if not in any settings file', () => {
|
||||
(mockFsExistsSync as Mock).mockReturnValue(true);
|
||||
const userSettingsContent = { theme: 'dark' };
|
||||
|
@ -1055,4 +1135,140 @@ describe('Settings Loading and Merging', () => {
|
|||
expect(loadedSettings.merged.theme).toBe('ocean');
|
||||
});
|
||||
});
|
||||
|
||||
describe('excludedProjectEnvVars integration', () => {
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
it('should exclude DEBUG and DEBUG_MODE from project .env files by default', () => {
|
||||
// Create a workspace settings file with excludedProjectEnvVars
|
||||
const workspaceSettingsContent = {
|
||||
excludedProjectEnvVars: ['DEBUG', 'DEBUG_MODE'],
|
||||
};
|
||||
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH,
|
||||
);
|
||||
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
|
||||
return JSON.stringify(workspaceSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
// Mock findEnvFile to return a project .env file
|
||||
const originalFindEnvFile = (
|
||||
loadSettings as unknown as { findEnvFile: () => string }
|
||||
).findEnvFile;
|
||||
(loadSettings as unknown as { findEnvFile: () => string }).findEnvFile =
|
||||
() => '/mock/project/.env';
|
||||
|
||||
// Mock fs.readFileSync for .env file content
|
||||
const originalReadFileSync = fs.readFileSync;
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === '/mock/project/.env') {
|
||||
return 'DEBUG=true\nDEBUG_MODE=1\nGEMINI_API_KEY=test-key';
|
||||
}
|
||||
if (p === MOCK_WORKSPACE_SETTINGS_PATH) {
|
||||
return JSON.stringify(workspaceSettingsContent);
|
||||
}
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
// This will call loadEnvironment internally with the merged settings
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
// Verify the settings were loaded correctly
|
||||
expect(settings.merged.excludedProjectEnvVars).toEqual([
|
||||
'DEBUG',
|
||||
'DEBUG_MODE',
|
||||
]);
|
||||
|
||||
// Note: We can't directly test process.env changes here because the mocking
|
||||
// prevents the actual file system operations, but we can verify the settings
|
||||
// are correctly merged and passed to loadEnvironment
|
||||
} finally {
|
||||
(loadSettings as unknown as { findEnvFile: () => string }).findEnvFile =
|
||||
originalFindEnvFile;
|
||||
(fs.readFileSync as Mock).mockImplementation(originalReadFileSync);
|
||||
}
|
||||
});
|
||||
|
||||
it('should respect custom excludedProjectEnvVars from user settings', () => {
|
||||
const userSettingsContent = {
|
||||
excludedProjectEnvVars: ['NODE_ENV', 'DEBUG'],
|
||||
};
|
||||
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === USER_SETTINGS_PATH,
|
||||
);
|
||||
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.user.settings.excludedProjectEnvVars).toEqual([
|
||||
'NODE_ENV',
|
||||
'DEBUG',
|
||||
]);
|
||||
expect(settings.merged.excludedProjectEnvVars).toEqual([
|
||||
'NODE_ENV',
|
||||
'DEBUG',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should merge excludedProjectEnvVars with workspace taking precedence', () => {
|
||||
const userSettingsContent = {
|
||||
excludedProjectEnvVars: ['DEBUG', 'NODE_ENV', 'USER_VAR'],
|
||||
};
|
||||
const workspaceSettingsContent = {
|
||||
excludedProjectEnvVars: ['WORKSPACE_DEBUG', 'WORKSPACE_VAR'],
|
||||
};
|
||||
|
||||
(mockFsExistsSync as Mock).mockReturnValue(true);
|
||||
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
|
||||
return JSON.stringify(workspaceSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
expect(settings.user.settings.excludedProjectEnvVars).toEqual([
|
||||
'DEBUG',
|
||||
'NODE_ENV',
|
||||
'USER_VAR',
|
||||
]);
|
||||
expect(settings.workspace.settings.excludedProjectEnvVars).toEqual([
|
||||
'WORKSPACE_DEBUG',
|
||||
'WORKSPACE_VAR',
|
||||
]);
|
||||
expect(settings.merged.excludedProjectEnvVars).toEqual([
|
||||
'WORKSPACE_DEBUG',
|
||||
'WORKSPACE_VAR',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,6 +24,7 @@ import { CustomTheme } from '../ui/themes/theme.js';
|
|||
export const SETTINGS_DIRECTORY_NAME = '.gemini';
|
||||
export const USER_SETTINGS_DIR = path.join(homedir(), SETTINGS_DIRECTORY_NAME);
|
||||
export const USER_SETTINGS_PATH = path.join(USER_SETTINGS_DIR, 'settings.json');
|
||||
export const DEFAULT_EXCLUDED_ENV_VARS = ['DEBUG', 'DEBUG_MODE'];
|
||||
|
||||
export function getSystemSettingsPath(): string {
|
||||
if (process.env.GEMINI_CLI_SYSTEM_SETTINGS_PATH) {
|
||||
|
@ -38,6 +39,10 @@ export function getSystemSettingsPath(): string {
|
|||
}
|
||||
}
|
||||
|
||||
export function getWorkspaceSettingsPath(workspaceDir: string): string {
|
||||
return path.join(workspaceDir, SETTINGS_DIRECTORY_NAME, 'settings.json');
|
||||
}
|
||||
|
||||
export type DnsResolutionOrder = 'ipv4first' | 'verbatim';
|
||||
|
||||
export enum SettingScope {
|
||||
|
@ -115,6 +120,9 @@ export interface Settings {
|
|||
disableUpdateNag?: boolean;
|
||||
|
||||
memoryDiscoveryMaxDirs?: number;
|
||||
|
||||
// Environment variables to exclude from project .env files
|
||||
excludedProjectEnvVars?: string[];
|
||||
dnsResolutionOrder?: DnsResolutionOrder;
|
||||
}
|
||||
|
||||
|
@ -292,15 +300,61 @@ export function setUpCloudShellEnvironment(envFilePath: string | null): void {
|
|||
}
|
||||
}
|
||||
|
||||
export function loadEnvironment(): void {
|
||||
export function loadEnvironment(settings?: Settings): void {
|
||||
const envFilePath = findEnvFile(process.cwd());
|
||||
|
||||
// Cloud Shell environment variable handling
|
||||
if (process.env.CLOUD_SHELL === 'true') {
|
||||
setUpCloudShellEnvironment(envFilePath);
|
||||
}
|
||||
|
||||
// If no settings provided, try to load workspace settings for exclusions
|
||||
let resolvedSettings = settings;
|
||||
if (!resolvedSettings) {
|
||||
const workspaceSettingsPath = getWorkspaceSettingsPath(process.cwd());
|
||||
try {
|
||||
if (fs.existsSync(workspaceSettingsPath)) {
|
||||
const workspaceContent = fs.readFileSync(
|
||||
workspaceSettingsPath,
|
||||
'utf-8',
|
||||
);
|
||||
const parsedWorkspaceSettings = JSON.parse(
|
||||
stripJsonComments(workspaceContent),
|
||||
) as Settings;
|
||||
resolvedSettings = resolveEnvVarsInObject(parsedWorkspaceSettings);
|
||||
}
|
||||
} catch (_e) {
|
||||
// Ignore errors loading workspace settings
|
||||
}
|
||||
}
|
||||
|
||||
if (envFilePath) {
|
||||
dotenv.config({ path: envFilePath, quiet: true });
|
||||
// Manually parse and load environment variables to handle exclusions correctly.
|
||||
// This avoids modifying environment variables that were already set from the shell.
|
||||
try {
|
||||
const envFileContent = fs.readFileSync(envFilePath, 'utf-8');
|
||||
const parsedEnv = dotenv.parse(envFileContent);
|
||||
|
||||
const excludedVars =
|
||||
resolvedSettings?.excludedProjectEnvVars || DEFAULT_EXCLUDED_ENV_VARS;
|
||||
const isProjectEnvFile = !envFilePath.includes(GEMINI_DIR);
|
||||
|
||||
for (const key in parsedEnv) {
|
||||
if (Object.hasOwn(parsedEnv, key)) {
|
||||
// If it's a project .env file, skip loading excluded variables.
|
||||
if (isProjectEnvFile && excludedVars.includes(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Load variable only if it's not already set in the environment.
|
||||
if (!Object.hasOwn(process.env, key)) {
|
||||
process.env[key] = parsedEnv[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_e) {
|
||||
// Errors are ignored to match the behavior of `dotenv.config({ quiet: true })`.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -309,7 +363,6 @@ export function loadEnvironment(): void {
|
|||
* Project settings override user settings.
|
||||
*/
|
||||
export function loadSettings(workspaceDir: string): LoadedSettings {
|
||||
loadEnvironment();
|
||||
let systemSettings: Settings = {};
|
||||
let userSettings: Settings = {};
|
||||
let workspaceSettings: Settings = {};
|
||||
|
@ -331,6 +384,8 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
|
|||
// We expect homedir to always exist and be resolvable.
|
||||
const realHomeDir = fs.realpathSync(resolvedHomeDir);
|
||||
|
||||
const workspaceSettingsPath = getWorkspaceSettingsPath(workspaceDir);
|
||||
|
||||
// Load system settings
|
||||
try {
|
||||
if (fs.existsSync(systemSettingsPath)) {
|
||||
|
@ -369,12 +424,6 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
|
|||
});
|
||||
}
|
||||
|
||||
const workspaceSettingsPath = path.join(
|
||||
workspaceDir,
|
||||
SETTINGS_DIRECTORY_NAME,
|
||||
'settings.json',
|
||||
);
|
||||
|
||||
// This comparison is now much more reliable.
|
||||
if (realWorkspaceDir !== realHomeDir) {
|
||||
// Load workspace settings
|
||||
|
@ -402,7 +451,8 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
|
|||
}
|
||||
}
|
||||
|
||||
return new LoadedSettings(
|
||||
// Create LoadedSettings first
|
||||
const loadedSettings = new LoadedSettings(
|
||||
{
|
||||
path: systemSettingsPath,
|
||||
settings: systemSettings,
|
||||
|
@ -417,6 +467,11 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
|
|||
},
|
||||
settingsErrors,
|
||||
);
|
||||
|
||||
// Load environment with merged settings
|
||||
loadEnvironment(loadedSettings.merged);
|
||||
|
||||
return loadedSettings;
|
||||
}
|
||||
|
||||
export function saveSettings(settingsFile: SettingsFile): void {
|
||||
|
|
Loading…
Reference in New Issue