Skip to content

LP1 HIGH false positive when allowed-tools uses scoped grants like Bash(git:*) #766

Open

Description

@Zhuoxi2000

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:

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 main at 2226747; the lookup is unchanged on current main (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

  1. Zhuoxi2000 commented on Oct 6, 2026

    @Zhuoxi2000
    ContributorAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions