What happened?
docs/extensions/best-practices.md recommends this manifest as the way to keep a
powerful tool in check:
{
"name": "my-safe-extension",
"excludeTools": ["run_shell_command(rm -rf *)"]
}
This ensures the CLI blocks dangerous commands even if the model attempts to
execute them.
docs/extensions/reference.md:162-166 gives the same guidance, and the repo
ships an example manifest using the same shape:
// packages/cli/src/commands/extensions/examples/exclude-tools/gemini-extension.json
{
"name": "excludeTools",
"version": "1.0.0",
"excludeTools": ["run_shell_command(rm -rf)"]
}
Extension excludeTools entries are matched by exact string equality, so an
entry containing (...) never matches a tool and nothing is excluded.
Config.getExcludeTools() collects the entries into a Set
(packages/core/src/config/config.ts:2431-2439) and ToolRegistry tests
membership directly (packages/core/src/tools/tool-registry.ts:637):
return !possibleNames.some((name) => excludeTools?.has(name));
possibleNames holds the tool's real names (run_shell_command, its class
name, and MCP-qualified variants), none of which equal
"run_shell_command(rm -rf *)".
How can this be reproduced?
Applying that matching to the documented manifests:
excludeTools: ["run_shell_command(rm -rf)"] -> run_shell_command still active
excludeTools: ["run_shell_command(rm -rf *)"] -> run_shell_command still active
excludeTools: ["run_shell_command"] -> run_shell_command excluded
Only the bare tool name has any effect. The documented form silently does
nothing — no warning is emitted that the entry was ignored.
Why this is a docs problem rather than a code one
The parenthesised toolName(args) syntax is real, but it belongs to a different
setting. tools.core and tools.allowed are parsed by mapToolsToRules
(packages/core/src/policy/config.ts:445-476), which splits the shape into a
tool name plus an args pattern. Extension excludeTools does not go through
that path, and tools.exclude — the settings-level equivalent — is already
deprecated in favour of the policy engine (#18508), which is documented in
docs/tools/shell.md:158 and docs/cli/enterprise.md:267.
For the "block one specific command" use case the extension docs are describing,
the policy engine already supports it directly, and extensions can ship policies
in a policies/ directory (docs/reference/policy-engine.md:143,
packages/cli/src/config/extension-manager.ts:932):
toolName = "run_shell_command"
commandPrefix = "rm -rf"
decision = "deny"
priority = 100
So the extension docs appear to have been missed when that guidance moved to the
policy engine.
What did you expect to happen?
The extension documentation and the shipped example should show a form that
actually takes effect — either the bare tool name, or a policy rule for
command-level blocking — rather than a form that is silently ignored while the
text promises it "blocks dangerous commands".
A documentation PR is linked below.
Client information
Client Information
> /about
About Gemini CLI
CLI Version 0.56.0-nightly.20260806.g761f604c1
Git Commit 5411f113c
Model gemini-3-pro-preview
Sandbox no sandbox
OS darwin
Platform: macOS. The matching path is platform independent — it compares
configuration strings only.
Login information
Not applicable. This concerns tool filtering configuration, independent of the
authentication method.
Anything else we need to know?
Severity. This is not an exploit path and needs no attacker. A user who
copies the documented snippet ends up without the exclusion they think they
configured; run_shell_command then follows the default write policy, which
still prompts for confirmation rather than running silently. The issue is the
gap between what the docs promise and what the configuration does.
Related. #17728 reported the same exact-match behaviour for the
settings-level tools.exclude and was closed as stale; that setting has since
been deprecated by #18508. Extension excludeTools carries no deprecation
marker and is still the documented mechanism, which is why the docs matter here.
What happened?
docs/extensions/best-practices.mdrecommends this manifest as the way to keep apowerful tool in check:
{ "name": "my-safe-extension", "excludeTools": ["run_shell_command(rm -rf *)"] }docs/extensions/reference.md:162-166gives the same guidance, and the repoships an example manifest using the same shape:
Extension
excludeToolsentries are matched by exact string equality, so anentry containing
(...)never matches a tool and nothing is excluded.Config.getExcludeTools()collects the entries into aSet(
packages/core/src/config/config.ts:2431-2439) andToolRegistrytestsmembership directly (
packages/core/src/tools/tool-registry.ts:637):possibleNamesholds the tool's real names (run_shell_command, its classname, and MCP-qualified variants), none of which equal
"run_shell_command(rm -rf *)".How can this be reproduced?
Applying that matching to the documented manifests:
Only the bare tool name has any effect. The documented form silently does
nothing — no warning is emitted that the entry was ignored.
Why this is a docs problem rather than a code one
The parenthesised
toolName(args)syntax is real, but it belongs to a differentsetting.
tools.coreandtools.allowedare parsed bymapToolsToRules(
packages/core/src/policy/config.ts:445-476), which splits the shape into atool name plus an args pattern. Extension
excludeToolsdoes not go throughthat path, and
tools.exclude— the settings-level equivalent — is alreadydeprecated in favour of the policy engine (#18508), which is documented in
docs/tools/shell.md:158anddocs/cli/enterprise.md:267.For the "block one specific command" use case the extension docs are describing,
the policy engine already supports it directly, and extensions can ship policies
in a
policies/directory (docs/reference/policy-engine.md:143,packages/cli/src/config/extension-manager.ts:932):So the extension docs appear to have been missed when that guidance moved to the
policy engine.
What did you expect to happen?
The extension documentation and the shipped example should show a form that
actually takes effect — either the bare tool name, or a policy rule for
command-level blocking — rather than a form that is silently ignored while the
text promises it "blocks dangerous commands".
A documentation PR is linked below.
Client information
Client Information
Platform: macOS. The matching path is platform independent — it compares
configuration strings only.
Login information
Not applicable. This concerns tool filtering configuration, independent of the
authentication method.
Anything else we need to know?
Severity. This is not an exploit path and needs no attacker. A user who
copies the documented snippet ends up without the exclusion they think they
configured;
run_shell_commandthen follows the default write policy, whichstill prompts for confirmation rather than running silently. The issue is the
gap between what the docs promise and what the configuration does.
Related. #17728 reported the same exact-match behaviour for the
settings-level
tools.excludeand was closed as stale; that setting has sincebeen deprecated by #18508. Extension
excludeToolscarries no deprecationmarker and is still the documented mechanism, which is why the docs matter here.