[BUG] Bash permissions in settings.json not enforced - requires custom hook workaround
Bug Report: Bash Permissions in settings.json Not Respected
Summary
The permissions.allow and permissions.deny rules for Bash commands in settings.json are not reliably enforced. Users must create custom PreToolUse hooks to achieve the permission behavior that the configuration system promises.
Environment
- Claude Code Version: Latest (check with
claude --version) - OS: macOS (Darwin 25.2.0)
- Interface: CLI (Terminal)
Steps to Reproduce
- Create
~/.claude/settings.jsonwith Bash permissions:
{
"permissions": {
"allow": [
"Bash(mkdir:*)",
"Bash(ls:*)",
"Bash(git status:*)"
],
"deny": [
"Bash(rm:*)"
]
}
}
- Start a new Claude Code session
- Ask Claude to run
mkdir -p /tmp/test - Expected: Command runs without permission prompt
- Actual: Permission prompt appears despite the allow rule
Expected Behavior
Commands matching patterns in permissions.allow should execute without prompting.
Commands matching patterns in permissions.deny should be blocked automatically.
Actual Behavior
- Allow rules are ignored for Bash commands
- Users are prompted for every Bash command regardless of configuration
- This forces users to either:
- Use
--dangerously-skip-permissions(unsafe) - Click "Allow" hundreds of times per session
- Implement custom PreToolUse hooks (workaround I had to use)
Related Issues
- #15921: VSCode Extension doesn't respect Bash/Write/Edit permissions at all
- #13340: Piped commands bypass permission allowlist
Impact
This is a high-friction issue that significantly degrades the user experience:
- Productivity loss: Users spend time approving commands that should be auto-approved
- Workaround complexity: Users must write custom Python hooks to get basic functionality
- Documentation mismatch: The settings.json documentation implies this should work
- Trust erosion: Users lose confidence in the permission system
Workaround
I created a PreToolUse hook that properly enforces the allow/deny rules:
#!/usr/bin/env python3
"""PreToolUse hook that enforces Bash permissions from settings.json."""
import json
import sys
from pathlib import Path
def load_settings():
settings_file = Path.home() / ".claude" / "settings.json"
allow_patterns = []
deny_patterns = []
if settings_file.exists():
with open(settings_file, 'r') as f:
settings = json.load(f)
permissions = settings.get("permissions", {})
for rule in permissions.get("allow", []):
if rule.startswith("Bash(") and rule.endswith(")"):
allow_patterns.append(rule[5:-1])
for rule in permissions.get("deny", []):
if rule.startswith("Bash(") and rule.endswith(")"):
deny_patterns.append(rule[5:-1])
return allow_patterns, deny_patterns
def pattern_matches(pattern, command):
command = command.strip()
if pattern.endswith(":*"):
return command.startswith(pattern[:-2])
if pattern.endswith(" *"):
return command.startswith(pattern[:-2])
return command.startswith(pattern)
def main():
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError:
sys.exit(0)
if input_data.get("tool_name") != "Bash":
sys.exit(0)
command = input_data.get("tool_input", {}).get("command", "")
allow_patterns, deny_patterns = load_settings()
# Deny takes priority
for pattern in deny_patterns:
if pattern_matches(pattern, command):
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": f"Denied: {pattern}"
}
}))
sys.exit(0)
# Check allow
for pattern in allow_patterns:
if pattern_matches(pattern, command):
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": f"Allowed: {pattern}"
}
}))
sys.exit(0)
sys.exit(0)
if __name__ == "__main__":
main()
Suggested Fix
The built-in permission system should:
- Actually enforce the allow/deny rules in settings.json for Bash commands
- Handle piped commands by checking each component (as noted in #13340)
- Work consistently across CLI and VSCode extension
- Document limitations clearly if certain patterns can't be supported
Additional Context
This user had 800+ Bash command patterns meticulously configured in settings.json, expecting them to work as documented. The discovery that none of them were being enforced was extremely frustrating and time-consuming to debug.
---
Submitted by: User via Claude Code session
Date: 2026-01-17
Showing cached comments. Read the full discussion on GitHub ↗
11 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Update: Same Issue Affects Edit and Write Tools
This bug isn't limited to Bash commands — Edit and Write tool permissions are also not enforced.
Reproduction
settings.json:~/code/Workaround: PreToolUse Hook for Edit/Write
I extended the hook approach to cover file operations:
Hook Configuration
Add to
settings.json:Summary
The permission system appears to be completely broken for all tool types:
The documented
permissions.allowpatterns are not being checked by the core permission system, forcing users to implement their own enforcement via hooks.🦗 🦗 🦗
The current state of permissions and how they are repeatedly requested is insanely bad and needs to be fixed ASAP. It has major impact on productivity. Further, when multiple agents are executing, the responsivity of permissions prompts shown is virtually close to none and the TUI becomes unusable.
@lucasmccomb
Using fnmatch enables you to place wildcards () anywhere in your patterns (e.g., mkdir _output_*), allowing you to match commands with variable middle sections.
Additionally, the permissionDecisionReason returned in hookSpecificOutput perfectly supports these patterns, clearly identifying which wildcard rule authorized the action.
for example in settings.json:
`"permissions": {
"allow": [
// Matches any mkdir command have _output_
"Bash(mkdir -p _output_)",
// Matches any .log file within any "logs" directory at any depth
"Read(/logs/.log)",
// Matches any python script in the src directory of any project user
"Edit(/Users//project/src/.py)"
]
}`
update in auto-approve-*.py:
`
import fnmatch
def path_matches_pattern(file_path: str, pattern: str) -> bool:
file_path = os.path.normpath(file_path)
# Support wildcard matching (glob pattern)
if '*' in pattern and fnmatch.fnmatch(file_path, pattern):
return True
if "" in pattern:
base_path = pattern.replace("", "").rstrip("/")
return file_path.startswith(base_path)
return False
`
thank you @Sendarg! I will look into adding this.
I just posted https://github.com/anthropics/claude-code/issues/24832 and am curious if this is the same issue or just similar. Is permissions.bash.blocked the same feature as permissions.deny? My config format is different but also not enforced.
Had a similar issue on "Claude Version: 2.1.22 (Claude Code)" where the agent committed and pushed, even though he should not have.
Claude used for that
This should not been possible due to my settings (at least from a user point of view, I understand why it was possible)
```json {
{
"permissions": {
"deny": [
"Bash(git commit )", "Bash(git push )"
]
}
}
You've identified the right solution. Here's a complete hook that replaces the broken
permissions.allow/denysystem:The hook receives the raw command via JSON, splits compound commands itself, and checks each component. It doesn't depend on the pattern matcher that has the known bugs (wildcard matching, escape handling, heredoc parsing, compound command splitting).
Deny rules are also enforced reliably — the hook checks them first and
exit 2blocks the command regardless.thank you for the workaround, this is golden!!! I had to ask claude to rewrite it into node tho since I had no python installed but it worked 🤣
I haven't had an issue with this anymore. Closing.