Skip to content

feat: allow command-specific restrictions for ShellTool #2605

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions docs/cli/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,17 @@ In addition to a project settings file, a project's `.gemini` directory can cont
```

- **`coreTools`** (array of strings):
- **Description:** Allows you to specify a list of core tool names that should be made available to the model. This can be used to restrict the set of built-in tools. See [Built-in Tools](../core/tools-api.md#built-in-tools) for a list of core tools.
- **Description:** Allows you to specify a list of core tool names that should be made available to the model. This can be used to restrict the set of built-in tools. See [Built-in Tools](../core/tools-api.md#built-in-tools) for a list of core tools. You can also specify command-specific restrictions for tools that support it, like the `ShellTool`. For example, `"coreTools": ["ShellTool(ls -l)"]` will only allow the `ls -l` command to be executed.
- **Default:** All tools available for use by the Gemini model.
- **Example:** `"coreTools": ["ReadFileTool", "GlobTool", "SearchText"]`.
- **Example:** `"coreTools": ["ReadFileTool", "GlobTool", "ShellTool(ls)"]`.

- **`excludeTools`** (array of strings):
- **Description:** Allows you to specify a list of core tool names that should be excluded from the model. A tool listed in both `excludeTools` and `coreTools` is excluded.
- **Description:** Allows you to specify a list of core tool names that should be excluded from the model. A tool listed in both `excludeTools` and `coreTools` is excluded. You can also specify command-specific restrictions for tools that support it, like the `ShellTool`. For example, `"excludeTools": ["ShellTool(rm -rf)"]` will block the `rm -rf` command.
- **Default**: No tools excluded.
- **Example:** `"excludeTools": ["run_shell_command", "findFiles"]`.
- **Security Note:** Command-specific restrictions in
`excludeTools` for `run_shell_command` are based on simple string matching and can be easily bypassed. This feature is **not a security mechanism** and should not be relied upon to safely execute untrusted code. It is recommended to use `coreTools` to explicitly select commands
that can be executed.

- **`autoAccept`** (boolean):
- **Description:** Controls whether the CLI automatically accepts and executes tool calls that are considered safe (e.g., read-only operations) without explicit user confirmation. If set to `true`, the CLI will bypass the confirmation prompt for tools deemed safe.
Expand Down
102 changes: 102 additions & 0 deletions docs/tools/shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,105 @@ run_shell_command(command="npm run dev &", description="Start development server
- **Interactive commands:** Avoid commands that require interactive user input, as this can cause the tool to hang. Use non-interactive flags if available (e.g., `npm init -y`).
- **Error handling:** Check the `Stderr`, `Error`, and `Exit Code` fields to determine if a command executed successfully.
- **Background processes:** When a command is run in the background with `&`, the tool will return immediately and the process will continue to run in the background. The `Background PIDs` field will contain the process ID of the background process.

## Command Restrictions

You can restrict the commands that can be executed by the `run_shell_command` tool by using the `coreTools` and `excludeTools` settings in your configuration file.

- `coreTools`: If you want to restrict the `run_shell_command` tool to a specific set of commands, you can add entries to the `coreTools` list in the format `ShellTool(<command>)`. For example, `"coreTools": ["ShellTool(ls -l)"]` will only allow the `ls -l` command to be executed. If you include `ShellTool` as a general entry in the `coreTools` list, it will act as a wildcard and allow any command to be executed, even if you have other specific commands in the list.
- `excludeTools`: If you want to block specific commands, you can add entries to the `excludeTools` list in the format `ShellTool(<command>)`. For example, `"excludeTools": ["ShellTool(rm -rf /)"]` will block the `rm -rf /` command.

### Command Restriction Examples

Here are some examples of how to use the `coreTools` and `excludeTools` settings to control which commands can be executed.

**Allow only specific commands**

To allow only `ls -l` and `git status`, and block all other commands:

```json
{
"coreTools": ["ShellTool(ls -l)", "ShellTool(git status)"]
}
```

- `ls -l`: Allowed
- `git status`: Allowed
- `npm install`: Blocked

**Block specific commands**

To block `rm -rf /` and `npm install`, and allow all other commands:

```json
{
"excludeTools": ["ShellTool(rm -rf /)", "ShellTool(npm install)"]
}
```

- `rm -rf /`: Blocked
- `npm install`: Blocked
- `ls -l`: Allowed

**Allow all commands**

To allow any command to be executed, you can use the `ShellTool` wildcard in `coreTools`:

```json
{
"coreTools": ["ShellTool"]
}
```

- `ls -l`: Allowed
- `npm install`: Allowed
- `any other command`: Allowed

**Wildcard with specific allowed commands**

If you include the `ShellTool` wildcard along with specific commands, the wildcard takes precedence, and all commands are allowed.

```json
{
"coreTools": ["ShellTool", "ShellTool(ls -l)"]
}
```

- `ls -l`: Allowed
- `npm install`: Allowed
- `any other command`: Allowed

**Wildcard with a blocklist**

You can use the `ShellTool` wildcard to allow all commands, while still blocking specific commands using `excludeTools`.

```json
{
"coreTools": ["ShellTool"],
"excludeTools": ["ShellTool(rm -rf /)"]
}
```

- `rm -rf /`: Blocked
- `ls -l`: Allowed
- `npm install`: Allowed

**Block all shell commands**

To block all shell commands, you can add the `ShellTool` wildcard to `excludeTools`:

```json
{
"excludeTools": ["ShellTool"]
}
```

- `ls -l`: Blocked
- `npm install`: Blocked
- `any other command`: Blocked

## Security Note for `excludeTools`

Command-specific restrictions in
`excludeTools` for `run_shell_command` are based on simple string matching and can be easily bypassed. This feature is **not a security mechanism** and should not be relied upon to safely execute untrusted code. It is recommended to use `coreTools` to explicitly select commands
that can be executed.
38 changes: 23 additions & 15 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,25 +456,33 @@ export class Config {
export function createToolRegistry(config: Config): Promise<ToolRegistry> {
const registry = new ToolRegistry(config);
const targetDir = config.getTargetDir();
const tools = config.getCoreTools()
? new Set(config.getCoreTools())
: undefined;
const excludeTools = config.getExcludeTools()
? new Set(config.getExcludeTools())
: undefined;

// helper to create & register core tools that are enabled
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const registerCoreTool = (ToolClass: any, ...args: unknown[]) => {
// check both the tool name (.Name) and the class name (.name)
if (
// coreTools contain tool name
(!tools || tools.has(ToolClass.Name) || tools.has(ToolClass.name)) &&
// excludeTools don't contain tool name
(!excludeTools ||
(!excludeTools.has(ToolClass.Name) &&
!excludeTools.has(ToolClass.name)))
) {
const className = ToolClass.name;
const toolName = ToolClass.Name || className;
const coreTools = config.getCoreTools();
const excludeTools = config.getExcludeTools();

let isEnabled = false;
if (coreTools === undefined) {
isEnabled = true;
} else {
isEnabled = coreTools.some(
(tool) =>
tool === className ||
tool === toolName ||
tool.startsWith(`${className}(`) ||
tool.startsWith(`${toolName}(`),
);
}

if (excludeTools?.includes(className) || excludeTools?.includes(toolName)) {
isEnabled = false;
}

if (isEnabled) {
registry.registerTool(new ToolClass(...args));
}
};
Expand Down
171 changes: 171 additions & 0 deletions packages/core/src/tools/shell.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { expect, describe, it } from 'vitest';
import { ShellTool } from './shell.js';
import { Config } from '../config/config.js';

describe('ShellTool', () => {
it('should allow a command if no restrictions are provided', async () => {
const config = {
getCoreTools: () => undefined,
getExcludeTools: () => undefined,
} as Config;
const shellTool = new ShellTool(config);
const isAllowed = shellTool.isCommandAllowed('ls -l');
expect(isAllowed).toBe(true);
});

it('should allow a command if it is in the allowed list', async () => {
const config = {
getCoreTools: () => ['ShellTool(ls -l)'],
getExcludeTools: () => undefined,
} as unknown as Config;
const shellTool = new ShellTool(config);
const isAllowed = shellTool.isCommandAllowed('ls -l');
expect(isAllowed).toBe(true);
});

it('should block a command if it is not in the allowed list', async () => {
const config = {
getCoreTools: () => ['ShellTool(ls -l)'],
getExcludeTools: () => undefined,
} as unknown as Config;
const shellTool = new ShellTool(config);
const isAllowed = shellTool.isCommandAllowed('rm -rf /');
expect(isAllowed).toBe(false);
});

it('should block a command if it is in the blocked list', async () => {
const config = {
getCoreTools: () => undefined,
getExcludeTools: () => ['ShellTool(rm -rf /)'],
} as unknown as Config;
const shellTool = new ShellTool(config);
const isAllowed = shellTool.isCommandAllowed('rm -rf /');
expect(isAllowed).toBe(false);
});

it('should allow a command if it is not in the blocked list', async () => {
const config = {
getCoreTools: () => undefined,
getExcludeTools: () => ['ShellTool(rm -rf /)'],
} as unknown as Config;
const shellTool = new ShellTool(config);
const isAllowed = shellTool.isCommandAllowed('ls -l');
expect(isAllowed).toBe(true);
});

it('should block a command if it is in both the allowed and blocked lists', async () => {
const config = {
getCoreTools: () => ['ShellTool(rm -rf /)'],
getExcludeTools: () => ['ShellTool(rm -rf /)'],
} as unknown as Config;
const shellTool = new ShellTool(config);
const isAllowed = shellTool.isCommandAllowed('rm -rf /');
expect(isAllowed).toBe(false);
});

it('should allow any command when ShellTool is in coreTools without specific commands', async () => {
const config = {
getCoreTools: () => ['ShellTool'],
getExcludeTools: () => [],
} as unknown as Config;
const shellTool = new ShellTool(config);
const isAllowed = shellTool.isCommandAllowed('any command');
expect(isAllowed).toBe(true);
});

it('should block any command when ShellTool is in excludeTools without specific commands', async () => {
const config = {
getCoreTools: () => [],
getExcludeTools: () => ['ShellTool'],
} as unknown as Config;
const shellTool = new ShellTool(config);
const isAllowed = shellTool.isCommandAllowed('any command');
expect(isAllowed).toBe(false);
});

it('should allow a command if it is in the allowed list using the public-facing name', async () => {
const config = {
getCoreTools: () => ['run_shell_command(ls -l)'],
getExcludeTools: () => undefined,
} as unknown as Config;
const shellTool = new ShellTool(config);
const isAllowed = shellTool.isCommandAllowed('ls -l');
expect(isAllowed).toBe(true);
});

it('should block a command if it is in the blocked list using the public-facing name', async () => {
const config = {
getCoreTools: () => undefined,
getExcludeTools: () => ['run_shell_command(rm -rf /)'],
} as unknown as Config;
const shellTool = new ShellTool(config);
const isAllowed = shellTool.isCommandAllowed('rm -rf /');
expect(isAllowed).toBe(false);
});

it('should block any command when ShellTool is in excludeTools using the public-facing name', async () => {
const config = {
getCoreTools: () => [],
getExcludeTools: () => ['run_shell_command'],
} as unknown as Config;
const shellTool = new ShellTool(config);
const isAllowed = shellTool.isCommandAllowed('any command');
expect(isAllowed).toBe(false);
});

it('should block any command if coreTools contains an empty ShellTool command list using the public-facing name', async () => {
const config = {
getCoreTools: () => ['run_shell_command()'],
getExcludeTools: () => [],
} as unknown as Config;
const shellTool = new ShellTool(config);
const isAllowed = shellTool.isCommandAllowed('any command');
expect(isAllowed).toBe(false);
});

it('should block any command if coreTools contains an empty ShellTool command list', async () => {
const config = {
getCoreTools: () => ['ShellTool()'],
getExcludeTools: () => [],
} as unknown as Config;
const shellTool = new ShellTool(config);
const isAllowed = shellTool.isCommandAllowed('any command');
expect(isAllowed).toBe(false);
});

it('should block a command with extra whitespace if it is in the blocked list', async () => {
const config = {
getCoreTools: () => undefined,
getExcludeTools: () => ['ShellTool(rm -rf /)'],
} as unknown as Config;
const shellTool = new ShellTool(config);
const isAllowed = shellTool.isCommandAllowed(' rm -rf / ');
expect(isAllowed).toBe(false);
});

it('should allow any command when ShellTool is present with specific commands', async () => {
const config = {
getCoreTools: () => ['ShellTool', 'ShellTool(ls)'],
getExcludeTools: () => [],
} as unknown as Config;
const shellTool = new ShellTool(config);
const isAllowed = shellTool.isCommandAllowed('any command');
expect(isAllowed).toBe(true);
});

it('should block a command on the blocklist even with a wildcard allow', async () => {
const config = {
getCoreTools: () => ['ShellTool'],
getExcludeTools: () => ['ShellTool(rm -rf /)'],
} as unknown as Config;
const shellTool = new ShellTool(config);
const isAllowed = shellTool.isCommandAllowed('rm -rf /');
expect(isAllowed).toBe(false);
});
});
Loading