feat(dis): flag forbidden-param permission rules CC silently ignores

Extends the DIS scanner and its shared permission-rules lib with a third
documented Claude Code permission footgun. Verified verbatim against
code.claude.com/docs/en/permissions (fetched 2026-06-19).

CC's Tool(param:value) matching (2.1.178) is off-limits for a tool's own
canonicalizing field — CC ignores such a rule and emits a startup warning,
because e.g. Bash(command:rm *) is bypassable by a compound command. The
forbidden fields: command (Bash/PowerShell), file_path (Read/Edit/Write),
path (Grep/Glob), notebook_path (NotebookEdit), url (WebFetch).

- lib/permission-rules.mjs: new forbiddenParamRule(entry) returning
  { tool, key, hint } or null. Only the param:value form (colon present)
  whose key equals the tool's forbidden field is flagged; Bash(npm:*),
  WebFetch(domain:host), Agent(model:opus), and Bash(command) (no colon)
  are left valid. FORBIDDEN_PARAMS map is the single source of truth.
- DIS: scans allow + deny + ask and splits severity by intent — deny/ask
  hits are false security (medium: the block never applies), allow hits are
  dead config (low: param:value matching is deny/ask-only). Two findings,
  permissions-hygiene, CA-DIS-NNN.
- 11 new tests (7 lib, 4 DIS) + 1 fixture forbidden-param-permissions
  (force-added past .gitignore .claude/). Suite 918 -> 929. Snapshot
  unchanged (SC-5 byte-equal), contamination grep clean, gitleaks clean.
  README/CLAUDE document the broadened DIS mandate; test badge synced.
  self-audit: PASS, configGrade A 97, pluginGrade A 100, scanners 13.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
This commit is contained in:
Kjell Tore Guttormsen 2026-06-19 14:04:25 +02:00
commit d678765fad
7 changed files with 269 additions and 6 deletions

View file

@ -12,7 +12,7 @@
![Commands](https://img.shields.io/badge/commands-18-green)
![Agents](https://img.shields.io/badge/agents-6-orange)
![Hooks](https://img.shields.io/badge/hooks-4-red)
![Tests](https://img.shields.io/badge/tests-918+-brightgreen)
![Tests](https://img.shields.io/badge/tests-929+-brightgreen)
![License](https://img.shields.io/badge/license-MIT-lightgrey)
A Claude Code plugin that checks configuration health, suggests context-aware improvements, and auto-fixes issues — `CLAUDE.md`, `settings.json`, hooks, rules, MCP servers, `@imports`, and plugins. 13 deterministic scanners across 10 quality areas, context-aware feature recommendations, auto-fix with backup/rollback, a prompt-cache-aware Token Hotspots scanner with optional API-calibrated `--accurate-tokens` mode, plus cache-prefix stability, dead-tool, and cross-plugin collision detection. Zero external dependencies.
@ -309,7 +309,7 @@ By default, `/config-audit` auto-detects scope from your git context. Override w
| `feature-gap-scanner.mjs` | GAP | 25 feature checks shown as opportunities, not grades — plus a conditional `disableBundledSkills` recommendation when the active skill listing is over budget |
| `token-hotspots.mjs` | TOK | Cache-breaking volatile content, redundant tool permissions, deep import chains, oversized cascades, bloated skill descriptions, MCP tool-schema budget |
| `cache-prefix-scanner.mjs` | CPS | Volatile content in lines 31150 of the CLAUDE.md cascade — beyond the cache-prefix window but still re-loaded every turn |
| `disabled-in-schema-scanner.mjs` | DIS | Dead/ineffective permission entries: (1) tools in BOTH `permissions.deny` and `permissions.allow` — deny wins (incl. the `Tool(*)` deny-all glob, equivalent to a bare deny); (2) unanchored allow wildcards (`*`, `B*`, `mcp__*`) that Claude Code silently skips — valid only as `mcp__<server>__*` |
| `disabled-in-schema-scanner.mjs` | DIS | Dead/ineffective permission entries: (1) tools in BOTH `permissions.deny` and `permissions.allow` — deny wins (incl. the `Tool(*)` deny-all glob, equivalent to a bare deny); (2) unanchored allow wildcards (`*`, `B*`, `mcp__*`) that Claude Code silently skips — valid only as `mcp__<server>__*`; (3) `Tool(param:value)` rules whose key is the tool's own canonicalizing field (`command`/`file_path`/`path`/`notebook_path`/`url`) — CC ignores these and emits a startup warning |
| `collision-scanner.mjs` | COL | Cross-plugin skill name collisions; user-vs-plugin overlaps |
> **Cross-scanner remediation — diagnosis meets the fix.** SKL diagnoses an over-budget
@ -330,6 +330,17 @@ By default, `/config-audit` auto-detects scope from your git context. Override w
> both signals earn their place. The 200k/1M window constants live in the shared
> `scanners/lib/context-window.mjs` (single source of truth with the skill-listing budget).
> **Permission rules CC silently ignores — severity follows intent.** `Tool(param:value)`
> matching is real (CC 2.1.178), but the tool's own canonicalizing fields are off-limits:
> `command` (Bash/PowerShell), `file_path` (Read/Edit/Write), `path` (Grep/Glob),
> `notebook_path` (NotebookEdit), `url` (WebFetch). CC ignores a rule keyed on its tool's
> field and emits a startup warning, because `Bash(command:rm *)` is bypassable by a compound
> command. DIS splits severity by where the rule lives: in **deny/ask** it is **false
> security** (medium — the block you intended never applies), in **allow** it is **dead
> config** (low — `param:value` matching is deny/ask-only, so the entry grants nothing). The
> predicate lives in `scanners/lib/permission-rules.mjs`; valid forms like `Bash(npm:*)`,
> `WebFetch(domain:host)`, and `Agent(model:opus)` are never flagged.
### CLI Tools
All tools work standalone — no Claude Code session needed: