claude-code

Least-privilege carve-outs for Claude Code permissions

Promtime

claude-code

permcheck, an Apache-2.0 hook engine for Claude Code written by cloud and platform architect Saleem Mirza, lets a narrow allow rule survive a broader deny in exactly one case: when the allow's match-set is a strict subset of the deny's. The engine and its reasoning are documented at Zethian, and the matcher source is public at saleem-mirza/permcheck.

At a glance

  • Before matching, the engine splits compound commands, peels wrappers such as sudo and env, and tests file operands against Read, Write and Edit denies, so an allowed cat .env still hits a secret-path deny.
  • Every verdict is an exit code: 0 allow, 1 ask, 2 deny, and 3 for a corrupt rules file, returned in about 1.7 ms, the author's median of 50 warm runs on an M3 Max.
  • On a seven-command incident session, permcheck returned three allows and four denies, where the same session under a native ask default produced seven prompts and under a blanket allow ran all seven commands unchecked.

Agent permission files are drifting toward the shape of firewall rules, and the same failure appears: a coarse deny is safe but unusable, so teams widen it until it stops protecting anything. Containment-based carve-outs read as an attempt to keep the broad restriction and the narrow exception in one reviewable file, with the trade that the proof is about match-sets, not about whether the permitted operation is actually safe.

A deny on Bash(aws:*) also blocks aws ec2 describe-instances

Claude Code resolves permissions by fixed tier precedence, so pairing deny Bash(aws:*) with allow Bash(aws * describe-*) blocks inspection along with mutation. Dropping the deny admits destructive calls. The mirror policy fails too: issue claude-code#6527, open and labelled bug and area:security, reports that a bare Bash token in allow suppresses the ask list, and rm test.txt runs unprompted.

permcheck gathers every rule matching a call and resolves in three steps: an allow or ask carves out a matching deny only when its match-set is a strict subset, any uncarved deny denies the call, and otherwise the most specific allow or ask wins, with ties going to ask. Specificity counts literal characters and adds a fixed bonus for an exact specifier.

Seven commands from one incident session end as three allows and four denies

Mirza runs one incident session through four policy shapes. Denying kubectl, aws and terraform blocks all seven commands, allowing them permits all seven, and leaving them unlisted under defaultMode ask produces seven prompts. The permcheck policy runs three inspection commands silently and denies the other four, including terraform apply and kubectl get secret db-creds.

One of those denials, aws s3 ls s3://audit-logs, is a coverage gap rather than a dangerous call, because the pattern aws * list-* misses the ls alias. Mirza reports that one engineer approved more than 700 tool calls over two days, and that a 900-plus-rule allow-list still prompted on a chain of git status and gh pr list.

Prebuilt binaries ship through the Claude Code plugin marketplace, which covers macOS, Linux and Windows and registers the hook without editing settings.json, and through Homebrew for a standalone CLI. Disabling the plugin returns every decision to the native model. The engine makes no network calls.

Auto mode's classifier pauses after three consecutive blocks or twenty in total

Anthropic's auto mode answers the same prompt fatigue with a classifier that judges whether an unresolved action fits the current request, trust boundary and conversation, with administrators describing that boundary through environment, allow, soft_deny and hard_deny entries. Three consecutive classifier blocks, or twenty in total, pause auto mode in an interactive session.

Auto mode drops broad allow rules such as Bash(*) while narrow ones such as Bash(npm test) carry over, and autoMode.classifyAllShell forces every shell command to the classifier despite those narrow allows. A block from the classifier usually returns the fixed text Blocked by classifier, while a rule engine names the rule that matched.

Because each verdict is also an exit code, a policy can be asserted in CI at no token cost. Mirza warns to assert the exact code, since a corrupt rules file exits 3, which a check for anything other than 2 would pass as safe.

What permcheck does not model

The engine reads command text and never executes a shell, so variables, aliases and eval hide their targets, sh -c and find -exec leave the inner command to defaultMode, and tar, git and rsync reach a protected file with no cross-check. PowerShell and cmd.exe get no analysis at all. Releases publish SHA256SUMS, but the build is not bit-for-bit reproducible, so the checksums pin the artifact without tying it to a source tree.

Comments

No comments yet. Be the first.

Join the conversation

Sign in with Google to leave a comment. Your name and avatar come from your Google profile, and the comment appears after moderation.

We only use your name and avatar from Google. We never store your email address.