Skip to content

Commit 5ad5aa5

Browse files
Ilanlidoclaude
andcommitted
CM-58331: Add --ide all option for AI guardrails commands
- Add support for `--ide all` to install/uninstall/status hooks for all IDEs at once - Update validate_and_parse_ide to return None for "all" - Split Claude Code PreToolUse into PreToolUse:Read and PreToolUse:mcp for better status visibility - Report results separately for each IDE when using --ide all Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b0625ac commit 5ad5aa5

File tree

7 files changed

+107
-47
lines changed

7 files changed

+107
-47
lines changed

cycode/cli/apps/ai_guardrails/command_utils.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,26 @@
1212
console = Console()
1313

1414

15-
def validate_and_parse_ide(ide: str) -> AIIDEType:
16-
"""Validate IDE parameter and convert to AIIDEType enum.
15+
def validate_and_parse_ide(ide: str) -> Optional[AIIDEType]:
16+
"""Validate IDE parameter, returning None for 'all'.
1717
1818
Args:
19-
ide: IDE name string (e.g., 'cursor')
19+
ide: IDE name string (e.g., 'cursor', 'claude-code', 'all')
2020
2121
Returns:
22-
AIIDEType enum value
22+
AIIDEType enum value, or None if 'all' was specified
2323
2424
Raises:
2525
typer.Exit: If IDE is invalid
2626
"""
27+
if ide.lower() == 'all':
28+
return None
2729
try:
2830
return AIIDEType(ide.lower())
2931
except ValueError:
3032
valid_ides = ', '.join([ide_type.value for ide_type in AIIDEType])
3133
console.print(
32-
f'[red]Error:[/] Invalid IDE "{ide}". Supported IDEs: {valid_ides}',
34+
f'[red]Error:[/] Invalid IDE "{ide}". Supported IDEs: {valid_ides}, all',
3335
style='bold red',
3436
)
3537
raise typer.Exit(1) from None

cycode/cli/apps/ai_guardrails/consts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def _get_claude_code_hooks_dir() -> Path:
6060
hooks_dir=_get_claude_code_hooks_dir(),
6161
repo_hooks_subdir='.claude',
6262
hooks_file_name='settings.json',
63-
hook_events=['UserPromptSubmit', 'PreToolUse'],
63+
hook_events=['UserPromptSubmit', 'PreToolUse:Read', 'PreToolUse:mcp'],
6464
),
6565
}
6666

cycode/cli/apps/ai_guardrails/hooks_manager.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,15 @@ def get_hooks_status(scope: str = 'user', repo_path: Optional[Path] = None, ide:
203203
ide_config = IDE_CONFIGS[ide]
204204
has_cycode_hooks = False
205205
for event in ide_config.hook_events:
206-
entries = existing.get('hooks', {}).get(event, [])
206+
# Handle event:matcher format
207+
if ':' in event:
208+
actual_event, matcher_prefix = event.split(':', 1)
209+
all_entries = existing.get('hooks', {}).get(actual_event, [])
210+
# Filter entries by matcher
211+
entries = [e for e in all_entries if e.get('matcher', '').startswith(matcher_prefix)]
212+
else:
213+
entries = existing.get('hooks', {}).get(event, [])
214+
207215
cycode_entries = [e for e in entries if is_cycode_hook_entry(e)]
208216
if cycode_entries:
209217
has_cycode_hooks = True

cycode/cli/apps/ai_guardrails/install_command.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
validate_and_parse_ide,
1212
validate_scope,
1313
)
14-
from cycode.cli.apps.ai_guardrails.consts import IDE_CONFIGS
14+
from cycode.cli.apps.ai_guardrails.consts import IDE_CONFIGS, AIIDEType
1515
from cycode.cli.apps.ai_guardrails.hooks_manager import install_hooks
1616
from cycode.cli.utils.sentry import add_breadcrumb
1717

@@ -30,7 +30,7 @@ def install_command(
3030
str,
3131
typer.Option(
3232
'--ide',
33-
help='IDE to install hooks for (e.g., "cursor"). Defaults to cursor.',
33+
help='IDE to install hooks for (e.g., "cursor", "claude-code", or "all" for all IDEs). Defaults to cursor.',
3434
),
3535
] = 'cursor',
3636
repo_path: Annotated[
@@ -54,6 +54,7 @@ def install_command(
5454
cycode ai-guardrails install # Install for all projects (user scope)
5555
cycode ai-guardrails install --scope repo # Install for current repo only
5656
cycode ai-guardrails install --ide cursor # Install for Cursor IDE
57+
cycode ai-guardrails install --ide all # Install for all supported IDEs
5758
cycode ai-guardrails install --scope repo --repo-path /path/to/repo
5859
"""
5960
add_breadcrumb('ai-guardrails-install')
@@ -62,17 +63,35 @@ def install_command(
6263
validate_scope(scope)
6364
repo_path = resolve_repo_path(scope, repo_path)
6465
ide_type = validate_and_parse_ide(ide)
65-
ide_name = IDE_CONFIGS[ide_type].name
66-
success, message = install_hooks(scope, repo_path, ide=ide_type)
6766

68-
if success:
69-
console.print(f'[green]✓[/] {message}')
67+
ides_to_install: list[AIIDEType] = list(AIIDEType) if ide_type is None else [ide_type]
68+
69+
results: list[tuple[str, bool, str]] = []
70+
for current_ide in ides_to_install:
71+
ide_name = IDE_CONFIGS[current_ide].name
72+
success, message = install_hooks(scope, repo_path, ide=current_ide)
73+
results.append((ide_name, success, message))
74+
75+
# Report results for each IDE
76+
any_success = False
77+
all_success = True
78+
for _ide_name, success, message in results:
79+
if success:
80+
console.print(f'[green]✓[/] {message}')
81+
any_success = True
82+
else:
83+
console.print(f'[red]✗[/] {message}', style='bold red')
84+
all_success = False
85+
86+
if any_success:
7087
console.print()
7188
console.print('[bold]Next steps:[/]')
72-
console.print(f'1. Restart {ide_name} to activate the hooks')
89+
successful_ides = [name for name, success, _ in results if success]
90+
ide_list = ', '.join(successful_ides)
91+
console.print(f'1. Restart {ide_list} to activate the hooks')
7392
console.print('2. (Optional) Customize policy in ~/.cycode/ai-guardrails.yaml')
7493
console.print()
7594
console.print('[dim]The hooks will scan prompts, file reads, and MCP tool calls for secrets.[/]')
76-
else:
77-
console.print(f'[red]✗[/] {message}', style='bold red')
95+
96+
if not all_success:
7897
raise typer.Exit(1)

cycode/cli/apps/ai_guardrails/status_command.py

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from rich.table import Table
99

1010
from cycode.cli.apps.ai_guardrails.command_utils import console, validate_and_parse_ide, validate_scope
11+
from cycode.cli.apps.ai_guardrails.consts import IDE_CONFIGS, AIIDEType
1112
from cycode.cli.apps.ai_guardrails.hooks_manager import get_hooks_status
1213
from cycode.cli.utils.sentry import add_breadcrumb
1314

@@ -26,7 +27,7 @@ def status_command(
2627
str,
2728
typer.Option(
2829
'--ide',
29-
help='IDE to check status for (e.g., "cursor"). Defaults to cursor.',
30+
help='IDE to check status for (e.g., "cursor", "claude-code", or "all" for all IDEs). Defaults to cursor.',
3031
),
3132
] = 'cursor',
3233
repo_path: Annotated[
@@ -50,6 +51,7 @@ def status_command(
5051
cycode ai-guardrails status --scope user # Show only user-level status
5152
cycode ai-guardrails status --scope repo # Show only repo-level status
5253
cycode ai-guardrails status --ide cursor # Check status for Cursor IDE
54+
cycode ai-guardrails status --ide all # Check status for all supported IDEs
5355
"""
5456
add_breadcrumb('ai-guardrails-status')
5557

@@ -59,34 +61,41 @@ def status_command(
5961
repo_path = Path(os.getcwd())
6062
ide_type = validate_and_parse_ide(ide)
6163

62-
scopes_to_check = ['user', 'repo'] if scope == 'all' else [scope]
64+
ides_to_check: list[AIIDEType] = list(AIIDEType) if ide_type is None else [ide_type]
6365

64-
for check_scope in scopes_to_check:
65-
status = get_hooks_status(check_scope, repo_path if check_scope == 'repo' else None, ide=ide_type)
66+
scopes_to_check = ['user', 'repo'] if scope == 'all' else [scope]
6667

68+
for current_ide in ides_to_check:
69+
ide_name = IDE_CONFIGS[current_ide].name
6770
console.print()
68-
console.print(f'[bold]{check_scope.upper()} SCOPE[/]')
69-
console.print(f'Path: {status["hooks_path"]}')
71+
console.print(f'[bold cyan]═══ {ide_name} ═══[/]')
72+
73+
for check_scope in scopes_to_check:
74+
status = get_hooks_status(check_scope, repo_path if check_scope == 'repo' else None, ide=current_ide)
75+
76+
console.print()
77+
console.print(f'[bold]{check_scope.upper()} SCOPE[/]')
78+
console.print(f'Path: {status["hooks_path"]}')
7079

71-
if not status['file_exists']:
72-
console.print('[dim]No hooks.json file found[/]')
73-
continue
80+
if not status['file_exists']:
81+
console.print('[dim]No hooks file found[/]')
82+
continue
7483

75-
if status['cycode_installed']:
76-
console.print('[green]✓ Cycode AI guardrails: INSTALLED[/]')
77-
else:
78-
console.print('[yellow]○ Cycode AI guardrails: NOT INSTALLED[/]')
84+
if status['cycode_installed']:
85+
console.print('[green]✓ Cycode AI guardrails: INSTALLED[/]')
86+
else:
87+
console.print('[yellow]○ Cycode AI guardrails: NOT INSTALLED[/]')
7988

80-
# Show hook details
81-
table = Table(show_header=True, header_style='bold')
82-
table.add_column('Hook Event')
83-
table.add_column('Cycode Enabled')
84-
table.add_column('Total Hooks')
89+
# Show hook details
90+
table = Table(show_header=True, header_style='bold')
91+
table.add_column('Hook Event')
92+
table.add_column('Cycode Enabled')
93+
table.add_column('Total Hooks')
8594

86-
for event, info in status['hooks'].items():
87-
enabled = '[green]Yes[/]' if info['enabled'] else '[dim]No[/]'
88-
table.add_row(event, enabled, str(info['total_entries']))
95+
for event, info in status['hooks'].items():
96+
enabled = '[green]Yes[/]' if info['enabled'] else '[dim]No[/]'
97+
table.add_row(event, enabled, str(info['total_entries']))
8998

90-
console.print(table)
99+
console.print(table)
91100

92101
console.print()

cycode/cli/apps/ai_guardrails/uninstall_command.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
validate_and_parse_ide,
1212
validate_scope,
1313
)
14-
from cycode.cli.apps.ai_guardrails.consts import IDE_CONFIGS
14+
from cycode.cli.apps.ai_guardrails.consts import IDE_CONFIGS, AIIDEType
1515
from cycode.cli.apps.ai_guardrails.hooks_manager import uninstall_hooks
1616
from cycode.cli.utils.sentry import add_breadcrumb
1717

@@ -30,7 +30,7 @@ def uninstall_command(
3030
str,
3131
typer.Option(
3232
'--ide',
33-
help='IDE to uninstall hooks from (e.g., "cursor"). Defaults to cursor.',
33+
help='IDE to uninstall hooks from (e.g., "cursor", "claude-code", "all"). Defaults to cursor.',
3434
),
3535
] = 'cursor',
3636
repo_path: Annotated[
@@ -54,20 +54,39 @@ def uninstall_command(
5454
cycode ai-guardrails uninstall # Remove user-level hooks
5555
cycode ai-guardrails uninstall --scope repo # Remove repo-level hooks
5656
cycode ai-guardrails uninstall --ide cursor # Uninstall from Cursor IDE
57+
cycode ai-guardrails uninstall --ide all # Uninstall from all supported IDEs
5758
"""
5859
add_breadcrumb('ai-guardrails-uninstall')
5960

6061
# Validate inputs
6162
validate_scope(scope)
6263
repo_path = resolve_repo_path(scope, repo_path)
6364
ide_type = validate_and_parse_ide(ide)
64-
ide_name = IDE_CONFIGS[ide_type].name
65-
success, message = uninstall_hooks(scope, repo_path, ide=ide_type)
6665

67-
if success:
68-
console.print(f'[green]✓[/] {message}')
66+
ides_to_uninstall: list[AIIDEType] = list(AIIDEType) if ide_type is None else [ide_type]
67+
68+
results: list[tuple[str, bool, str]] = []
69+
for current_ide in ides_to_uninstall:
70+
ide_name = IDE_CONFIGS[current_ide].name
71+
success, message = uninstall_hooks(scope, repo_path, ide=current_ide)
72+
results.append((ide_name, success, message))
73+
74+
# Report results for each IDE
75+
any_success = False
76+
all_success = True
77+
for _ide_name, success, message in results:
78+
if success:
79+
console.print(f'[green]✓[/] {message}')
80+
any_success = True
81+
else:
82+
console.print(f'[red]✗[/] {message}', style='bold red')
83+
all_success = False
84+
85+
if any_success:
6986
console.print()
70-
console.print(f'[dim]Restart {ide_name} for changes to take effect.[/]')
71-
else:
72-
console.print(f'[red]✗[/] {message}', style='bold red')
87+
successful_ides = [name for name, success, _ in results if success]
88+
ide_list = ', '.join(successful_ides)
89+
console.print(f'[dim]Restart {ide_list} for changes to take effect.[/]')
90+
91+
if not all_success:
7392
raise typer.Exit(1)

tests/cli/commands/ai_guardrails/test_command_utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ def test_validate_and_parse_ide_valid() -> None:
1515
assert validate_and_parse_ide('cursor') == AIIDEType.CURSOR
1616
assert validate_and_parse_ide('CURSOR') == AIIDEType.CURSOR
1717
assert validate_and_parse_ide('CuRsOr') == AIIDEType.CURSOR
18+
assert validate_and_parse_ide('claude-code') == AIIDEType.CLAUDE_CODE
19+
assert validate_and_parse_ide('Claude-Code') == AIIDEType.CLAUDE_CODE
20+
assert validate_and_parse_ide('all') is None
1821

1922

2023
def test_validate_and_parse_ide_invalid() -> None:

0 commit comments

Comments
 (0)