Skip to main content

#!/usr/bin/env python3 """ Validate and clean settings.local.json permissions.

Prevents permission bloat by:

  1. Removing shell fragments (for, do, done, while, etc.)
  2. Consolidating redundant patterns
  3. Validating pattern syntax
  4. Removing malformed entries

Usage: python3 validate-settings-permissions.py [--fix] [--verbose]

Options: --fix Auto-fix issues (otherwise just report) --verbose Show detailed output """

import json import re import sys from pathlib import Path

Shell fragments that should never be permissions

SHELL_FRAGMENTS = { 'do', 'done', 'for', 'while', 'if', 'then', 'else', 'fi', 'case', 'esac', 'EOF', 'do)', 'NEW_LINE' }

Patterns that indicate a malformed permission

MALFORMED_PATTERNS = [ r'^Bash([a-z]+\s*$', # Incomplete Bash pattern r'^Bash(\s*)$', # Empty Bash pattern r'^Bash([^:]+:[^].)$', # Wildcard not at end r'^\s*$', # Empty string ]

General patterns that make specific ones redundant

GENERAL_PATTERNS = { 'git': 'Bash(git:)', 'docker': 'Bash(docker:)', 'kubectl': 'Bash(kubectl:)', 'gcloud': 'Bash(gcloud:)', 'python': 'Bash(python:)', 'python3': 'Bash(python3:)', 'npm': 'Bash(npm:)', 'npx': 'Bash(npx:)', }

def is_shell_fragment(perm: str) -> bool: """Check if permission is a shell fragment.""" if not perm.startswith('Bash('): return False

inner = perm[5:-1] if perm.endswith(')') else perm[5:]

# Check for exact shell keywords (must be the whole command or followed by space/paren)
for fragment in SHELL_FRAGMENTS:
# Must be exact match or followed by whitespace, not part of a command name
if inner == fragment or inner == f'{fragment}:*':
return True
if re.match(rf'^{re.escape(fragment)}(\s|\()', inner):
return True

# Check for variable assignments (but not command prefixes like PYTHONPATH=)
# Only flag if it's JUST a variable assignment, not a command prefix
if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*="[^"]*"$', inner):
return True

# Check for loop constructs with full syntax
if re.match(r'^for\s+\w+\s+in\s+', inner):
return True

if re.match(r'^while\s+read\s+', inner):
return True

return False

def is_malformed(perm: str) -> bool: """Check if permission is malformed.""" for pattern in MALFORMED_PATTERNS: if re.match(pattern, perm): return True return False

def is_redundant(perm: str, all_perms: set) -> bool: """Check if permission is redundant given other permissions.""" if not perm.startswith('Bash('): return False

# Specific git commit commands are covered by Bash(git commit:*)
if 'git commit' in perm and 'Bash(git commit:*)' in all_perms:
if perm != 'Bash(git commit:*)':
return True

# Specific patterns covered by general ones
for cmd, general in GENERAL_PATTERNS.items():
if general in all_perms and perm.startswith(f'Bash({cmd} '):
# Check if it's not the general pattern itself
if not perm.endswith(':*)'):
return True

return False

def is_specific_command(perm: str) -> bool: """Check if this is a specific command rather than a pattern.""" if not perm.startswith('Bash('): return False

# Patterns should end with :*)
if perm.endswith(':*)'):
return False

# Check if it contains specific paths or long content
inner = perm[5:-1] if perm.endswith(')') else perm[5:]

# Long specific commands are likely artifacts
if len(inner) > 100:
return True

# Contains absolute paths
if '/Users/' in inner or '/home/' in inner:
return True

# Contains heredoc markers
if 'EOF' in inner or '<<' in inner:
return True

return False

def validate_settings(settings_path: Path, fix: bool = False, verbose: bool = False) -> dict: """Validate and optionally fix settings file."""

if not settings_path.exists():
return {'error': f'File not found: {settings_path}'}

with open(settings_path) as f:
settings = json.load(f)

if 'permissions' not in settings or 'allow' not in settings['permissions']:
return {'error': 'No permissions.allow found in settings'}

perms = settings['permissions']['allow']
original_count = len(perms)
all_perms = set(perms)

issues = {
'shell_fragments': [],
'malformed': [],
'redundant': [],
'specific_commands': [],
}

clean_perms = []

for perm in perms:
if is_shell_fragment(perm):
issues['shell_fragments'].append(perm)
if verbose:
print(f" [SHELL FRAGMENT] {perm[:80]}...")
elif is_malformed(perm):
issues['malformed'].append(perm)
if verbose:
print(f" [MALFORMED] {perm[:80]}...")
elif is_redundant(perm, all_perms):
issues['redundant'].append(perm)
if verbose:
print(f" [REDUNDANT] {perm[:80]}...")
elif is_specific_command(perm):
issues['specific_commands'].append(perm)
if verbose:
print(f" [SPECIFIC CMD] {perm[:80]}...")
else:
clean_perms.append(perm)

total_issues = sum(len(v) for v in issues.values())

result = {
'original_count': original_count,
'clean_count': len(clean_perms),
'issues_found': total_issues,
'issues': {k: len(v) for k, v in issues.items()},
}

if fix and total_issues > 0:
settings['permissions']['allow'] = clean_perms
with open(settings_path, 'w') as f:
json.dump(settings, f, indent=2)
result['fixed'] = True
result['message'] = f"Removed {total_issues} problematic permissions"
else:
result['fixed'] = False

return result

def main(): fix = '--fix' in sys.argv verbose = '--verbose' in sys.argv

# Find settings file
settings_paths = [
Path('.claude/settings.local.json'),
Path('/Users/halcasteel/PROJECTS/coditect-rollout-master/.claude/settings.local.json'),
]

settings_path = None
for p in settings_paths:
if p.exists():
settings_path = p
break

if not settings_path:
print("ERROR: settings.local.json not found")
sys.exit(1)

print(f"Validating: {settings_path}")
result = validate_settings(settings_path, fix=fix, verbose=verbose)

if 'error' in result:
print(f"ERROR: {result['error']}")
sys.exit(1)

print(f"\nResults:")
print(f" Original permissions: {result['original_count']}")
print(f" Clean permissions: {result['clean_count']}")
print(f" Issues found: {result['issues_found']}")

if result['issues_found'] > 0:
print(f"\nIssue breakdown:")
for issue_type, count in result['issues'].items():
if count > 0:
print(f" - {issue_type}: {count}")

if result['fixed']:
print(f"\n{result['message']}")
elif result['issues_found'] > 0:
print(f"\nRun with --fix to auto-clean")
else:
print(f"\nNo issues found")

sys.exit(0 if result['issues_found'] == 0 else 1)

if name == 'main': main()