[FEATURE] Permission deny list: blacklist specific commands without LLM hooks
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
When running Claude Code with --dangerously-skip-permissions (because manual approval on every tool call is impractical for real workflows), there's no way to blacklist specific dangerous commands while allowing everything else.
The current hook system (PreToolUse) is the only mechanism to block commands, but it requires an LLM-powered judgment call on every tool invocation. This causes:
- High false-positive rate: Hooks that try to block patterns like
git push --forceordrizzle-kit pushfrequently misfire on completely safe commands (git push origin mainblocked because the hook LLM sees "push" and gets confused). The hook output literally shows the LLM arguing with itself about whether to allow the command.
- All-or-nothing permissions: The only built-in options are "allow everything" (
--dangerously-skip-permissions) or "prompt for everything" (default). There's no middle ground for "allow everything EXCEPT these specific patterns."
- Overhead: Every Bash tool call pays the latency cost of the hook evaluation, even for
lsorgit status.
The most common real-world need is simple: "run autonomously, but never execute these 5 specific command patterns." This should be trivial to configure, but currently requires a fragile hook setup that generates more noise than protection.
Proposed Solution
Add a deny_patterns (or blocked_commands) option to settings.json or .claude/settings.json that takes a list of regex or glob patterns to match against Bash commands. Any matching command is blocked with a clear error message. No LLM evaluation needed.
{
"permissions": {
"deny": [
"git push --force",
"git reset --hard",
"drizzle-kit push",
"drizzle-kit migrate",
"rm -rf",
"sudo *"
]
}
}
Behavior:
- Patterns matched as substrings or globs against the full command string
- Blocked commands show a clear message: "Command blocked by deny rule:
<pattern>" - No LLM evaluation, no latency overhead, no false positives
- Works alongside
--dangerously-skip-permissions(deny rules still enforced) - Scoped per-project (
.claude/settings.json) or global (~/.claude/settings.json)
Alternative Solutions
- Current
PreToolUsehooks: Work in theory but use LLM-based evaluation that produces frequent false positives. A hook blockingpush --forcewill sometimes blockgit push origin mainbecause the LLM sees "push" and deliberates. The hook output shows the LLM literally reasoning "this is not force push... but it involves pushing... when in doubt, ALLOW... but wait..." before ultimately blocking anyway. - Default permission mode (no
--dangerously-skip-permissions): Requires manual approval for every single tool call. Completely impractical for any real workflow. - Allowlist approach: Some users create allowlists of permitted commands, but this is impossibly verbose. You'd need to enumerate every possible
git,npm,bun,ls,grepinvocation. The common case is "allow everything except 5 things," not "block everything except 500 things."
Priority
Critical - Blocking my work
Feature Category
Configuration and settings
Use Case Example
- I work on a project with a production database, Stripe integration, and git repo
- I want Claude to run autonomously (
--dangerously-skip-permissions) for productivity - But I need to prevent:
drizzle-kit push/migrate(untracked DB mutations),git push --force(destructive),sudo(privilege escalation) - I configure 5 deny patterns in
.claude/settings.json - Claude runs freely for 99.9% of operations, hard-blocked on the 5 dangerous ones
- No false positives, no latency overhead, no LLM deliberation on every command
Additional Context
The PreToolUse hook's LLM-based approach is fundamentally wrong for command blocking. Pattern matching is a solved problem. When I write a rule "block push --force," I mean the literal string push --force in the command. I don't need an LLM to interpret whether git push origin main is "kind of like" a force push. The current system adds complexity, latency, and unreliability to something that should be a simple string match.
This issue has 6 comments on GitHub. Read the full discussion on GitHub ↗