Skip to content

[BUG] Bash permissions in settings.json not enforced - requires custom hook workaround #18846

@lucasmccomb

Description

@lucasmccomb

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

  1. Create ~/.claude/settings.json with Bash permissions:
{
  "permissions": {
    "allow": [
      "Bash(mkdir:*)",
      "Bash(ls:*)",
      "Bash(git status:*)"
    ],
    "deny": [
      "Bash(rm:*)"
    ]
  }
}
  1. Start a new Claude Code session
  2. Ask Claude to run mkdir -p /tmp/test
  3. Expected: Command runs without permission prompt
  4. 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

Impact

This is a high-friction issue that significantly degrades the user experience:

  1. Productivity loss: Users spend time approving commands that should be auto-approved
  2. Workaround complexity: Users must write custom Python hooks to get basic functionality
  3. Documentation mismatch: The settings.json documentation implies this should work
  4. 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:

  1. Actually enforce the allow/deny rules in settings.json for Bash commands
  2. Handle piped commands by checking each component (as noted in [BUG] global/local settings.json allow permissions are not respected by Claude Code #13340)
  3. Work consistently across CLI and VSCode extension
  4. 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

Metadata

Metadata

Assignees

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions