cycode 3.8.11.dev1__py3-none-any.whl → 3.9.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cycode/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = '3.8.11.dev1' # DON'T TOUCH. Placeholder. Will be filled automatically on poetry build from Git Tag
1
+ __version__ = '3.9.1' # DON'T TOUCH. Placeholder. Will be filled automatically on poetry build from Git Tag
@@ -12,24 +12,26 @@ from cycode.cli.apps.ai_guardrails.consts import AIIDEType
12
12
  console = Console()
13
13
 
14
14
 
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'.
17
17
 
18
18
  Args:
19
- ide: IDE name string (e.g., 'cursor')
19
+ ide: IDE name string (e.g., 'cursor', 'claude-code', 'all')
20
20
 
21
21
  Returns:
22
- AIIDEType enum value
22
+ AIIDEType enum value, or None if 'all' was specified
23
23
 
24
24
  Raises:
25
25
  typer.Exit: If IDE is invalid
26
26
  """
27
+ if ide.lower() == 'all':
28
+ return None
27
29
  try:
28
30
  return AIIDEType(ide.lower())
29
31
  except ValueError:
30
32
  valid_ides = ', '.join([ide_type.value for ide_type in AIIDEType])
31
33
  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',
33
35
  style='bold red',
34
36
  )
35
37
  raise typer.Exit(1) from None
@@ -2,12 +2,7 @@
2
2
 
3
3
  Currently supports:
4
4
  - Cursor
5
-
6
- To add a new IDE (e.g., Claude Code):
7
- 1. Add new value to AIIDEType enum
8
- 2. Create _get_<ide>_hooks_dir() function with platform-specific paths
9
- 3. Add entry to IDE_CONFIGS dict with IDE-specific hook event names
10
- 4. Unhide --ide option in commands (install, uninstall, status)
5
+ - Claude Code
11
6
  """
12
7
 
13
8
  import platform
@@ -20,6 +15,14 @@ class AIIDEType(str, Enum):
20
15
  """Supported AI IDE types."""
21
16
 
22
17
  CURSOR = 'cursor'
18
+ CLAUDE_CODE = 'claude-code'
19
+
20
+
21
+ class PolicyMode(str, Enum):
22
+ """Policy enforcement mode for global mode and per-feature actions."""
23
+
24
+ BLOCK = 'block'
25
+ WARN = 'warn'
23
26
 
24
27
 
25
28
  class IDEConfig(NamedTuple):
@@ -42,6 +45,14 @@ def _get_cursor_hooks_dir() -> Path:
42
45
  return Path.home() / '.config' / 'Cursor'
43
46
 
44
47
 
48
+ def _get_claude_code_hooks_dir() -> Path:
49
+ """Get Claude Code hooks directory.
50
+
51
+ Claude Code uses ~/.claude on all platforms.
52
+ """
53
+ return Path.home() / '.claude'
54
+
55
+
45
56
  # IDE-specific configurations
