The DIS scanner collapsed Tool(param) rules to the bare tool name, so
Agent(model:opus) deny + Agent(model:sonnet) allow (and the same for
WebFetch(domain:...)) were flagged as dead config — a false positive now
that CC 2.1.178 matches Tool(param:value) and 2.1.172 adds domain rules.
The conflict-detector shared the blind spot from the other side: a
wildcard deny like WebFetch(domain:*) did not cover a
WebFetch(domain:good.com) allow, so a genuine cross-scope conflict was
missed (false negative).
New shared scanners/lib/permission-rules.mjs:
- parseRule / paramMatches (glob)
- dominates(deny, allow) -> DIS dead-allow (deny fully covers allow)
- rulesIntersect(a, b) -> CNF cross-scope conflict (match sets intersect)
DIS now delegates to dominates; conflict-detector :156 delegates to
rulesIntersect. A bare deny still covers all params, so true positives
are preserved (Bash deny + Bash(npm:*) allow still flagged).
Re-seeded the marketplace-medium snapshots: the false-positive CA-DIS
finding (Read(src/**) allow + Read(./.env) deny) is correctly gone. This
changes snapshot CONTENT only — envelope schema is unchanged, so --json
and --raw stay byte-stable.
Full suite: 837/837 green (+25). self-audit PASS, A(100)/A(97).
New DIS scanner detects tools that appear in BOTH permissions.deny
and permissions.allow within the same settings.json file. The deny
list wins, so allow entries are dead config but still load on every
turn and confuse intent.
Tool identity = bare name (everything before "("). `Bash(npm:*)` and
`Bash` are treated as the same tool, so a deny on `Bash` flags any
`Bash(...)` allow entry.
Severity: low. Wired into scan-orchestrator + scoring (area: Settings).
Fixture denied-tools-in-schema has Bash in both arrays; healthy-project
serves as the negative case.
[skip-docs] reason: v5 plan fences off README/CLAUDE.md badge updates
to Session 5; Forgejo pre-commit-docs-gate hook requires this tag.
Tests: 611 → 617 (+6).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>