Introduce system defaults (vs system overrides) (#6724)

This commit is contained in:
Billy Biggs
2025-08-24 21:21:22 -07:00
committed by GitHub
parent 1918f4466b
commit 04953d60c1
9 changed files with 241 additions and 26 deletions

View File

@@ -7,25 +7,29 @@ Gemini CLI offers several ways to configure its behavior, including environment
Configuration is applied in the following order of precedence (lower numbers are overridden by higher numbers):
1. **Default values:** Hardcoded defaults within the application.
2. **User settings file:** Global settings for the current user.
3. **Project settings file:** Project-specific settings.
4. **System settings file:** System-wide settings.
5. **Environment variables:** System-wide or session-specific variables, potentially loaded from `.env` files.
6. **Command-line arguments:** Values passed when launching the CLI.
2. **System defaults file:** System-wide default settings that can be overridden by other settings files.
3. **User settings file:** Global settings for the current user.
4. **Project settings file:** Project-specific settings.
5. **System settings file:** System-wide settings that override all other settings files.
6. **Environment variables:** System-wide or session-specific variables, potentially loaded from `.env` files.
7. **Command-line arguments:** Values passed when launching the CLI.
## Settings files
Gemini CLI uses `settings.json` files for persistent configuration. There are three locations for these files:
Gemini CLI uses JSON settings files for persistent configuration. There are four locations for these files:
- **System defaults file:**
- **Location:** `/etc/gemini-cli/system-defaults.json` (Linux), `C:\ProgramData\gemini-cli\system-defaults.json` (Windows) or `/Library/Application Support/GeminiCli/system-defaults.json` (macOS). The path can be overridden using the `GEMINI_CLI_SYSTEM_DEFAULTS_PATH` environment variable.
- **Scope:** Provides a base layer of system-wide default settings. These settings have the lowest precedence and are intended to be overridden by user, project, or system override settings.
- **User settings file:**
- **Location:** `~/.gemini/settings.json` (where `~` is your home directory).
- **Scope:** Applies to all Gemini CLI sessions for the current user.
- **Scope:** Applies to all Gemini CLI sessions for the current user. User settings override system defaults.
- **Project settings file:**
- **Location:** `.gemini/settings.json` within your project's root directory.
- **Scope:** Applies only when running Gemini CLI from that specific project. Project settings override user settings.
- **Scope:** Applies only when running Gemini CLI from that specific project. Project settings override user settings and system defaults.
- **System settings file:**
- **Location:** `/etc/gemini-cli/settings.json` (Linux), `C:\ProgramData\gemini-cli\settings.json` (Windows) or `/Library/Application Support/GeminiCli/settings.json` (macOS). The path can be overridden using the `GEMINI_CLI_SYSTEM_SETTINGS_PATH` environment variable.
- **Scope:** Applies to all Gemini CLI sessions on the system, for all users. System settings override user and project settings. May be useful for system administrators at enterprises to have controls over users' Gemini CLI setups.
- **Scope:** Applies to all Gemini CLI sessions on the system, for all users. System settings act as overrides, taking precedence over all other settings files. May be useful for system administrators at enterprises to have controls over users' Gemini CLI setups.
**Note on environment variables in settings:** String values within your `settings.json` files can reference environment variables using either `$VAR_NAME` or `${VAR_NAME}` syntax. These variables will be automatically resolved when the settings are loaded. For example, if you have an environment variable `MY_API_TOKEN`, you could use it in `settings.json` like this: `"apiKey": "$MY_API_TOKEN"`.

View File

@@ -6,23 +6,27 @@ This document outlines configuration patterns and best practices for deploying a
## Centralized Configuration: The System Settings File
The most powerful tool for enterprise administration is the system-wide `settings.json` file. This file allows you to define a baseline configuration that applies to all users on a machine. For a complete overview of configuration options, see the [Configuration documentation](./configuration.md). Settings from system, user, and project-level `settings.json` files are merged together. For most settings, the system-wide configuration takes precedence, overriding any conflicting user or project-level settings. However, some settings, like `customThemes`, `mcpServers`, and `includeDirectories`, are merged from all configuration files, and if there are conflicting values (e.g., both workspace and system settings have a 'github' MCP server defined), the system value will take precedence.
The most powerful tools for enterprise administration are the system-wide settings files. These files allow you to define a baseline configuration (`system-defaults.json`) and a set of overrides (`settings.json`) that apply to all users on a machine. For a complete overview of configuration options, see the [Configuration documentation](./configuration.md).
Settings are merged from four files. The precedence order for single-value settings (like `theme`) is:
1. System Defaults (`system-defaults.json`)
2. User Settings (`~/.gemini/settings.json`)
3. Workspace Settings (`<project>/.gemini/settings.json`)
4. System Overrides (`settings.json`)
This means the System Overrides file has the final say. For settings that are arrays (`includeDirectories`) or objects (`mcpServers`), the values are merged.
**Example of Merging and Precedence:**
Here is how settings from different levels are combined.
- **System `settings.json`:**
- **System Defaults `system-defaults.json`:**
```json
{
"theme": "system-enforced-theme",
"mcpServers": {
"corp-server": {
"command": "/usr/local/bin/corp-server-prod"
}
},
"includeDirectories": ["/etc/gemini-cli/global-context"]
"theme": "default-corporate-theme",
"includeDirectories": ["/etc/gemini-cli/common-context"]
}
```
@@ -44,6 +48,7 @@ Here is how settings from different levels are combined.
```
- **Workspace `settings.json` (`<project>/.gemini/settings.json`):**
```json
{
"theme": "project-specific-light-theme",
@@ -56,6 +61,19 @@ Here is how settings from different levels are combined.
}
```
- **System Overrides `settings.json`:**
```json
{
"theme": "system-enforced-theme",
"mcpServers": {
"corp-server": {
"command": "/usr/local/bin/corp-server-prod"
}
},
"includeDirectories": ["/etc/gemini-cli/global-context"]
}
```
This results in the following merged configuration:
- **Final Merged Configuration:**
@@ -74,18 +92,19 @@ This results in the following merged configuration:
}
},
"includeDirectories": [
"/etc/gemini-cli/global-context",
"/etc/gemini-cli/common-context",
"~/gemini-context",
"./project-context"
"./project-context",
"/etc/gemini-cli/global-context"
]
}
```
**Why:**
- **`theme`**: The value from the system settings is used, overriding both user and workspace settings.
- **`mcpServers`**: The objects are merged. The `corp-server` definition from the system settings takes precedence over the user's definition. The unique `user-tool` and `project-tool` are included.
- **`includeDirectories`**: The arrays are concatenated in the order of System, User, and then Workspace.
- **`theme`**: The value from the system overrides (`system-enforced-theme`) is used, as it has the highest precedence.
- **`mcpServers`**: The objects are merged. The `corp-server` definition from the system overrides takes precedence over the user's definition. The unique `user-tool` and `project-tool` are included.
- **`includeDirectories`**: The arrays are concatenated in the order of System Defaults, User, Workspace, and then System Overrides.
- **Location**:
- **Linux**: `/etc/gemini-cli/settings.json`

View File

@@ -53,6 +53,7 @@ import {
loadSettings,
USER_SETTINGS_PATH, // This IS the mocked path.
getSystemSettingsPath,
getSystemDefaultsPath,
SETTINGS_DIRECTORY_NAME, // This is from the original module, but used by the mock.
SettingScope,
} from './settings.js';
@@ -317,6 +318,68 @@ describe('Settings Loading and Merging', () => {
});
});
it('should merge all settings files with the correct precedence', () => {
(mockFsExistsSync as Mock).mockReturnValue(true);
const systemDefaultsContent = {
theme: 'default-theme',
sandbox: true,
telemetry: true,
includeDirectories: ['/system/defaults/dir'],
};
const userSettingsContent = {
theme: 'user-theme',
contextFileName: 'USER_CONTEXT.md',
includeDirectories: ['/user/dir1', '/user/dir2'],
};
const workspaceSettingsContent = {
sandbox: false,
contextFileName: 'WORKSPACE_CONTEXT.md',
includeDirectories: ['/workspace/dir'],
};
const systemSettingsContent = {
theme: 'system-theme',
telemetry: false,
includeDirectories: ['/system/dir'],
};
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
if (p === getSystemDefaultsPath())
return JSON.stringify(systemDefaultsContent);
if (p === getSystemSettingsPath())
return JSON.stringify(systemSettingsContent);
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.systemDefaults.settings).toEqual(systemDefaultsContent);
expect(settings.system.settings).toEqual(systemSettingsContent);
expect(settings.user.settings).toEqual(userSettingsContent);
expect(settings.workspace.settings).toEqual(workspaceSettingsContent);
expect(settings.merged).toEqual({
theme: 'system-theme',
sandbox: false,
telemetry: false,
contextFileName: 'WORKSPACE_CONTEXT.md',
customThemes: {},
mcpServers: {},
includeDirectories: [
'/system/defaults/dir',
'/user/dir1',
'/user/dir2',
'/workspace/dir',
'/system/dir',
],
chatCompression: {},
});
});
it('should ignore folderTrust from workspace settings', () => {
(mockFsExistsSync as Mock).mockReturnValue(true);
const userSettingsContent = {
@@ -806,6 +869,9 @@ describe('Settings Loading and Merging', () => {
const systemSettingsContent = {
includeDirectories: ['/system/dir'],
};
const systemDefaultsContent = {
includeDirectories: ['/system/defaults/dir'],
};
const userSettingsContent = {
includeDirectories: ['/user/dir1', '/user/dir2'],
};
@@ -817,6 +883,8 @@ describe('Settings Loading and Merging', () => {
(p: fs.PathOrFileDescriptor) => {
if (p === getSystemSettingsPath())
return JSON.stringify(systemSettingsContent);
if (p === getSystemDefaultsPath())
return JSON.stringify(systemDefaultsContent);
if (p === USER_SETTINGS_PATH)
return JSON.stringify(userSettingsContent);
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
@@ -828,10 +896,11 @@ describe('Settings Loading and Merging', () => {
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.includeDirectories).toEqual([
'/system/dir',
'/system/defaults/dir',
'/user/dir1',
'/user/dir2',
'/workspace/dir',
'/system/dir',
]);
});
@@ -1290,6 +1359,11 @@ describe('Settings Loading and Merging', () => {
expect(loadedSettings.system.settings.theme).toBe('ocean');
expect(loadedSettings.merged.theme).toBe('ocean');
// SystemDefaults theme is overridden by user, workspace, and system themes
loadedSettings.setValue(SettingScope.SystemDefaults, 'theme', 'default');
expect(loadedSettings.systemDefaults.settings.theme).toBe('default');
expect(loadedSettings.merged.theme).toBe('ocean');
});
});

View File

@@ -40,12 +40,23 @@ export function getSystemSettingsPath(): string {
}
}
export function getSystemDefaultsPath(): string {
if (process.env['GEMINI_CLI_SYSTEM_DEFAULTS_PATH']) {
return process.env['GEMINI_CLI_SYSTEM_DEFAULTS_PATH'];
}
return path.join(
path.dirname(getSystemSettingsPath()),
'system-defaults.json',
);
}
export type { DnsResolutionOrder } from './settingsSchema.js';
export enum SettingScope {
User = 'User',
Workspace = 'Workspace',
System = 'System',
SystemDefaults = 'SystemDefaults',
}
export interface CheckpointingSettings {
@@ -73,6 +84,7 @@ export interface SettingsFile {
function mergeSettings(
system: Settings,
systemDefaults: Settings,
user: Settings,
workspace: Settings,
isTrusted: boolean,
@@ -83,29 +95,43 @@ function mergeSettings(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { folderTrust, ...safeWorkspaceWithoutFolderTrust } = safeWorkspace;
// Settings are merged with the following precedence (last one wins for
// single values):
// 1. System Defaults
// 2. User Settings
// 3. Workspace Settings
// 4. System Settings (as overrides)
//
// For properties that are arrays (e.g., includeDirectories), the arrays
// are concatenated. For objects (e.g., customThemes), they are merged.
return {
...systemDefaults,
...user,
...safeWorkspaceWithoutFolderTrust,
...system,
customThemes: {
...(systemDefaults.customThemes || {}),
...(user.customThemes || {}),
...(safeWorkspace.customThemes || {}),
...(system.customThemes || {}),
},
mcpServers: {
...(systemDefaults.mcpServers || {}),
...(user.mcpServers || {}),
...(safeWorkspace.mcpServers || {}),
...(system.mcpServers || {}),
},
includeDirectories: [
...(system.includeDirectories || []),
...(systemDefaults.includeDirectories || []),
...(user.includeDirectories || []),
...(safeWorkspace.includeDirectories || []),
...(system.includeDirectories || []),
],
chatCompression: {
...(system.chatCompression || {}),
...(systemDefaults.chatCompression || {}),
...(user.chatCompression || {}),
...(safeWorkspace.chatCompression || {}),
...(system.chatCompression || {}),
},
};
}
@@ -113,12 +139,14 @@ function mergeSettings(
export class LoadedSettings {
constructor(
system: SettingsFile,
systemDefaults: SettingsFile,
user: SettingsFile,
workspace: SettingsFile,
errors: SettingsError[],
isTrusted: boolean,
) {
this.system = system;
this.systemDefaults = systemDefaults;
this.user = user;
this.workspace = workspace;
this.errors = errors;
@@ -127,6 +155,7 @@ export class LoadedSettings {
}
readonly system: SettingsFile;
readonly systemDefaults: SettingsFile;
readonly user: SettingsFile;
readonly workspace: SettingsFile;
readonly errors: SettingsError[];
@@ -141,6 +170,7 @@ export class LoadedSettings {
private computeMergedSettings(): Settings {
return mergeSettings(
this.system.settings,
this.systemDefaults.settings,
this.user.settings,
this.workspace.settings,
this.isTrusted,
@@ -155,6 +185,8 @@ export class LoadedSettings {
return this.workspace;
case SettingScope.System:
return this.system;
case SettingScope.SystemDefaults:
return this.systemDefaults;
default:
throw new Error(`Invalid scope: ${scope}`);
}
@@ -331,10 +363,12 @@ export function loadEnvironment(settings?: Settings): void {
*/
export function loadSettings(workspaceDir: string): LoadedSettings {
let systemSettings: Settings = {};
let systemDefaultSettings: Settings = {};
let userSettings: Settings = {};
let workspaceSettings: Settings = {};
const settingsErrors: SettingsError[] = [];
const systemSettingsPath = getSystemSettingsPath();
const systemDefaultsPath = getSystemDefaultsPath();
// Resolve paths to their canonical representation to handle symlinks
const resolvedWorkspaceDir = path.resolve(workspaceDir);
@@ -368,6 +402,25 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
});
}
// Load system defaults
try {
if (fs.existsSync(systemDefaultsPath)) {
const systemDefaultsContent = fs.readFileSync(
systemDefaultsPath,
'utf-8',
);
const parsedSystemDefaults = JSON.parse(
stripJsonComments(systemDefaultsContent),
) as Settings;
systemDefaultSettings = resolveEnvVarsInObject(parsedSystemDefaults);
}
} catch (error: unknown) {
settingsErrors.push({
message: getErrorMessage(error),
path: systemDefaultsPath,
});
}
// Load user settings
try {
if (fs.existsSync(USER_SETTINGS_PATH)) {
@@ -419,6 +472,7 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
// Create a temporary merged settings object to pass to loadEnvironment.
const tempMergedSettings = mergeSettings(
systemSettings,
systemDefaultSettings,
userSettings,
workspaceSettings,
isTrusted,
@@ -439,6 +493,10 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
path: systemSettingsPath,
settings: systemSettings,
},
{
path: systemDefaultsPath,
settings: systemDefaultSettings,
},
{
path: USER_SETTINGS_PATH,
settings: userSettings,

View File

@@ -146,8 +146,13 @@ describe('gemini.tsx main function', () => {
path: '/system/settings.json',
settings: {},
};
const systemDefaultsFile: SettingsFile = {
path: '/system/system-defaults.json',
settings: {},
};
const mockLoadedSettings = new LoadedSettings(
systemSettingsFile,
systemDefaultsFile,
userSettingsFile,
workspaceSettingsFile,
[settingsError],

View File

@@ -287,6 +287,10 @@ describe('App UI', () => {
path: '/system/settings.json',
settings: settings.system || {},
};
const systemDefaultsFile: SettingsFile = {
path: '/system/system-defaults.json',
settings: {},
};
const userSettingsFile: SettingsFile = {
path: '/user/settings.json',
settings: settings.user || {},
@@ -297,6 +301,7 @@ describe('App UI', () => {
};
return new LoadedSettings(
systemSettingsFile,
systemDefaultsFile,
userSettingsFile,
workspaceSettingsFile,
[],

View File

@@ -34,6 +34,10 @@ describe('AuthDialog', () => {
settings: { customThemes: {}, mcpServers: {} },
path: '',
},
{
settings: {},
path: '',
},
{
settings: {
selectedAuthType: AuthType.USE_GEMINI,
@@ -74,6 +78,10 @@ describe('AuthDialog', () => {
},
path: '',
},
{
settings: {},
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
path: '',
@@ -108,6 +116,10 @@ describe('AuthDialog', () => {
},
path: '',
},
{
settings: {},
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
path: '',
@@ -142,6 +154,10 @@ describe('AuthDialog', () => {
},
path: '',
},
{
settings: {},
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
path: '',
@@ -177,6 +193,10 @@ describe('AuthDialog', () => {
},
path: '',
},
{
settings: {},
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
path: '',
@@ -207,6 +227,10 @@ describe('AuthDialog', () => {
},
path: '',
},
{
settings: {},
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
path: '',
@@ -239,6 +263,10 @@ describe('AuthDialog', () => {
},
path: '',
},
{
settings: {},
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
path: '',
@@ -271,6 +299,10 @@ describe('AuthDialog', () => {
settings: { customThemes: {}, mcpServers: {} },
path: '',
},
{
settings: {},
path: '',
},
{
settings: {
selectedAuthType: undefined,
@@ -311,6 +343,10 @@ describe('AuthDialog', () => {
settings: { customThemes: {}, mcpServers: {} },
path: '',
},
{
settings: {},
path: '',
},
{
settings: {
selectedAuthType: undefined,
@@ -354,6 +390,10 @@ describe('AuthDialog', () => {
settings: { customThemes: {}, mcpServers: {} },
path: '',
},
{
settings: {},
path: '',
},
{
settings: {
selectedAuthType: AuthType.USE_GEMINI,

View File

@@ -43,6 +43,10 @@ const createMockSettings = (
settings: { customThemes: {}, mcpServers: {}, ...systemSettings },
path: '/system/settings.json',
},
{
settings: {},
path: '/system/system-defaults.json',
},
{
settings: {
customThemes: {},
@@ -155,6 +159,10 @@ describe('SettingsDialog', () => {
settings: { customThemes: {}, mcpServers: {}, ...systemSettings },
path: '/system/settings.json',
},
{
settings: {},
path: '/system/system-defaults.json',
},
{
settings: {
customThemes: {},

View File

@@ -22,6 +22,7 @@ describe('<MarkdownDisplay />', () => {
{ path: '', settings: {} },
{ path: '', settings: {} },
{ path: '', settings: {} },
{ path: '', settings: {} },
[],
true,
);
@@ -221,6 +222,7 @@ Another paragraph.
it('hides line numbers in code blocks when showLineNumbers is false', () => {
const text = '```javascript\nconst x = 1;\n```'.replace(/\n/g, EOL);
const settings = new LoadedSettings(
{ path: '', settings: {} },
{ path: '', settings: {} },
{ path: '', settings: { showLineNumbers: false } },
{ path: '', settings: {} },