Repository navigation
LP1 HIGH false positive when allowed-tools uses scoped grants like Bash(git:*) #766
Description
What happens
mcp_least_privilege raises LP1 (HIGH, confidence 0.75) for the shell capability when a SKILL.md declares a scoped Bash grant such as Bash(git:*). The same skill with a bare Bash grant gets no LP1. A narrower grant scores worse than the broader one.
Cause: _map_allowed_tools_to_categories looks up the whole entry in _TOOL_TO_CAPABILITY:
cat = _TOOL_TO_CAPABILITY.get(tool.lower().strip())"bash(git:*)" is not a key, so the entry maps to no category, and every capability it should cover is reported as undeclared. The same applies to WebFetch(domain:...), Read(...), Edit(...), and so on.
Minimal repro
SKILL.md:
---
name: git-status
description: Report git status of the repo.
allowed-tools: Bash(git:*) Bash(python3:*) Read
---
Run scripts/run.py to print git status.scripts/run.py:
import subprocess
subprocess.run(['git', 'status'], check=False)skillspector scan --no-llm ./skill
allowed-tools |
Result on main (2226747) |
|---|---|
Bash Read |
score 9, LOW, SAFE (AST4 only) |
Bash(git:*) Bash(python3:*) Read |
score 33, MEDIUM, CAUTION, plus HIGH: LP1 - Code capability 'shell' detected in scripts/run.py but not covered by declared permissions. |
The list form (allowed-tools: [Bash(git:*)]) and the comma form (Bash(git status:*), Read) behave the same way.
Expected vs actual
Expected: Bash(git:*) covers the shell category, as bare Bash already does (test_allowed_tools_fully_covered_no_lp1).
Actual: LP1 HIGH for shell.
Why this form should be recognised:
- The Agent Skills specification's own
allowed-toolsexample uses it:allowed-tools: Bash(git:*) Bash(jq:*) Read. Claude Code's skill and permission docs useBash(cmd:*)entries too. - SkillSpector already parses this
Tool(specifier)syntax elsewhere (_permission_rule_is_valid/_permission_allow_rule_is_validinbundled_execution_surface.pysplit on(). - A strictly narrower grant of the same tool should not produce a HIGH finding when the broader grant produces none. As things stand, the only way to clear the finding without touching the code is to widen
Bash(git:*)to bareBash. - Space-separated
allowed-toolsis parsed as a single tool name, so LP1 fires on fully-declared skills #327 mentions in passing that theallowed-tools: Bash(python:*) Read Writesnippet from LP1/LP3 remediation points at apermissionsfield that the Agent Skills spec does not define #300 "declares nothing under the current parser". fix: parse space-separated allowed-tools strings (fixes #327) #330 fixed whitespace splitting only, so the scoped form is still unhandled.
This is separate from #674 / #685, which changed how LP1 frames allowed-tools as preapproval. That change did not touch the tool-name lookup, so on current main a scoped grant still maps to no category. Whatever the framing, a scoped grant should count at least as much as the bare tool name.
Environment
- SkillSpector
mainat 2226747; the lookup is unchanged on currentmain(b6f0304), where_map_allowed_tools_to_categories(['Bash(git:*)', 'Bash(python3:*)', 'Read'])still returns only{'file_read'} - Python 3.13, macOS (arm64)
--no-llm, so the result is deterministic
Suggested fix
In _map_allowed_tools_to_categories, look up only the tool name before an optional (, so Bash(...) maps to shell, WebFetch(...) to network, Read(...) to file_read, and so on. This also covers the first fragment of a space-split entry such as Bash(git status:*) Read, because that fragment is Bash(git. Add tests for the list, space and comma forms, plus a negative control: Read(./docs/**) with subprocess code must still raise LP1 for shell.
I have a fix and regression tests ready on a branch and will open a PR referencing this issue.
Activity
Zhuoxi2000 commentedon Oct 6, 2026
I'd like to take this one myself. The fix and regression tests are ready, and I'll open a PR that references this issue, as CONTRIBUTING.md asks.