46
57
  IDE_CONFIGS: dict[AIIDEType, IDEConfig] = {
47
58
  AIIDEType.CURSOR: IDEConfig(
@@ -51,6 +62,13 @@ IDE_CONFIGS: dict[AIIDEType, IDEConfig] = {
51
62
  hooks_file_name='hooks.json',
52
63
  hook_events=['beforeSubmitPrompt', 'beforeReadFile', 'beforeMCPExecution'],
53
64
  ),
65
+ AIIDEType.CLAUDE_CODE: IDEConfig(
66
+ name='Claude Code',
67
+ hooks_dir=_get_claude_code_hooks_dir(),
68
+ repo_hooks_subdir='.claude',
69
+ hooks_file_name='settings.json',
70
+ hook_events=['UserPromptSubmit', 'PreToolUse:Read', 'PreToolUse:mcp'],
71
+ ),
54
72
  }
55
73
 
56
74
  # Default IDE
@@ -60,6 +78,47 @@ DEFAULT_IDE = AIIDEType.CURSOR
60
78
  CYCODE_SCAN_PROMPT_COMMAND = 'cycode ai-guardrails scan'
61
79
 
62
80
 
81
+ def _get_cursor_hooks_config() -> dict:
82
+ """Get Cursor-specific hooks configuration."""
83
+ config = IDE_CONFIGS[AIIDEType.CURSOR]
84
+ hooks = {event: [{'command': CYCODE_SCAN_PROMPT_COMMAND}] for event in config.hook_events}
85
+
86
+ return {
87
+ 'version': 1,
88
+ 'hooks': hooks,
89
+ }
90
+
91
+
92
+ def _get_claude_code_hooks_config() -> dict:
93
+ """Get Claude Code-specific hooks configuration.
94
+
95
+ Claude Code uses a different hook format with nested structure:
96
+ - hooks are arrays of objects with 'hooks' containing command arrays
97
+ - PreToolUse uses 'matcher' field to specify which tools to intercept
98
+ """
99
+ command = f'{CYCODE_SCAN_PROMPT_COMMAND} --ide claude-code'
100
+
101
+ return {
102
+ 'hooks': {
103
+ 'UserPromptSubmit': [
104
+ {
105
+ 'hooks': [{'type': 'command', 'command': command}],
106
+ }
107
+ ],
108
+ 'PreToolUse': [
109
+ {
110
+ 'matcher': 'Read',
111
+ 'hooks': [{'type': 'command', 'command': command}],
112
+ },
113
+ {
114
+ 'matcher': 'mcp__.*',
115
+ 'hooks': [{'type': 'command', 'command': command}],
116
+ },
117
+ ],
118
+ },
119
+ }
120
+
121
+
63
122
  def get_hooks_config(ide: AIIDEType) -> dict:
64
123
  """Get the hooks configuration for a specific IDE.
65
124
 
@@ -69,10 +128,6 @@ def get_hooks_config(ide: AIIDEType) -> dict:
69
128
  Returns:
70
129
  Dict with hooks configuration for the specified IDE
71
130
  """
72
- config = IDE_CONFIGS[ide]
73
- hooks = {event: [{'command': CYCODE_SCAN_PROMPT_COMMAND}] for event in config.hook_events}
74
-
75
- return {
76
- 'version': 1,
77
- 'hooks': hooks,
78
- }
131
+ if ide == AIIDEType.CLAUDE_CODE:
132
+ return _get_claude_code_hooks_config()
133
+ return _get_cursor_hooks_config()
@@ -59,9 +59,27 @@ def save_hooks_file(hooks_path: Path, hooks_config: dict) -> bool:
59
59
 
60
60
 
61
61
  def is_cycode_hook_entry(entry: dict) -> bool:
62
- """Check if a hook entry is from cycode-cli."""
62
+ """Check if a hook entry is from cycode-cli.
63
+
64
+ Handles both Cursor format (flat) and Claude Code format (nested).
65
+
66
+ Cursor format: {"command": "cycode ai-guardrails scan"}
67
+ Claude Code format: {"hooks": [{"type": "command", "command": "cycode ai-guardrails scan --ide claude-code"}]}
68
+ """
69
+ # Check Cursor format (flat command)
63
70
  command = entry.get('command', '')
64
- return CYCODE_SCAN_PROMPT_COMMAND in command
71
+ if CYCODE_SCAN_PROMPT_COMMAND in command:
72
+ return True
73
+
74
+ # Check Claude Code format (nested hooks array)
75
+ hooks = entry.get('hooks', [])
76
+ for hook in hooks:
77
+ if isinstance(hook, dict):
78
+ hook_command = hook.get('command', '')
79
+ if CYCODE_SCAN_PROMPT_COMMAND in hook_command:
80
+ return True
81
+
82
+ return False
65
83
 
66
84
 
67
85
  def install_hooks(
@@ -185,7 +203,15 @@ def get_hooks_status(scope: str = 'user', repo_path: Optional[Path] = None, ide:
185
203
  ide_config = IDE_CONFIGS[ide]
186
204
  has_cycode_hooks = False
187
205
  for event in ide_config.hook_events:
188
- 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
+
189
215
  cycode_entries = [e for e in entries if is_cycode_hook_entry(e)]
190
216
  if cycode_entries:
191
217
  has_cycode_hooks = True
@@ -11,7 +11,7 @@ from cycode.cli.apps.ai_guardrails.command_utils import (
11
11
  validate_and_parse_ide,
12
12
  validate_scope,
13
13
  )
14
- from cycode.cli.apps.ai_guardrails.consts import IDE_CONFIGS
14
+ from cycode.cli.apps.ai_guardrails.consts import IDE_CONFIGS, AIIDEType
15
15
  from cycode.cli.apps.ai_guardrails.hooks_manager import install_hooks
16
16
  from cycode.cli.utils.sentry import add_breadcrumb
17
17
 
@@ -30,9 +30,9 @@ def install_command(
30
30
  str,
31
31
  typer.Option(
32
32
  '--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.',
34
34
  ),
35
- ] = 'cursor',
35
+ ] = AIIDEType.CURSOR,
36
36
  repo_path: Annotated[
37
37
  Optional[Path],
38
38
  typer.Option(
@@ -54,6 +54,7 @@ def install_command(
54
54
  cycode ai-guardrails install # Install for all projects (user scope)
55
55
  cycode ai-guardrails install --scope repo # Install for current repo only
56
56
  cycode ai-guardrails install --ide cursor # Install for Cursor IDE
57
+ cycode ai-guardrails install --ide all # Install for all supported IDEs
57
58
  cycode ai-guardrails install --scope repo --repo-path /path/to/repo
58
59
  """
59
60
  add_breadcrumb('ai-guardrails-install')
@@ -62,17 +63,35 @@ def install_command(
62
63
  validate_scope(scope)
63
64
  repo_path = resolve_repo_path(scope, repo_path)
64
65
  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)
67
66
 
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:
70
87
  console.print()
71
88
  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')
73
92
  console.print('2. (Optional) Customize policy in ~/.cycode/ai-guardrails.yaml')
74
93
  console.print()
75
94
  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:
78
97
  raise typer.Exit(1)
@@ -13,6 +13,7 @@ from typing import Callable, Optional
13
13
 
14
14
  import typer
15
15
 
16
+ from cycode.cli.apps.ai_guardrails.consts import PolicyMode
16
17
  from cycode.cli.apps.ai_guardrails.scan.payload import AIHookPayload
17
18
  from cycode.cli.apps.ai_guardrails.scan.policy import get_policy_value
18
19
  from cycode.cli.apps.ai_guardrails.scan.response_builders import get_response_builder
@@ -46,7 +47,7 @@ def handle_before_submit_prompt(ctx: typer.Context, payload: AIHookPayload, poli
46
47
  ai_client.create_event(payload, AiHookEventType.PROMPT, AIHookOutcome.ALLOWED)
47
48
  return response_builder.allow_prompt()
48
49
 
49
- mode = get_policy_value(policy, 'mode', default='block')
50
+ mode = get_policy_value(policy, 'mode', default=PolicyMode.BLOCK)
50
51
  prompt = payload.prompt or ''
51
52
  max_bytes = get_policy_value(policy, 'secrets', 'max_bytes', default=200000)
52
53
  timeout_ms = get_policy_value(policy, 'secrets', 'timeout_ms', default=30000)
@@ -55,29 +56,26 @@ def handle_before_submit_prompt(ctx: typer.Context, payload: AIHookPayload, poli
55
56
  scan_id = None
56
57
  block_reason = None
57
58
  outcome = AIHookOutcome.ALLOWED
59
+ error_message = None
58
60
 
59
61
  try:
60
62
  violation_summary, scan_id = _scan_text_for_secrets(ctx, clipped, timeout_ms)
61
63
 
62
- if (
63
- violation_summary
64
- and get_policy_value(prompt_config, 'action', default='block') == 'block'
65
- and mode == 'block'
66
- ):
67
- outcome = AIHookOutcome.BLOCKED
64
+ if violation_summary:
68
65
  block_reason = BlockReason.SECRETS_IN_PROMPT
69
- user_message = f'{violation_summary}. Remove secrets before sending.'
70
- response = response_builder.deny_prompt(user_message)
71
- else:
72
- if violation_summary:
73
- outcome = AIHookOutcome.WARNED
74
- response = response_builder.allow_prompt()
75
- return response
66
+ action = get_policy_value(prompt_config, 'action', default=PolicyMode.BLOCK)
67
+ if action == PolicyMode.BLOCK and mode == PolicyMode.BLOCK:
68
+ outcome = AIHookOutcome.BLOCKED
69
+ user_message = f'{violation_summary}. Remove secrets before sending.'
70
+ return response_builder.deny_prompt(user_message)
71
+ outcome = AIHookOutcome.WARNED
72
+ return response_builder.allow_prompt()
76
73
  except Exception as e:
77
74
  outcome = (
78
75
  AIHookOutcome.ALLOWED if get_policy_value(policy, 'fail_open', default=True) else AIHookOutcome.BLOCKED
79
76
  )
80
- block_reason = BlockReason.SCAN_FAILURE if outcome == AIHookOutcome.BLOCKED else None
77
+ block_reason = BlockReason.SCAN_FAILURE
78
+ error_message = str(e)
81
79
  raise e
82
80
  finally:
83
81
  ai_client.create_event(
@@ -86,6 +84,7 @@ def handle_before_submit_prompt(ctx: typer.Context, payload: AIHookPayload, poli
86
84
  outcome,
87
85
  scan_id=scan_id,
88
86
  block_reason=block_reason,
87
+ error_message=error_message,
89
88
  )
90
89
 
91
90
 
@@ -106,38 +105,53 @@ def handle_before_read_file(ctx: typer.Context, payload: AIHookPayload, policy:
106
105
  ai_client.create_event(payload, AiHookEventType.FILE_READ, AIHookOutcome.ALLOWED)
107
106
  return response_builder.allow_permission()
108
107
 
109
- mode = get_policy_value(policy, 'mode', default='block')
108
+ mode = get_policy_value(policy, 'mode', default=PolicyMode.BLOCK)
110
109
  file_path = payload.file_path or ''
111
- action = get_policy_value(file_read_config, 'action', default='block')
110
+ action = get_policy_value(file_read_config, 'action', default=PolicyMode.BLOCK)
112
111
 
113
112
  scan_id = None
114
113
  block_reason = None
115
114
  outcome = AIHookOutcome.ALLOWED
115
+ error_message = None
116
116
 
117
117
  try:
118
118
  # Check path-based denylist first
119
- if is_denied_path(file_path, policy) and action == 'block':
120
- outcome = AIHookOutcome.BLOCKED
119
+ if is_denied_path(file_path, policy):
121
120
  block_reason = BlockReason.SENSITIVE_PATH
122
- user_message = f'Cycode blocked sending {file_path} to the AI (sensitive path policy).'
123
- return response_builder.deny_permission(
121
+ if mode == PolicyMode.BLOCK and action == PolicyMode.BLOCK:
122
+ outcome = AIHookOutcome.BLOCKED
123
+ user_message = f'Cycode blocked sending {file_path} to the AI (sensitive path policy).'
124
+ return response_builder.deny_permission(
125
+ user_message,
126
+ 'This file path is classified as sensitive; do not read/send it to the model.',
127
+ )
128
+ # Warn mode - ask user for permission
129
+ outcome = AIHookOutcome.WARNED
130
+ user_message = f'Cycode flagged {file_path} as sensitive. Allow reading?'
131
+ return response_builder.ask_permission(
124
132
  user_message,
125
- 'This file path is classified as sensitive; do not read/send it to the model.',
133
+ 'This file path is classified as sensitive; proceed with caution.',
126
134
  )
127
135
 
128
136
  # Scan file content if enabled
129
137
  if get_policy_value(file_read_config, 'scan_content', default=True):
130
138
  violation_summary, scan_id = _scan_path_for_secrets(ctx, file_path, policy)
131
- if violation_summary and action == 'block' and mode == 'block':
132
- outcome = AIHookOutcome.BLOCKED
139
+ if violation_summary:
133
140
  block_reason = BlockReason.SECRETS_IN_FILE
134
- user_message = f'Cycode blocked reading {file_path}. {violation_summary}'
135
- return response_builder.deny_permission(
141
+ if mode == PolicyMode.BLOCK and action == PolicyMode.BLOCK:
142
+ outcome = AIHookOutcome.BLOCKED
143
+ user_message = f'Cycode blocked reading {file_path}. {violation_summary}'
144
+ return response_builder.deny_permission(
145
+ user_message,
146
+ 'Secrets detected; do not send this file to the model.',
147
+ )
148
+ # Warn mode - ask user for permission
149
+ outcome = AIHookOutcome.WARNED
150
+ user_message = f'Cycode detected secrets in {file_path}. {violation_summary}'
151
+ return response_builder.ask_permission(
136
152
  user_message,
137
- 'Secrets detected; do not send this file to the model.',
153
+ 'Possible secrets detected; proceed with caution.',
138
154
  )
139
- if violation_summary:
140
- outcome = AIHookOutcome.WARNED
141
155
  return response_builder.allow_permission()
142
156
 
143
157
  return response_builder.allow_permission()
@@ -145,7 +159,8 @@ def handle_before_read_file(ctx: typer.Context, payload: AIHookPayload, policy:
145
159
  outcome = (
146
160
  AIHookOutcome.ALLOWED if get_policy_value(policy, 'fail_open', default=True) else AIHookOutcome.BLOCKED
147
161
  )
148
- block_reason = BlockReason.SCAN_FAILURE if outcome == AIHookOutcome.BLOCKED else None
162
+ block_reason = BlockReason.SCAN_FAILURE
163
+ error_message = str(e)
149
164
  raise e
150
165
  finally:
151
166
  ai_client.create_event(
@@ -154,6 +169,7 @@ def handle_before_read_file(ctx: typer.Context, payload: AIHookPayload, policy:
154
169
  outcome,
155
170
  scan_id=scan_id,
156
171
  block_reason=block_reason,
172
+ error_message=error_message,
157
173
  )
158
174
 
159
175
 
@@ -175,26 +191,27 @@ def handle_before_mcp_execution(ctx: typer.Context, payload: AIHookPayload, poli
175
191
  ai_client.create_event(payload, AiHookEventType.MCP_EXECUTION, AIHookOutcome.ALLOWED)
176
192
  return response_builder.allow_permission()
177
193
 
178
- mode = get_policy_value(policy, 'mode', default='block')
194
+ mode = get_policy_value(policy, 'mode', default=PolicyMode.BLOCK)
179
195
  tool = payload.mcp_tool_name or 'unknown'
180
196
  args = payload.mcp_arguments or {}
181
197
  args_text = args if isinstance(args, str) else json.dumps(args)
182
198
  max_bytes = get_policy_value(policy, 'secrets', 'max_bytes', default=200000)
183
199
  timeout_ms = get_policy_value(policy, 'secrets', 'timeout_ms', default=30000)
184
200
  clipped = truncate_utf8(args_text, max_bytes)
185
- action = get_policy_value(mcp_config, 'action', default='block')
201
+ action = get_policy_value(mcp_config, 'action', default=PolicyMode.BLOCK)
186
202
 
187
203
  scan_id = None
188
204
  block_reason = None
189
205
  outcome = AIHookOutcome.ALLOWED
206
+ error_message = None
190
207
 
191
208
  try:
192
209
  if get_policy_value(mcp_config, 'scan_arguments', default=True):
193
210
  violation_summary, scan_id = _scan_text_for_secrets(ctx, clipped, timeout_ms)
194
211
  if violation_summary:
195
- if mode == 'block' and action == 'block':
212
+ block_reason = BlockReason.SECRETS_IN_MCP_ARGS
213
+ if mode == PolicyMode.BLOCK and action == PolicyMode.BLOCK:
196
214
  outcome = AIHookOutcome.BLOCKED
197
- block_reason = BlockReason.SECRETS_IN_MCP_ARGS
198
215
  user_message = f'Cycode blocked MCP tool call "{tool}". {violation_summary}'
199
216
  return response_builder.deny_permission(
200
217
  user_message,
@@ -211,7 +228,8 @@ def handle_before_mcp_execution(ctx: typer.Context, payload: AIHookPayload, poli
211
228
  outcome = (
212
229
  AIHookOutcome.ALLOWED if get_policy_value(policy, 'fail_open', default=True) else AIHookOutcome.BLOCKED
213
230
  )
214
- block_reason = BlockReason.SCAN_FAILURE if outcome == AIHookOutcome.BLOCKED else None
231
+ block_reason = BlockReason.SCAN_FAILURE
232
+ error_message = str(e)
215
233
  raise e
216
234
  finally:
217
235
  ai_client.create_event(
@@ -220,6 +238,7 @@ def handle_before_mcp_execution(ctx: typer.Context, payload: AIHookPayload, poli
220
238
  outcome,
221
239
  scan_id=scan_id,
222
240
  block_reason=block_reason,
241
+ error_message=error_message,
223
242
  )
224
243
 
225
244
 
@@ -1,9 +1,120 @@
1
1
  """Unified payload object for AI hook events from different tools."""
2
2
 
3
+ import json
4
+ from collections.abc import Iterator
3
5
  from dataclasses import dataclass
6
+ from pathlib import Path
4
7
  from typing import Optional
5
8
 
6
- from cycode.cli.apps.ai_guardrails.scan.types import CURSOR_EVENT_MAPPING
9
+ from cycode.cli.apps.ai_guardrails.consts import AIIDEType
10
+ from cycode.cli.apps.ai_guardrails.scan.types import (
11
+ CLAUDE_CODE_EVENT_MAPPING,
12
+ CLAUDE_CODE_EVENT_NAMES,
13
+ CURSOR_EVENT_MAPPING,
14
+ CURSOR_EVENT_NAMES,
15
+ AiHookEventType,
16
+ )
17
+
18
+
19
+ def _reverse_readline(path: Path, buf_size: int = 8192) -> Iterator[str]:
20
+ """Read a file line by line from the end without loading entire file into memory.
21
+
22
+ Yields lines in reverse order (last line first).
23
+ """
24
+ with path.open('rb') as f:
25
+ f.seek(0, 2) # Seek to end
26
+ file_size = f.tell()
27
+ if file_size == 0:
28
+ return
29
+
30
+ remaining = file_size
31
+ buffer = b''
32
+
33
+ while remaining > 0:
34
+ # Read a chunk from the end
35
+ read_size = min(buf_size, remaining)
36
+ remaining -= read_size
37
+ f.seek(remaining)
38
+ chunk = f.read(read_size)
39
+ buffer = chunk + buffer
40
+
41
+ # Yield complete lines from buffer
42
+ while b'\n' in buffer:
43
+ # Find the last newline
44
+ newline_pos = buffer.rfind(b'\n')
45
+ if newline_pos == len(buffer) - 1:
46
+ # Trailing newline, look for previous one
47
+ newline_pos = buffer.rfind(b'\n', 0, newline_pos)
48
+ if newline_pos == -1:
49
+ break
50
+ # Yield the line after this newline
51
+ line = buffer[newline_pos + 1 :]
52
+ buffer = buffer[: newline_pos + 1]
53
+ if line.strip():
54
+ yield line.decode('utf-8', errors='replace')
55
+
56
+ # Yield any remaining content as the first line of the file
57
+ if buffer.strip():
58
+ yield buffer.decode('utf-8', errors='replace')
59
+
60
+
61
+ def _extract_model(entry: dict) -> Optional[str]:
62
+ """Extract model from a transcript entry (top level or nested in message)."""
63
+ return entry.get('model') or (entry.get('message') or {}).get('model')
64
+
65
+
66
+ def _extract_generation_id(entry: dict) -> Optional[str]:
67
+ """Extract generation ID from a user-type transcript entry."""
68
+ if entry.get('type') == 'user':
69
+ return entry.get('uuid')
70
+ return None
71
+
72
+
73
+ def _extract_from_claude_transcript(
74
+ transcript_path: str,
75
+ ) -> tuple[Optional[str], Optional[str], Optional[str]]:
76
+ """Extract IDE version, model, and latest generation ID from Claude Code transcript file.
77
+
78
+ The transcript is a JSONL file where each line is a JSON object.
79
+ We look for 'version' (IDE version), 'model', and 'uuid' (generation ID) fields.
80
+ The generation_id is the UUID of the latest 'user' type message.
81
+
82
+ Scans from end to start since latest entries are at the end.
83
+ Uses reverse reading to avoid loading entire file into memory.
84
+
85
+ Returns:
86
+ Tuple of (ide_version, model, generation_id), any may be None if not found.
87
+ """
88
+ if not transcript_path:
89
+ return None, None, None
90
+
91
+ path = Path(transcript_path)
92
+ if not path.exists():
93
+ return None, None, None
94
+
95
+ ide_version = None
96
+ model = None
97
+ generation_id = None
98
+
99
+ try:
100
+ for line in _reverse_readline(path):
101
+ line = line.strip()
102
+ if not line:
103
+ continue
104
+ try:
105
+ entry = json.loads(line)
106
+ ide_version = ide_version or entry.get('version')
107
+ model = model or _extract_model(entry)
108
+ generation_id = generation_id or _extract_generation_id(entry)
109
+
110
+ if ide_version and model and generation_id:
111
+ break
112
+ except json.JSONDecodeError:
113
+ continue
114
+ except OSError:
115
+ pass
116
+
117
+ return ide_version, model, generation_id
7
118
 
8
119
 
9
120
  @dataclass
@@ -18,7 +129,7 @@ class AIHookPayload:
18
129
  # User and IDE information
19
130
  ide_user_email: Optional[str] = None
20
131
  model: Optional[str] = None
21
- ide_provider: str = None # e.g., 'cursor', 'claude-code'
132
+ ide_provider: str = None # AIIDEType value (e.g., 'cursor', 'claude-code')
22
133
  ide_version: Optional[str] = None
23
134
 
24
135
  # Event-specific data
@@ -44,7 +155,7 @@ class AIHookPayload:
44
155
  generation_id=payload.get('generation_id'),
45
156
  ide_user_email=payload.get('user_email'),
46
157
  model=payload.get('model'),
47
- ide_provider='cursor',
158
+ ide_provider=AIIDEType.CURSOR,
48
159
  ide_version=payload.get('cursor_version'),
49
160
  prompt=payload.get('prompt', ''),
50
161
  file_path=payload.get('file_path') or payload.get('path'),
@@ -54,12 +165,95 @@ class AIHookPayload:
54
165
  )
55
166
 
56
167
  @classmethod
57
- def from_payload(cls, payload: dict, tool: str = 'cursor') -> 'AIHookPayload':
168
+ def from_claude_code_payload(cls, payload: dict) -> 'AIHookPayload':
169
+ """Create AIHookPayload from Claude Code IDE payload.
170
+
171
+ Claude Code has a different structure:
172
+ - hook_event_name: 'UserPromptSubmit' or 'PreToolUse'
173
+ - For PreToolUse: tool_name determines if it's file read ('Read') or MCP ('mcp__*')
174
+ - tool_input contains tool arguments (e.g., file_path for Read tool)
175
+ - transcript_path points to JSONL file with version and model info
176
+ """
177
+ hook_event_name = payload.get('hook_event_name', '')
178
+ tool_name = payload.get('tool_name', '')
179
+ tool_input = payload.get('tool_input')
180
+
181
+ if hook_event_name == 'UserPromptSubmit':
182
+ canonical_event = AiHookEventType.PROMPT
183
+ elif hook_event_name == 'PreToolUse':
184
+ canonical_event = AiHookEventType.FILE_READ if tool_name == 'Read' else AiHookEventType.MCP_EXECUTION
185
+ else:
186
+ # Unknown event, use the raw event name
187
+ canonical_event = CLAUDE_CODE_EVENT_MAPPING.get(hook_event_name, hook_event_name)
188
+
189
+ # Extract file_path from tool_input for Read tool
190
+ file_path = None
191
+ if tool_name == 'Read' and isinstance(tool_input, dict):
192
+ file_path = tool_input.get('file_path')
193
+
194
+ # For MCP tools, the entire tool_input is the arguments
195
+ mcp_arguments = tool_input if tool_name.startswith('mcp__') else None
196
+
197
+ # Extract MCP server and tool name from tool_name (format: mcp__<server>__<tool>)
198
+ mcp_server_name = None
199
+ mcp_tool_name = None
200
+ if tool_name.startswith('mcp__'):
201
+ parts = tool_name.split('__')
202
+ if len(parts) >= 2:
203
+ mcp_server_name = parts[1]
204
+ if len(parts) >= 3:
205
+ mcp_tool_name = parts[2]
206
+
207
+ # Extract IDE version, model, and generation ID from transcript file
208
+ ide_version, model, generation_id = _extract_from_claude_transcript(payload.get('transcript_path'))
209
+
210
+ return cls(
211
+ event_name=canonical_event,
212
+ conversation_id=payload.get('session_id'),
213
+ generation_id=generation_id,
214
+ ide_user_email=None, # Claude Code doesn't provide this in hook payload
215
+ model=model,
216
+ ide_provider=AIIDEType.CLAUDE_CODE,
217
+ ide_version=ide_version,
218
+ prompt=payload.get('prompt', ''),
219
+ file_path=file_path,
220
+ mcp_server_name=mcp_server_name,
221
+ mcp_tool_name=mcp_tool_name,
222
+ mcp_arguments=mcp_arguments,
223
+ )
224
+
225
+ @staticmethod
226
+ def is_payload_for_ide(payload: dict, ide: str) -> bool:
227
+ """Check if the payload's event name matches the expected IDE.
228
+
229
+ This prevents double-processing when Cursor reads Claude Code hooks
230
+ or vice versa. If the payload's hook_event_name doesn't match the
231
+ expected IDE's event names, we should skip processing.
232
+
233
+ Args:
234
+ payload: The raw payload from the IDE
235
+ ide: The IDE name or AIIDEType enum value
236
+
237
+ Returns:
238
+ True if the payload matches the IDE, False otherwise.
239
+ """
240
+ hook_event_name = payload.get('hook_event_name', '')
241
+
242
+ if ide == AIIDEType.CLAUDE_CODE:
243
+ return hook_event_name in CLAUDE_CODE_EVENT_NAMES
244
+ if ide == AIIDEType.CURSOR:
245
+ return hook_event_name in CURSOR_EVENT_NAMES
246
+
247
+ # Unknown IDE, allow processing
248
+ return True
249
+
250
+ @classmethod
251
+ def from_payload(cls, payload: dict, tool: str = AIIDEType.CURSOR) -> 'AIHookPayload':
58
252
  """Create AIHookPayload from any tool's payload.
59
253
 
60
254
  Args:
61
255
  payload: The raw payload from the IDE
62
- tool: The IDE/tool name (e.g., 'cursor')
256
+ tool: The IDE/tool name or AIIDEType enum value
63
257
 
64
258
  Returns:
65
259
  AIHookPayload instance
@@ -67,6 +261,8 @@ class AIHookPayload:
67
261
  Raises:
68
262
  ValueError: If the tool is not supported
69
263
  """
70
- if tool == 'cursor':
264
+ if tool == AIIDEType.CURSOR:
71
265
  return cls.from_cursor_payload(payload)
72
- raise ValueError(f'Unsupported IDE/tool: {tool}.')
266
+ if tool == AIIDEType.CLAUDE_CODE:
267
+ return cls.from_claude_code_payload(payload)
268
+ raise ValueError(f'Unsupported IDE/tool: {tool}')
@@ -7,6 +7,8 @@ an abstract interface and concrete implementations for each supported IDE.
7
7
 
8
8
  from abc import ABC, abstractmethod
9
9
 
10
+ from cycode.cli.apps.ai_guardrails.consts import AIIDEType
11
+
10
12
 
11
13
  class IDEResponseBuilder(ABC):
12
14
  """Abstract base class for IDE-specific response builders."""
@@ -62,17 +64,64 @@ class CursorResponseBuilder(IDEResponseBuilder):
62
64
  return {'continue': False, 'user_message': user_message}
63
65
 
64
66
 
65
- # Registry of response builders by IDE name
67
+ class ClaudeCodeResponseBuilder(IDEResponseBuilder):
68
+ """Response builder for Claude Code IDE hooks.
69
+
70
+ Claude Code hook response formats:
71
+ - UserPromptSubmit: {} for allow, {"decision": "block", "reason": str} for deny
72
+ - PreToolUse: hookSpecificOutput with permissionDecision (allow/deny/ask)
73
+ """
74
+
75
+ def allow_permission(self) -> dict:
76
+ """Allow file read or MCP execution."""
77
+ return {
78
+ 'hookSpecificOutput': {
79
+ 'hookEventName': 'PreToolUse',
80
+ 'permissionDecision': 'allow',
81
+ }
82
+ }
83
+
84
+ def deny_permission(self, user_message: str, agent_message: str) -> dict:
85
+ """Deny file read or MCP execution."""
86
+ return {
87
+ 'hookSpecificOutput': {
88
+ 'hookEventName': 'PreToolUse',
89
+ 'permissionDecision': 'deny',
90
+ 'permissionDecisionReason': user_message,
91
+ }
92
+ }
93
+
94
+ def ask_permission(self, user_message: str, agent_message: str) -> dict:
95
+ """Ask user for permission (warn mode)."""
96
+ return {
97
+ 'hookSpecificOutput': {
98
+ 'hookEventName': 'PreToolUse',
99
+ 'permissionDecision': 'ask',
100
+ 'permissionDecisionReason': user_message,
101
+ }
102
+ }
103
+
104
+ def allow_prompt(self) -> dict:
105
+ """Allow prompt submission (empty response means allow)."""
106
+ return {}
107
+
108
+ def deny_prompt(self, user_message: str) -> dict:
109
+ """Deny prompt submission."""
110
+ return {'decision': 'block', 'reason': user_message}
111
+
112
+
113
+ # Registry of response builders by IDE type
66
114
  _RESPONSE_BUILDERS: dict[str, IDEResponseBuilder] = {
67
- 'cursor': CursorResponseBuilder(),
115
+ AIIDEType.CURSOR: CursorResponseBuilder(),
116
+ AIIDEType.CLAUDE_CODE: ClaudeCodeResponseBuilder(),
68
117
  }
69
118
 
70
119
 
71
- def get_response_builder(ide: str = 'cursor') -> IDEResponseBuilder:
120
+ def get_response_builder(ide: str = AIIDEType.CURSOR) -> IDEResponseBuilder:
72
121
  """Get the response builder for a specific IDE.
73
122
 
74
123
  Args:
75
- ide: The IDE name (e.g., 'cursor', 'claude-code')
124
+ ide: The IDE name (e.g., 'cursor', 'claude-code') or AIIDEType enum
76
125
 
77
126
  Returns:
78
127
  IDEResponseBuilder instance for the specified IDE
@@ -80,7 +129,10 @@ def get_response_builder(ide: str = 'cursor') -> IDEResponseBuilder:
80
129
  Raises:
81
130
  ValueError: If the IDE is not supported
82
131
  """
83
- builder = _RESPONSE_BUILDERS.get(ide.lower())
132
+ # Normalize to AIIDEType if string passed
133
+ if isinstance(ide, str):
134
+ ide = ide.lower()
135
+ builder = _RESPONSE_BUILDERS.get(ide)
84
136
  if not builder:
85
137
  raise ValueError(f'Unsupported IDE: {ide}. Supported IDEs: {list(_RESPONSE_BUILDERS.keys())}')
86
138
  return builder
@@ -16,6 +16,7 @@ from typing import Annotated
16
16
  import click
17
17
  import typer
18
18
 
19
+ from cycode.cli.apps.ai_guardrails.consts import AIIDEType
19
20
  from cycode.cli.apps.ai_guardrails.scan.handlers import get_handler_for_event
20
21
  from cycode.cli.apps.ai_guardrails.scan.payload import AIHookPayload
21
22
  from cycode.cli.apps.ai_guardrails.scan.policy import load_policy
@@ -69,7 +70,7 @@ def scan_command(
69
70
  help='IDE that sent the payload (e.g., "cursor"). Defaults to cursor.',
70
71
  hidden=True,
71
72
  ),
72
- ] = 'cursor',
73
+ ] = AIIDEType.CURSOR,
73
74
  ) -> None:
74
75
  """Scan content from AI IDE hooks for secrets.
75
76
 
@@ -96,6 +97,16 @@ def scan_command(
96
97
  output_json(response_builder.allow_prompt())
97
98
  return
98
99
 
100
+ # Check if the payload matches the expected IDE - prevents double-processing
101
+ # when Cursor reads Claude Code hooks from ~/.claude/settings.json
102
+ if not AIHookPayload.is_payload_for_ide(payload, tool):
103
+ logger.debug(
104
+ 'Payload event does not match expected IDE, skipping',
105
+ extra={'hook_event_name': payload.get('hook_event_name'), 'expected_ide': tool},
106
+ )
107
+ output_json(response_builder.allow_prompt())
108
+ return
109
+
99
110
  unified_payload = AIHookPayload.from_payload(payload, tool=tool)
100
111
  event_name = unified_payload.event_name
101
112
  logger.debug('Processing AI guardrails hook', extra={'event_name': event_name, 'tool': tool})
@@ -31,6 +31,17 @@ CURSOR_EVENT_MAPPING = {
31
31
  'beforeMCPExecution': AiHookEventType.MCP_EXECUTION,
32
32
  }
33
33
 
34
+ # Claude Code event mapping - note that PreToolUse requires tool_name inspection
35
+ # to determine the actual event type (file read vs MCP execution)
36
+ CLAUDE_CODE_EVENT_MAPPING = {
37
+ 'UserPromptSubmit': AiHookEventType.PROMPT,
38
+ 'PreToolUse': None, # Requires tool_name inspection to determine actual type
39
+ }
40
+
41
+ # Set of known event names per IDE (for IDE detection)
42
+ CURSOR_EVENT_NAMES = set(CURSOR_EVENT_MAPPING.keys())
43
+ CLAUDE_CODE_EVENT_NAMES = set(CLAUDE_CODE_EVENT_MAPPING.keys())
44
+
34
45
 
35
46
  class AIHookOutcome(StrEnum):
36
47
  """Outcome of an AI hook event evaluation."""
@@ -8,6 +8,7 @@ import typer
8
8
  from rich.table import Table
9
9
 
10
10
  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
11
12
  from cycode.cli.apps.ai_guardrails.hooks_manager import get_hooks_status
12
13
  from cycode.cli.utils.sentry import add_breadcrumb
13
14
 
@@ -26,9 +27,9 @@ def status_command(
26
27
  str,
27
28
  typer.Option(
28
29
  '--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.',
30
31
  ),
31
- ] = 'cursor',
32
+ ] = AIIDEType.CURSOR,
32
33
  repo_path: Annotated[
33
34
  Optional[Path],
34
35
  typer.Option(
@@ -50,6 +51,7 @@ def status_command(
50
51
  cycode ai-guardrails status --scope user # Show only user-level status
51
52
  cycode ai-guardrails status --scope repo # Show only repo-level status
52
53
  cycode ai-guardrails status --ide cursor # Check status for Cursor IDE
54
+ cycode ai-guardrails status --ide all # Check status for all supported IDEs
53
55
  """
54
56
  add_breadcrumb('ai-guardrails-status')
55
57
 
@@ -59,34 +61,41 @@ def status_command(
59
61
  repo_path = Path(os.getcwd())
60
62
  ide_type = validate_and_parse_ide(ide)
61
63
 
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]
63
65
 
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]
66
67
 
68
+ for current_ide in ides_to_check:
69
+ ide_name = IDE_CONFIGS[current_ide].name
67
70
  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"]}')
70
79
 
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
74
83
 
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[/]')
79
88
 
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')
85
94
 
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']))
89
98
 
90
- console.print(table)
99
+ console.print(table)
91
100
 
92
101
  console.print()
@@ -11,7 +11,7 @@ from cycode.cli.apps.ai_guardrails.command_utils import (
11
11
  validate_and_parse_ide,
12
12
  validate_scope,
13
13
  )
14
- from cycode.cli.apps.ai_guardrails.consts import IDE_CONFIGS
14
+ from cycode.cli.apps.ai_guardrails.consts import IDE_CONFIGS, AIIDEType
15
15
  from cycode.cli.apps.ai_guardrails.hooks_manager import uninstall_hooks
16
16
  from cycode.cli.utils.sentry import add_breadcrumb
17
17
 
@@ -30,9 +30,9 @@ def uninstall_command(
30
30
  str,
31
31
  typer.Option(
32
32
  '--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.',
34
34
  ),
35
- ] = 'cursor',
35
+ ] = AIIDEType.CURSOR,
36
36
  repo_path: Annotated[
37
37
  Optional[Path],
38
38
  typer.Option(
@@ -54,6 +54,7 @@ def uninstall_command(
54
54
  cycode ai-guardrails uninstall # Remove user-level hooks
55
55
  cycode ai-guardrails uninstall --scope repo # Remove repo-level hooks
56
56
  cycode ai-guardrails uninstall --ide cursor # Uninstall from Cursor IDE
57
+ cycode ai-guardrails uninstall --ide all # Uninstall from all supported IDEs
57
58
  """
58
59
  add_breadcrumb('ai-guardrails-uninstall')
59
60
 
@@ -61,13 +62,31 @@ def uninstall_command(
61
62
  validate_scope(scope)
62
63
  repo_path = resolve_repo_path(scope, repo_path)
63
64
  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)
66
65
 
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:
69
86
  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:
73
92
  raise typer.Exit(1)
@@ -61,6 +61,7 @@ class AISecurityManagerClient:
61
61
  outcome: 'AIHookOutcome',
62
62
  scan_id: Optional[str] = None,
63
63
  block_reason: Optional['BlockReason'] = None,
64
+ error_message: Optional[str] = None,
64
65
  ) -> None:
65
66
  """Create an AI hook event from hook payload."""
66
67
  conversation_id = payload.conversation_id
@@ -77,6 +78,7 @@ class AISecurityManagerClient:
77
78
  'cli_scan_id': scan_id,
78
79
  'mcp_server_name': payload.mcp_server_name,
79
80
  'mcp_tool_name': payload.mcp_tool_name,
81
+ 'error_message': error_message,
80
82
  }
81
83
 
82
84
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cycode
3
- Version: 3.8.11.dev1
3
+ Version: 3.9.1
4
4
  Summary: Boost security in your dev lifecycle via SAST, SCA, Secrets & IaC scanning.
5
5
  License-Expression: MIT
6
6
  License-File: LICENCE
@@ -1,24 +1,24 @@
1
- cycode/__init__.py,sha256=ORTRSeXMazgov1gBeQ0xEh8qplYERK_Bjx9-Pht9Hv0,115
1
+ cycode/__init__.py,sha256=COYqmiJKyEDoNPdMU1QWil4k91dLcVgOyUEvwlAxMBo,109
2
2
  cycode/__main__.py,sha256=Z3bD5yrA7yPvAChcADQrqCaZd0ChGI1gdiwALwbWJ6U,104
3
3
  cycode/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  cycode/cli/app.py,sha256=j7jXFJwVbXROvi6LeBcf7MxhY9q-O7-XkLOkfqOqL3I,6460
5
5
  cycode/cli/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  cycode/cli/apps/ai_guardrails/__init__.py,sha256=7VsEUYYyqiuJJAV-_Ti_Wudr4SWQZGnoVLBuq8NTvVQ,902
7
- cycode/cli/apps/ai_guardrails/command_utils.py,sha256=o1LAsmr1jkTxue6sSWzRC8WKUiuzXZd4nRCZJjaWwRw,1810
8
- cycode/cli/apps/ai_guardrails/consts.py,sha256=85FxVg23-KrF4QnCO7nWRQ1LC2UciV1zJS3zSLOGZwU,2046
9
- cycode/cli/apps/ai_guardrails/hooks_manager.py,sha256=HEmyYAsdEPOHnLIZyIyeOkZvzH1e7SluP1OqvRHX9Vo,6478
10
- cycode/cli/apps/ai_guardrails/install_command.py,sha256=cMqEmdIJZgQGCb9y3jy4FPmJ4arSU3_J5pJc3Lst5lM,2671
7
+ cycode/cli/apps/ai_guardrails/command_utils.py,sha256=itWoARiiqC-kCJuppBxBwKDjCSnci2m0EG95GQPy3r4,1924
8
+ cycode/cli/apps/ai_guardrails/consts.py,sha256=6V6jdT2R5wR-_lI7h3pJuWkcTWlVsSnssc0K9KOpOA4,3580
9
+ cycode/cli/apps/ai_guardrails/hooks_manager.py,sha256=YiLY8AnNSmPerMCJpjbJkybnkmhZZ_A93TM3glNZpIM,7448
10
+ cycode/cli/apps/ai_guardrails/install_command.py,sha256=pgE5USLVil1D4Jkeuo4Wxf3t_knL0zn4o1367crz3RU,3439
11
11
  cycode/cli/apps/ai_guardrails/scan/__init__.py,sha256=qJc82XiQGiAuc1sYY8Ij_A-qXpxgLPuayQq8xWlouMA,48
12
12
  cycode/cli/apps/ai_guardrails/scan/consts.py,sha256=drAslw6vW3kxmbCs2qPCUbUPR7PJouT2lsXtu5sD-lQ,1094
13
- cycode/cli/apps/ai_guardrails/scan/handlers.py,sha256=R55cfOAJ9Yd8ENddJ0P2_llDsnMoOvXZ7b6vXYVjwJ0,13902
14
- cycode/cli/apps/ai_guardrails/scan/payload.py,sha256=ZmMj-GYL3BttsL515RF5aEVDqYWlcl3uEfEzmJlGxOc,2832
13
+ cycode/cli/apps/ai_guardrails/scan/handlers.py,sha256=nVFMGtxNFkfiWlhQlIHmx2T1J4jJcOe-WIFiqxxMgu4,14902
14
+ cycode/cli/apps/ai_guardrails/scan/payload.py,sha256=Zxzd1OkM11aCssg7gwwR9OpEjdQOzYklFvEkOqLnb3I,10160
15
15
  cycode/cli/apps/ai_guardrails/scan/policy.py,sha256=39s8hnxgjny1l6XAO59wsRcAlpW-LG00GUnO0PfqvuY,2566
16
- cycode/cli/apps/ai_guardrails/scan/response_builders.py,sha256=rdynnvvKRBjIeXnStafiIp1w5cNuj7uQolRcL453pkI,2934
17
- cycode/cli/apps/ai_guardrails/scan/scan_command.py,sha256=NYVFRemxbzEZJ8pynKt9uB46qML8-mjxkXUz_zFsEuU,5180
18
- cycode/cli/apps/ai_guardrails/scan/types.py,sha256=dYpiQ9F5McYBoDfX9AeiuyAuQZpuZ1KbHuqfBo8A79A,1384
16
+ cycode/cli/apps/ai_guardrails/scan/response_builders.py,sha256=VZvMQ3pxqq4ROpT8rvuEF1y9SiX6tVYvgjhh5UBhz78,4740
17
+ cycode/cli/apps/ai_guardrails/scan/scan_command.py,sha256=lAkb-8txg722NjfJXgXT7gJpJp732dm7-vXWxkT3_EQ,5720
18
+ cycode/cli/apps/ai_guardrails/scan/types.py,sha256=H25MKJhAXmp7Mz1YeCIRmAY1Zg5GSpgBq8G1TEI9PFk,1868
19
19
  cycode/cli/apps/ai_guardrails/scan/utils.py,sha256=KVfX-NrcM-QW4quLtoNqfmz4GF0FlDs-TkqUOu1hAWM,2057
20
- cycode/cli/apps/ai_guardrails/status_command.py,sha256=ThkbEkAbZPQgTP8G6ZzfodgkkUvUBTQLnqeO3D-KikU,3066
21
- cycode/cli/apps/ai_guardrails/uninstall_command.py,sha256=LTvpahXhyivL2eO-fDltyYRR7svjiH8paFZJ3ug23oQ,2300
20
+ cycode/cli/apps/ai_guardrails/status_command.py,sha256=Vqys2-Tp_VerldXfLnXtjq66XU3AQ9h0sbbXkfQ4CoQ,3628
21
+ cycode/cli/apps/ai_guardrails/uninstall_command.py,sha256=dC3U9DBQs3zznvFAMeITcdmBeYApb3cJbrH9RtGmLfI,3061
22
22
  cycode/cli/apps/ai_remediation/__init__.py,sha256=8vYthY9RQeJqEni3AIF5sryz8n-XJQ6VNqG4aEFBAdY,553
23
23
  cycode/cli/apps/ai_remediation/ai_remediation_command.py,sha256=u1EdebaKCEmzv9fXmnIN0xDSLcCmGyjueYKvYfLOj_8,1549
24
24
  cycode/cli/apps/ai_remediation/apply_fix.py,sha256=9zgqiqF9HBQXi7Oz9ZIiANIAuKAMTji1PlNncCEOf5Q,817
@@ -166,7 +166,7 @@ cycode/cli/utils/version_checker.py,sha256=0f5PaTk02ZkDxzBqZOeMV9mU_CWcx6HKW80jU
166
166
  cycode/cli/utils/yaml_utils.py,sha256=R-tqzl0C-zoa42rS7nfWeHu3GJ0jpbQUyyqYYU2hleM,1818
167
167
  cycode/config.py,sha256=jHORGZQcAXkAGSf2XreC-RQoc8sdNWja69QKtPWTbWo,1044
168
168
  cycode/cyclient/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
169
- cycode/cyclient/ai_security_manager_client.py,sha256=JqiSbnoHe_MPljIrW5jvBeyzIIbhj_HbIaErs9ewlaI,3327
169
+ cycode/cyclient/ai_security_manager_client.py,sha256=btkphNQPCwJUdm9N7eLFhFBHbKOu1nOGnPfUHS8xTvo,3416
170
170
  cycode/cyclient/ai_security_manager_service_config.py,sha256=83pQzgOb93JW6E-dznJkI4c0NEXmQRlx9YZKMmjVwp8,808
171
171
  cycode/cyclient/auth_client.py,sha256=TwbmZ358Ancf-Q-IZolvfljZ8691_6botsqd0R0PLPk,2105
172
172
  cycode/cyclient/base_token_auth_client.py,sha256=3JIrSz0-ywVTIfxIs2zs5aGcE-x5GW3AgPHm9qA4ZDE,3857
@@ -186,8 +186,8 @@ cycode/cyclient/report_client.py,sha256=Scq30NeJPzgXv0hPLO1U05AdE9i_2iu6cIrSKpEJ
186
186
  cycode/cyclient/scan_client.py,sha256=uTBEjgfaCVuJREo73p_zkIVA23NQfdJ1d1-bzc7nSKk,12682
187
187
  cycode/cyclient/scan_config_base.py,sha256=mXsPZGYCtp85rv5GIige40yQZXuRcEKUW-VQJ0vgFzk,1201
188
188
  cycode/logger.py,sha256=xAzpkWLZhixO4egRcYn4HXM9lIfx5wHdpkHxNc5jrX8,2225
189
- cycode-3.8.11.dev1.dist-info/METADATA,sha256=6kqiIO-oQ4XKfD1CYqltxVbe3yZUyQtDVh2wP7lEVnE,79038
190
- cycode-3.8.11.dev1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
191
- cycode-3.8.11.dev1.dist-info/entry_points.txt,sha256=iDcVJM8ByLElVgvBgtYxDjw1kT7O8Mo0LcWZIT5L3Ig,45
192
- cycode-3.8.11.dev1.dist-info/licenses/LICENCE,sha256=2Wx4N6mD_4xB7-E3hPkZ3MPhpJy__k_I8MaCSO-PDRo,1068
193
- cycode-3.8.11.dev1.dist-info/RECORD,,
189
+ cycode-3.9.1.dist-info/METADATA,sha256=kvWHRVBFZQNl34xm263wYUijWeWHXqO1cxRiS43fLp4,79032
190
+ cycode-3.9.1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
191
+ cycode-3.9.1.dist-info/entry_points.txt,sha256=iDcVJM8ByLElVgvBgtYxDjw1kT7O8Mo0LcWZIT5L3Ig,45
192
+ cycode-3.9.1.dist-info/licenses/LICENCE,sha256=2Wx4N6mD_4xB7-E3hPkZ3MPhpJy__k_I8MaCSO-PDRo,1068
193
+ cycode-3.9.1.dist-info/RECORD,,