Critical Security Bug: `deny` permissions in `settings.json` are not enforced**

Status Fixed / completed
Maintainer reply ✓ Yes — bogini
Activity 10 comments · opened Aug 28, 2025 · closed Sep 2, 2025
💡 Likely answer: A maintainer (bogini, collaborator) responded on this thread — see the highlighted reply below.

Title: Critical Security Bug: deny permissions in settings.json are not enforced

Labels: bug, security, critical

Description

As of version 1.0.93, the deny permission system configured in settings.json files is completely non-functional. All tested deny rules are ignored, allowing Claude Code unrestricted access to files and tools that should be blocked. This presents a significant security risk for users who rely on this documented feature to protect sensitive data and prevent dangerous operations.

Based on extensive testing (see attached test results summary), the following deny rules failed to work:

  • File Access: Read, Edit, Write rules for specific files, patterns (*.env), and recursive directories (secrets/**) were all ignored.
  • Tool Access: Denying entire tools like WebFetch was ineffective.
  • Command Access: Denying specific Bash commands (e.g., rm:*) had no effect.
  • Default Blocks: Even default-denied commands like curl were executable.

The current behavior completely contradicts the official documentation, which details a robust system for restricting agent capabilities.

Steps to Reproduce

  1. In a project directory, create a .claude/settings.json file with the following content:

``json
{
"permissions": {
"deny": [
"Read(./.env)"
]
}
}
``

  1. Create a file named .env in the same directory with some content (e.g., SECRET=123).
  2. Start claude in the terminal.
  3. Ask Claude to: read the .env file.

Expected Behavior:
Claude should be blocked from reading the file, stating that the permission is denied by the configuration.

Actual Behavior:
Claude successfully reads and displays the contents of the .env file, completely ignoring the deny rule.

Community Workaround: Using PreToolUse Hooks

Until the core deny functionality is fixed, the only reliable way to protect sensitive files is by using a PreToolUse hook. This solution intercepts tool calls before execution and can block them based on custom logic.

I am sharing my implementation below to help other users secure their environments and to provide the team with a clear example of the expected behavior.

1. Create the Hook Script (.claude/hooks/protect_sensitive_files.py)

This Python script checks the file_path of any Read or Edit operation against a list of sensitive patterns. If a match is found, it exits with code 2, which blocks the tool and feeds the stderr message back to Claude.

#!/usr/bin/env python3
import sys
import json
from pathlib import Path

# A list of file extensions and exact filenames considered sensitive.
SENSITIVE_PATTERNS = {
    '.env', '.pem', '.key', '.credential', '.token', 'credentials.json',
    'google-credentials.json', 'service-account.json', 'package-lock.json',
    'poetry.lock', 'yarn.lock'
}

def main():
    """
    Main function to process the hook input and check for sensitive file access.
    """
    try:
        # Read the JSON data passed from Claude Code via stdin
        data = json.load(sys.stdin)
        tool_input = data.get('tool_input', {})
        file_path_str = tool_input.get('file_path')

        if not file_path_str:
            # If no file path is involved, the hook doesn't need to act.
            sys.exit(0)

        file_path = Path(file_path_str)
        file_name = file_path.name
        file_extension = file_path.suffix.lower()

        # Check if the file name or extension matches our sensitive patterns
        if file_name in SENSITIVE_PATTERNS or file_extension in SENSITIVE_PATTERNS:
            # Construct a clear, educational error message for the LLM
            error_message = (
                f"SECURITY_POLICY_VIOLATION: Access to the sensitive file '{file_name}' is blocked by a hook.\n"
                f"Reason: Files like '{file_name}' often contain credentials, private keys, or locked dependencies and should not be accessed or modified by the AI.\n"
                "Action: Please use environment variables for secrets or ask for the specific information you need without reading the file directly."
            )
            
            # Print the error message to stderr
            print(error_message, file=sys.stderr)
            
            # Exit with code 2 to block the tool and feed stderr back to Claude
            sys.exit(2)

    except (json.JSONDecodeError, KeyError) as e:
        # Handle potential errors in the input data
        print(f"Error processing hook input: {e}", file=sys.stderr)
        # Exit with a non-blocking error code
        sys.exit(1)

    # If no sensitive file was detected, exit with 0 to allow the action
    sys.exit(0)

if __name__ == "__main__":
    main()

(Ensure this script is executable: chmod +x .claude/hooks/protect_sensitive_files.py)

2. Configure settings.json to Use the Hook

Add the following to your .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read|Edit|MultiEdit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/protect_sensitive_files.py"
          }
        ]
      }
    ]
  }
}
3. Test the Hook

When you ask Claude to read a blocked file (e.g., .env), it now correctly blocks the action and provides feedback:

⏺ I'll read the .env file for you.

⏺ Read(.env)
  ⎿  Error: Read operation blocked by hook:
     - [$CLAUDE_PROJECT_DIR/.claude/hooks/protect_sensitive_files.py]: SECURITY_POLICY_VIOLATION: Access to the
     sensitive file '.env' is blocked by a hook.
     Reason: Files like '.env' often contain credentials, private keys, or locked dependencies and should not be
     accessed or modified by the AI.
     Action: Please use environment variables for secrets or ask for the specific information you need without
     reading the file directly.

⏺ The hook protecting sensitive files has blocked access to the .env file. This is a security measure to
  prevent exposure of credentials and private keys that are typically stored in .env files.

Request to the Claude Code Team

  1. Acknowledge and Investigate: Please investigate this critical bug as a high-priority security issue.
  2. Fix the Core Functionality: Restore the documented behavior of the deny permission system.
  3. Update Documentation: In the meantime, please add a warning to the IAM documentation page to inform users that this feature is currently not functional and recommend using hooks as an alternative.

Thank you for your attention to this matter. This functionality is essential for the safe and secure use of Claude Code in both individual and enterprise environments.

View original on GitHub ↗

10 Comments

jamescook · 1 year ago
paulsutherland · 1 year ago

If you're implementing this security fix at the home directory level (rather than per-project), you need to update the path in ~/.claude/settings.json:

"hooks": {
  "PreToolUse": [
    {
      "matcher": "Read|Edit|MultiEdit|Write",
      "hooks": [
        {
          "type": "command",
          "command": "~/.claude/hooks/protect_sensitive_files.py"
        }
      ]
    }
  ]
}

Important notes:

  • Use ~/.claude/hooks/protect_sensitive_files.py (tilde notation works)
  • Don't use $HOME/.claude/hooks/protect_sensitive_files.py (environment variables aren't expanded in JSON)
  • The absolute path /home/username/.claude/hooks/protect_sensitive_files.py also works but is less portable

This applies the security hooks globally for all your Claude Code projects, rather than having to configure it per-project.

xdevs23 · 12 months ago

I asked Claude Code to extend the script to read the denials from settings.json. This should behave the same as expected behavior:
Since I'm using NixOS, I've modified the shebang. Feel free to change it to #!/usr/bin/env python3.

#!/usr/bin/env nix
#! nix shell nixpkgs#python3 --command python3
import sys
import json
import os
from pathlib import Path
from fnmatch import fnmatch

def load_settings():
    """Load Claude settings and extract deny patterns."""
    settings_path = Path.home() / '.claude' / 'settings.json'
    try:
        with open(settings_path) as f:
            settings = json.load(f)
            deny_patterns = settings.get('permissions', {}).get('deny', [])
            return deny_patterns
    except (FileNotFoundError, json.JSONDecodeError, KeyError):
        return []

def extract_path_pattern(deny_rule):
    """Extract file path pattern from deny rule like 'Read(./path/pattern)'."""
    if deny_rule.startswith('Read(') and deny_rule.endswith(')'):
        return deny_rule[5:-1]  # Remove 'Read(' and ')'
    return None

def matches_pattern(file_path, pattern):
    """Check if file path matches a deny pattern."""
    file_path = str(file_path)
    
    # Convert relative patterns to work with absolute paths
    if pattern.startswith('./'):
        # For patterns like './local/**', check if any part of the path matches
        pattern = pattern[2:]  # Remove './'
        # Check if the pattern matches any suffix of the path
        path_parts = file_path.split('/')
        for i in range(len(path_parts)):
            partial_path = '/'.join(path_parts[i:])
            if fnmatch(partial_path, pattern):
                return True
    else:
        # Direct pattern matching
        if fnmatch(file_path, pattern):
            return True
    
    return False

def main():
    """
    Main function to process the hook input and check for sensitive file access.
    """
    try:
        # Read the JSON data passed from Claude Code via stdin
        data = json.load(sys.stdin)
        tool_input = data.get('tool_input', {})
        file_path_str = tool_input.get('file_path')

        if not file_path_str:
            # If no file path is involved, the hook doesn't need to act.
            sys.exit(0)

        file_path = Path(file_path_str)
        
        # Load deny patterns from settings
        deny_rules = load_settings()
        
        # Check if the file path matches any deny pattern
        for rule in deny_rules:
            pattern = extract_path_pattern(rule)
            if pattern and matches_pattern(file_path, pattern):
                # Construct a clear, educational error message for the LLM
                error_message = (
                    f"SECURITY_POLICY_VIOLATION: Access to '{file_path}' is blocked by deny rule: {rule}\n"
                    f"Reason: This file matches a pattern in your Claude settings.json deny list.\n"
                    "Action: Files in denied paths contain sensitive information and should not be accessed by the AI."
                )
                
                # Print the error message to stderr
                print(error_message, file=sys.stderr)
                
                # Exit with code 2 to block the tool and feed stderr back to Claude
                sys.exit(2)

    except (json.JSONDecodeError, KeyError) as e:
        # Handle potential errors in the input data
        print(f"Error processing hook input: {e}", file=sys.stderr)
        # Exit with a non-blocking error code
        sys.exit(1)

    # If no sensitive file was detected, exit with 0 to allow the action
    sys.exit(0)

if __name__ == "__main__":
    main()
coygeek · 12 months ago

@xdevs23, wow, this is fantastic. Thanks for building on this.

This is a brilliant evolution of the workaround. Having it read the deny rules directly from settings.json is exactly the right move—it effectively polyfills the broken functionality and makes the hook a true drop-in replacement. Your implementation looks solid.

Appreciate the note on the NixOS shebang as well; good call-out for users on different distros.

This is an excellent stop-gap for the community.

Great contribution

xdevs23 · 12 months ago

@coygeek I appreciate it.

If Claude Code was open source we could simply let Claude Code implement the feature using this example.
Now we have to rely on the Claude Code developers to use Claude Code to code the Claude Code feature using the Claude Code hook mentioned above 😂

coygeek · 12 months ago

If this was open source, a security vulnerability like this would be fixed in a minute.

bogini collaborator · 12 months ago

Thank you for the detailed report and the helpful hook workaround! I've investigated this issue and found that the deny rules are likely not working due to a common but understandable confusion about pattern syntax. The pattern Read(./.env) in your example is interpreted as relative to the current working directory where you run claude, not relative to your project or settings file location. This is why the deny rule isn't matching.

See https://docs.anthropic.com/en/docs/claude-code/iam#tool-specific-permission-rules. Claude Code supports four different pattern types for file paths:

| Pattern | Meaning | Example | What it matches |
|---------|---------|---------|-----------------|
| //path | Absolute path from filesystem root | Read(//.env) | /.env (root of filesystem) |
| ~/path | Path from home directory | Read(~/.env) | /Users/you/.env |
| /path | Path relative to settings file directory | Read(/.env) | <project>/.env |
| path or ./path | Path relative to current working directory | Read(.env) or Read(./.env) | <cwd>/.env |

To block .env files in your project, you should use:

{
  "permissions": {
    "deny": [
      "Read(/.env)",      // Blocks <project>/.env (relative to settings file)
      "Read(/**/.env)"    // Blocks any .env file in project subdirectories
    ]
  }
}

Make sure you're editing .claude/settings.json in your project root, not the global user settings.

Please let me know if the corrected pattern syntax resolves the issue. If not, there may indeed be a bug that needs further investigation.

xdevs23 · 12 months ago

@bogini But in my case I have:

      "Read(./.env.local)",
      "Read(./local/**)",
      "Read(./secrets/**)",
      "Read(./local-data/**)"

And I did run claude code from the directory that contains "local" and it still read the files inside of it.

coygeek · 12 months ago

Thanks for the response @bogini

I'm on v1.0.100, and I've just re-tested the following:

Read(~/.env) => Blocks /Users/user/.env

Read(/.env) => Blocks <project>/.env
Read(/**/.env) => Blocks any .env file in project subdirectories (so for example <project>/src/.env)

Read(.env) => Blocks <cwd>/.env
Read(./.env) => Blocks <cwd>/.env

It's working as intended / normally.

I suggest updating both settings and iam documentation to be more clear of all variations / examples.

I'll leave this open, in case someone needs to contribute, but this appears to be resolved for me.

github-actions[bot] · 11 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.