Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 24 additions & 9 deletions src/session_analytics/patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -551,13 +551,21 @@ def analyze_failures(


def load_allowed_commands(settings_path: Path = DEFAULT_SETTINGS_PATH) -> set[str]:
"""Load allowed commands from Claude Code settings.json.
"""Load allowed base commands from Claude Code settings.json.

Parses Bash permission patterns and extracts base commands:
- Bash(gh:*) → gh
- Bash(gh pr view:*) → gh
- Bash(git status:*) → git

This means a command like `gh` won't be reported as a permission gap
if ANY pattern for `gh` exists (e.g., `Bash(gh pr view:*)`).

Args:
settings_path: Path to settings.json

Returns:
Set of allowed command prefixes
Set of base commands that have any configured pattern
"""
if not settings_path.exists():
return set()
Expand All @@ -566,16 +574,23 @@ def load_allowed_commands(settings_path: Path = DEFAULT_SETTINGS_PATH) -> set[st
with open(settings_path) as f:
settings = json.load(f)

allowed = set()
base_commands = set()
permissions = settings.get("permissions", {})

# Look for allow patterns with Bash(command:*)
for pattern in permissions.get("allow", []):
if pattern.startswith("Bash(") and pattern.endswith(":*)"):
cmd = pattern[5:-3] # Extract command from "Bash(cmd:*)"
allowed.add(cmd)

return allowed
if pattern.startswith("Bash(") and ":*)" in pattern:
# Extract full command from "Bash(command args:*)"
# Find the position of ":*)" to handle patterns correctly
start = 5 # len("Bash(")
end = pattern.find(":*)")
if end > start:
full_cmd = pattern[start:end]
# Extract base command (first word)
base_cmd = full_cmd.split()[0] if full_cmd else None
if base_cmd:
base_commands.add(base_cmd)

return base_commands
except (json.JSONDecodeError, OSError) as e:
logger.warning(f"Could not load settings.json: {e}")
return set()
Expand Down
50 changes: 50 additions & 0 deletions tests/test_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,56 @@ def test_permission_gaps_respects_allowed(self, pattern_storage):
assert "make" not in pattern_keys
assert "git" in pattern_keys

def test_load_allowed_commands_extracts_base_from_subcommands(self):
"""Test that subcommand patterns extract the base command.

Patterns like Bash(gh pr view:*) should extract 'gh' as the base command,
so that 'gh' isn't reported as a gap when subcommand patterns exist.
"""
with tempfile.TemporaryDirectory() as tmpdir:
settings_path = Path(tmpdir) / "settings.json"
settings_path.write_text(
'{"permissions": {"allow": ['
'"Bash(gh pr view:*)", '
'"Bash(gh issue list:*)", '
'"Bash(git status:*)", '
'"Bash(cargo build:*)"'
"]}}"
)
allowed = load_allowed_commands(settings_path)

# Should extract base commands, not full subcommands
assert "gh" in allowed
assert "git" in allowed
assert "cargo" in allowed

# Should NOT contain full subcommand strings
assert "gh pr view" not in allowed
assert "git status" not in allowed

def test_permission_gaps_filters_subcommand_patterns(self, pattern_storage):
"""Test that gaps are filtered when subcommand patterns exist.

If settings.json has Bash(gh pr view:*), then 'gh' should not be
reported as a permission gap even though it's used frequently.
"""
with tempfile.TemporaryDirectory() as tmpdir:
settings_path = Path(tmpdir) / "settings.json"
# Only git subcommand patterns configured, make is NOT configured
settings_path.write_text(
'{"permissions": {"allow": ["Bash(git status:*)", "Bash(git diff:*)"]}}'
)

patterns = compute_permission_gaps(
pattern_storage, days=7, threshold=1, settings_path=settings_path
)

pattern_keys = {p.pattern_key for p in patterns}
# git has subcommand patterns, should be filtered out
assert "git" not in pattern_keys
# make has no patterns, should still be a gap
assert "make" in pattern_keys


class TestComputeAllPatterns:
"""Tests for computing all patterns."""
Expand Down