fix(cli): allow disabling hostname in sandbox for rootless containers

This PR introduces a new `setHostname` configuration option (and `GEMINI_CLI_SANDBOX_SET_HOSTNAME` environment variable) to allow disabling the `--hostname` argument when starting Docker or Podman sandboxes.

In rootless nested container environments, attempting to set the hostname can fail if `CAP_SYS_ADMIN` is not available in the ambient capability set. By setting `setHostname: false`, users can bypass this requirement and successfully run the sandbox in such environments.

The setting defaults to `true` to maintain existing behavior and descriptive hostnames in the CLI footer.

Fixes: #26880

cc @danielweis @tommasosciortino
This commit is contained in:
gemini-cli[bot]
2026-05-18 21:48:27 +00:00
parent 792654c88b
commit e4962d9235
8 changed files with 104 additions and 2 deletions
+1
View File
@@ -131,6 +131,7 @@ they appear in the UI.
| UI Label | Setting | Description | Default |
| -------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Sandbox Set Hostname | `tools.sandboxSetHostname` | Whether to set the hostname in the sandbox container. Set to false to avoid Mutating UTS namespace, which is required for some rootless container environments. | `true` |
| Sandbox Allowed Paths | `tools.sandboxAllowedPaths` | List of additional paths that the sandbox is allowed to access. | `[]` |
| Sandbox Network Access | `tools.sandboxNetworkAccess` | Whether the sandbox is allowed to access the network. | `false` |
| Enable Interactive Shell | `tools.shell.enableInteractiveShell` | Use node-pty for an interactive shell experience. Fallback to child_process still applies. | `true` |
+7
View File
@@ -1553,6 +1553,13 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `undefined`
- **Requires restart:** Yes
- **`tools.sandboxSetHostname`** (boolean):
- **Description:** Whether to set the hostname in the sandbox container. Set
to false to avoid Mutating UTS namespace, which is required for some
rootless container environments.
- **Default:** `true`
- **Requires restart:** Yes
- **`tools.sandboxAllowedPaths`** (array):
- **Description:** List of additional paths that the sandbox is allowed to
access.
+21 -1
View File
@@ -132,6 +132,16 @@ export async function loadSandboxConfig(
let sandboxValue: boolean | string | null | undefined;
let allowedPaths: string[] = [];
let networkAccess = true;
let setHostname = settings.tools?.sandboxSetHostname ?? true;
// Environment variable override
const envSetHostname = process.env['GEMINI_CLI_SANDBOX_SET_HOSTNAME']?.toLowerCase().trim();
if (envSetHostname === 'false') {
setHostname = false;
} else if (envSetHostname === 'true') {
setHostname = true;
}
let customImage: string | undefined;
if (
@@ -143,6 +153,9 @@ export async function loadSandboxConfig(
sandboxValue = config.enabled ? (config.command ?? true) : false;
allowedPaths = config.allowedPaths ?? [];
networkAccess = config.networkAccess ?? true;
if (config.setHostname !== undefined) {
setHostname = config.setHostname;
}
customImage = config.image;
} else if (typeof sandboxOption !== 'object' || sandboxOption === null) {
sandboxValue = sandboxOption;
@@ -163,6 +176,13 @@ export async function loadSandboxConfig(
command === 'lxc';
return command && (image || isNative)
? { enabled: true, allowedPaths, networkAccess, command, image }
? {
enabled: true,
allowedPaths,
networkAccess,
command,
image,
setHostname,
}
: undefined;
}
+12
View File
@@ -1552,6 +1552,18 @@ const SETTINGS_SCHEMA = {
`,
showInDialog: false,
},
sandboxSetHostname: {
type: 'boolean',
label: 'Sandbox Set Hostname',
category: 'Tools',
requiresRestart: true,
default: true,
description: oneLine`
Whether to set the hostname in the sandbox container.
Set to false to avoid Mutating UTS namespace, which is required for some rootless container environments.
`,
showInDialog: true,
},
sandboxAllowedPaths: {
type: 'array',
label: 'Sandbox Allowed Paths',
+50
View File
@@ -419,6 +419,56 @@ describe('sandbox', () => {
);
});
it('should skip setting hostname if setHostname is false', async () => {
const config: SandboxConfig = createMockSandboxConfig({
command: 'docker',
image: 'gemini-cli-sandbox',
setHostname: false,
});
interface MockProcessWithStdout extends EventEmitter {
stdout: EventEmitter;
}
const mockImageCheckProcess = new EventEmitter() as MockProcessWithStdout;
mockImageCheckProcess.stdout = new EventEmitter();
vi.mocked(spawn).mockImplementationOnce(() => {
setTimeout(() => {
mockImageCheckProcess.stdout.emit('data', Buffer.from('image-id'));
mockImageCheckProcess.emit('close', 0);
}, 1);
return mockImageCheckProcess as unknown as ReturnType<typeof spawn>;
});
const mockSpawnProcess = new EventEmitter() as unknown as ReturnType<
typeof spawn
>;
mockSpawnProcess.on = vi.fn().mockImplementation((event, cb) => {
if (event === 'close') {
setTimeout(() => cb(0), 10);
}
return mockSpawnProcess;
});
vi.mocked(spawn).mockImplementationOnce(() => mockSpawnProcess);
await expect(
start_sandbox(config, [], undefined, ['arg1']),
).resolves.toBe(0);
const containerName = 'gemini-cli-sandbox-a1b2c3d4e5f6';
expect(spawn).toHaveBeenNthCalledWith(
2,
'docker',
expect.not.arrayContaining(['--hostname']),
expect.objectContaining({ stdio: 'inherit' }),
);
expect(spawn).toHaveBeenNthCalledWith(
2,
'docker',
expect.arrayContaining(['--name', containerName]),
expect.anything(),
);
});
it('should pull image if missing', async () => {
const config: SandboxConfig = createMockSandboxConfig({
command: 'docker',
+4 -1
View File
@@ -509,7 +509,10 @@ export async function start_sandbox(
'hex',
)}`;
debugLogger.log(`ContainerName: ${containerName}`);
args.push('--name', containerName, '--hostname', containerName);
args.push('--name', containerName);
if (config.setHostname !== false) {
args.push('--hostname', containerName);
}
// copy GEMINI_CLI_TEST_VAR for integration tests
if (process.env['GEMINI_CLI_TEST_VAR']) {
+2
View File
@@ -523,6 +523,7 @@ export interface SandboxConfig {
allowedPaths?: string[];
includeDirectories?: string[];
networkAccess?: boolean;
setHostname?: boolean;
command?:
| 'docker'
| 'podman'
@@ -540,6 +541,7 @@ export const ConfigSchema = z.object({
allowedPaths: z.array(z.string()).default([]),
includeDirectories: z.array(z.string()).default([]),
networkAccess: z.boolean().default(false),
setHostname: z.boolean().default(true),
command: z
.enum([
'docker',
+7
View File
@@ -2730,6 +2730,13 @@
"markdownDescription": "Legacy full-process sandbox execution environment. Set to a boolean to enable or disable the sandbox, provide a string path to a sandbox profile, or specify an explicit sandbox command (e.g., \"docker\", \"podman\", \"lxc\", \"windows-native\").\n\n- Category: `Tools`\n- Requires restart: `yes`",
"$ref": "#/$defs/BooleanOrStringOrObject"
},
"sandboxSetHostname": {
"title": "Sandbox Set Hostname",
"description": "Whether to set the hostname in the sandbox container. Set to false to avoid Mutating UTS namespace, which is required for some rootless container environments.",
"markdownDescription": "Whether to set the hostname in the sandbox container. Set to false to avoid Mutating UTS namespace, which is required for some rootless container environments.\n\n- Category: `Tools`\n- Requires restart: `yes`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"sandboxAllowedPaths": {
"title": "Sandbox Allowed Paths",
"description": "List of additional paths that the sandbox is allowed to access.",