[FEATURE] specificity-aware permission precedence (let a narrow allow override a broad deny)

Status Closed — duplicate
Maintainer reply None cached
Activity 3 comments · opened Jul 21, 2026 · closed Aug 15, 2026

Preflight Checklist

  • [x] I have searched existing requests and this feature hasn't been requested yet
  • [x] This is a single feature request (not multiple features)

Problem Statement

Claude Code's permission engine resolves rule conflicts with a fixed precedence: a deny always beats an allow, no matter how broad or narrow each rule is. This makes the common least-privilege pattern — block a whole tool, carve out a narrow safe exception — impossible to express.

Reproduction

settings.json:

{
  "permissions": {
    "deny":  ["Bash(aws:*)"],
    "allow": ["Bash(aws * describe-*)"]
  }
}

Expected: read-only calls like aws ec2 describe-instances are allowed; everything else under aws (e.g. aws ec2 terminate-instances) is denied.

Actual: the broad deny swallows the specific allow — every aws call is denied, including the whitelisted read-only ones.

Request

Support specificity-aware precedence, where the most specific matching rule wins in either direction:

  • a narrow allow punches through a broad deny
  • a narrow deny punches through a broad allow
  • on equal specificity, fall back to most-restrictive-tier-wins (deny > ask > allow)

This is strictly more expressive than the current model and enables real allow-list-with-exceptions policies. Making it opt-in (e.g. a permissions.precedence: "specificity" setting) would preserve backward compatibility.

Proposed Solution

Workaround

I built a PreToolUse hook, permcheck (https://github.com/saleem-mirza/permcheck), that implements exactly this (most-specific-rule-wins, fail-closed). It works, but this belongs in the native model — which is the actual security boundary — rather than in a hook layered on top.

Alternative Solutions

An update in native security model is highly desired

Priority

High - Significant impact on productivity

Feature Category

Configuration and settings

Use Case Example

Guard destructive git while allowing everyday git

Let the agent stage, commit, and pull freely, prompt before pushing, and hard-block history-rewriting or destructive commands:

{
  "permissions": {
    "deny": [
      "Bash(git push --force:*)",
      "Bash(git push -f:*)",
      "Bash(git reset --hard:*)",
      "Bash(git clean:*)"
    ],
    "ask":  ["Bash(git push:*)"],
    "allow": ["Bash(git:*)"]
  }
}

| Command | Desired | Native result |
|---|---|---|
| git add -A | ✅ allow | ✅ allow |
| git commit -m "fix" | ✅ allow | ✅ allow |
| git push origin main | ⚠️ ask | ⚠️ ask |
| git push --force origin main | ❌ deny | ✅ allow* |

\* Under native precedence the broad Bash(git:*) allow and the Bash(git push:*) ask both apply, but there's no reliable way for the narrow --force deny to win over the broad allow in the general case — you can't layer "allow git, but deny this one dangerous variant" cleanly. Specificity-aware precedence makes the longest-matching git push --force deny authoritative.

Restrict web access to trusted domains

Block the web tools broadly, allow only vetted internal/docs domains:

{
  "permissions": {
    "deny":  ["WebFetch", "WebSearch"],
    "allow": [
      "WebFetch(domain:docs.internal.company.com)",
      "WebFetch(domain:github.com)"
    ]
  }
}

| Call | Desired | Native result |
|---|---|---|
| WebFetch docs.internal.company.com/... | ✅ allow | ❌ deny |
| WebFetch github.com/... | ✅ allow | ❌ deny |
| WebFetch random-blog.example/... | ❌ deny | ❌ deny |

The broad deny on WebFetch swallows the domain-scoped allows today, so you can't express "no web access except this short allow-list" — exactly the prompt-injection guardrail teams
want.

Protect secrets even through obfuscation

Deny reads of sensitive files while allowing general file reads:

{
  "permissions": {
    "deny": [
      "Read(/**/.env*)",
      "Read(/**/.ssh/**)",
      "Read(/**/*.pem)"
    ],
    "allow": ["Read(/**)"]
  }
}

The narrow secret-file denies must win over the broad Read(/**) allow — which specificity-aware precedence guarantees, but the native "broad allow, narrow deny" combination can't be
expressed as a clean least-privilege policy.

Additional Context

_No response_

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