sequential-hooks 0.1.0__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.
- plugins/agy/_sequential_hooks/__init__.py +1 -0
- plugins/agy/_sequential_hooks/_adapter.py +566 -0
- plugins/agy/_sequential_hooks/_doctor.py +413 -0
- plugins/claude/_sequential_hooks/__init__.py +1 -0
- plugins/claude/_sequential_hooks/_adapter.py +560 -0
- plugins/claude/_sequential_hooks/_doctor.py +455 -0
- plugins/codex/_sequential_hooks/__init__.py +1 -0
- plugins/codex/_sequential_hooks/_adapter.py +466 -0
- plugins/codex/_sequential_hooks/_doctor.py +563 -0
- sequential_hooks/__init__.py +3 -0
- sequential_hooks/__main__.py +5 -0
- sequential_hooks/_arguments.py +222 -0
- sequential_hooks/_cleanup.py +88 -0
- sequential_hooks/_cli.py +440 -0
- sequential_hooks/_containment/__init__.py +410 -0
- sequential_hooks/_containment/_posix.py +236 -0
- sequential_hooks/_containment/_uncontained.py +239 -0
- sequential_hooks/_containment/_windows.py +853 -0
- sequential_hooks/_containment/_windows_api.py +570 -0
- sequential_hooks/_containment/_windows_launcher.py +189 -0
- sequential_hooks/_diagnostics.py +265 -0
- sequential_hooks/_doctor/__init__.py +342 -0
- sequential_hooks/_doctor/_command.py +469 -0
- sequential_hooks/_doctor/_common.py +448 -0
- sequential_hooks/_doctor/_types.py +94 -0
- sequential_hooks/_downstream.py +181 -0
- sequential_hooks/_executable.py +187 -0
- sequential_hooks/_executor.py +825 -0
- sequential_hooks/_registry.py +103 -0
- sequential_hooks/_runner.py +48 -0
- sequential_hooks/_types.py +125 -0
- sequential_hooks/hosts/__init__.py +130 -0
- sequential_hooks/hosts/_contract.py +434 -0
- sequential_hooks/hosts/_inspection.py +78 -0
- sequential_hooks/hosts/_json.py +150 -0
- sequential_hooks/hosts/_records.py +273 -0
- sequential_hooks/hosts/_skeleton.py +1326 -0
- sequential_hooks/py.typed +0 -0
- sequential_hooks-0.1.0.dist-info/METADATA +90 -0
- sequential_hooks-0.1.0.dist-info/RECORD +43 -0
- sequential_hooks-0.1.0.dist-info/WHEEL +4 -0
- sequential_hooks-0.1.0.dist-info/entry_points.txt +7 -0
- sequential_hooks-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"""Inspect Claude Code hook configuration sources."""
|
|
2
|
+
|
|
3
|
+
import stat
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Collection, Iterable, Mapping
|
|
6
|
+
from dataclasses import dataclass, replace
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import cast
|
|
9
|
+
|
|
10
|
+
from sequential_hooks.hosts import (
|
|
11
|
+
ConfigSource,
|
|
12
|
+
Finding,
|
|
13
|
+
FindingSeverity,
|
|
14
|
+
InspectedCommand,
|
|
15
|
+
JsonObjectExpectedError,
|
|
16
|
+
JsonProtocolError,
|
|
17
|
+
LaunchSemantics,
|
|
18
|
+
Registration,
|
|
19
|
+
SourceKind,
|
|
20
|
+
deduplicate_source_paths,
|
|
21
|
+
load_object_text,
|
|
22
|
+
parse_timeout,
|
|
23
|
+
project_layers,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_DisableSetting = tuple[bool | None, Finding | None, bool]
|
|
27
|
+
|
|
28
|
+
_LIMITATIONS = (
|
|
29
|
+
'server-managed Claude Code settings cannot be inspected as a local file',
|
|
30
|
+
'macOS com.anthropic.claudecode managed preferences or plist delivery cannot be inspected',
|
|
31
|
+
(
|
|
32
|
+
'Windows HKLM and HKCU SOFTWARE\\Policies\\ClaudeCode Settings registry delivery '
|
|
33
|
+
'cannot be inspected'
|
|
34
|
+
),
|
|
35
|
+
'plugin-provided Claude Code hooks cannot be inspected without scanning plugin caches',
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ClaudeDoctorLoader:
|
|
40
|
+
"""Inspect the Claude Code settings files through the published `DoctorLoader` contract."""
|
|
41
|
+
|
|
42
|
+
def analyze_registration(
|
|
43
|
+
self,
|
|
44
|
+
registration: Registration,
|
|
45
|
+
inspected: InspectedCommand,
|
|
46
|
+
*,
|
|
47
|
+
windows: bool,
|
|
48
|
+
) -> tuple[Finding, ...]:
|
|
49
|
+
"""Return host-specific findings for one inspected registration.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
registration: Accepted registration.
|
|
53
|
+
inspected: Static inspection of the registration's command.
|
|
54
|
+
windows: Whether Windows launch semantics apply.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Findings in report order.
|
|
58
|
+
"""
|
|
59
|
+
del registration, inspected, windows
|
|
60
|
+
return ()
|
|
61
|
+
|
|
62
|
+
def discover_sources(
|
|
63
|
+
self,
|
|
64
|
+
home: Path,
|
|
65
|
+
cwd: Path,
|
|
66
|
+
system_root: Path,
|
|
67
|
+
) -> tuple[ConfigSource, ...]:
|
|
68
|
+
"""Return every configuration source in deterministic inspection order.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
home: Home directory used to locate user configuration.
|
|
72
|
+
cwd: Working directory used to locate project configuration.
|
|
73
|
+
system_root: Filesystem root used to locate system configuration.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Candidate sources, existing or not, in inspection order.
|
|
77
|
+
"""
|
|
78
|
+
return _discover_sources(home, cwd, system_root)
|
|
79
|
+
|
|
80
|
+
def inspection_limitations(self) -> tuple[str, ...]:
|
|
81
|
+
"""Return configuration sources static inspection cannot establish.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Stable limitation descriptions in report order.
|
|
85
|
+
"""
|
|
86
|
+
return _inspection_limitations()
|
|
87
|
+
|
|
88
|
+
def load_registrations(
|
|
89
|
+
self,
|
|
90
|
+
sources: tuple[ConfigSource, ...],
|
|
91
|
+
) -> tuple[tuple[Registration, ...], tuple[Finding, ...]]:
|
|
92
|
+
"""Extract registrations and structural findings in source order.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
sources: Sources returned by `discover_sources`.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Accepted registrations and source-isolated findings.
|
|
99
|
+
"""
|
|
100
|
+
return _load_registrations(sources)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True)
|
|
104
|
+
class _Activation:
|
|
105
|
+
"""Describe activation established from inspected settings files."""
|
|
106
|
+
|
|
107
|
+
_managed: bool | None
|
|
108
|
+
_non_managed: bool | None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _activation(
|
|
112
|
+
sources: Collection[ConfigSource],
|
|
113
|
+
settings: Mapping[Path, _DisableSetting],
|
|
114
|
+
) -> _Activation:
|
|
115
|
+
"""Resolve inspected-file activation without inventing same-scope order."""
|
|
116
|
+
by_scope = {
|
|
117
|
+
scope: [
|
|
118
|
+
value
|
|
119
|
+
for source in sources
|
|
120
|
+
if source.kind is scope
|
|
121
|
+
for value, _finding_value, rejected in (settings[source.path],)
|
|
122
|
+
if value is not None and not rejected
|
|
123
|
+
]
|
|
124
|
+
for scope in (
|
|
125
|
+
SourceKind.MANAGED,
|
|
126
|
+
SourceKind.LOCAL,
|
|
127
|
+
SourceKind.PROJECT,
|
|
128
|
+
SourceKind.USER,
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
winning_scope = next((scope for scope, values in by_scope.items() if values), None)
|
|
132
|
+
if winning_scope is None:
|
|
133
|
+
return _Activation(_managed=True, _non_managed=True)
|
|
134
|
+
values = by_scope[winning_scope]
|
|
135
|
+
if winning_scope is SourceKind.MANAGED:
|
|
136
|
+
disabled = values[-1]
|
|
137
|
+
elif len(set(values)) == 1:
|
|
138
|
+
disabled = values[0]
|
|
139
|
+
else:
|
|
140
|
+
return _Activation(_managed=True, _non_managed=None)
|
|
141
|
+
if not disabled:
|
|
142
|
+
return _Activation(_managed=True, _non_managed=True)
|
|
143
|
+
if winning_scope is SourceKind.MANAGED:
|
|
144
|
+
return _Activation(_managed=False, _non_managed=False)
|
|
145
|
+
return _Activation(_managed=True, _non_managed=False)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _enabled(source: ConfigSource, activation: _Activation) -> bool | None:
|
|
149
|
+
"""Return the inspected activation state for one registration source."""
|
|
150
|
+
if source.kind is SourceKind.MANAGED:
|
|
151
|
+
return activation._managed # noqa: SLF001 # pyright: ignore[reportPrivateUsage]
|
|
152
|
+
return activation._non_managed # noqa: SLF001 # pyright: ignore[reportPrivateUsage]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _finding(source: ConfigSource, code: str, message: str) -> Finding:
|
|
156
|
+
"""Build a source-specific structural finding."""
|
|
157
|
+
return Finding(
|
|
158
|
+
FindingSeverity.WARNING,
|
|
159
|
+
code,
|
|
160
|
+
f'Claude configuration at {source.path} {message}',
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _disable_setting(
|
|
165
|
+
source: ConfigSource,
|
|
166
|
+
loaded: dict[str, object] | None,
|
|
167
|
+
) -> _DisableSetting:
|
|
168
|
+
"""Return the visible setting, finding, and whole-source rejection state."""
|
|
169
|
+
if loaded is None or 'disableAllHooks' not in loaded:
|
|
170
|
+
return None, None, False
|
|
171
|
+
value = loaded['disableAllHooks']
|
|
172
|
+
if isinstance(value, bool):
|
|
173
|
+
return value, None, False
|
|
174
|
+
finding = _finding(
|
|
175
|
+
source,
|
|
176
|
+
'claude_disable_all_hooks_malformed',
|
|
177
|
+
'contains a malformed disableAllHooks setting',
|
|
178
|
+
)
|
|
179
|
+
return None, finding, source.kind is not SourceKind.MANAGED
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _hook_validation_finding(
|
|
183
|
+
source: ConfigSource,
|
|
184
|
+
hook: object,
|
|
185
|
+
) -> Finding | None:
|
|
186
|
+
"""Return the first structural problem in one hook."""
|
|
187
|
+
finding = None
|
|
188
|
+
if not isinstance(hook, dict):
|
|
189
|
+
finding = _finding(
|
|
190
|
+
source,
|
|
191
|
+
'claude_hook_malformed',
|
|
192
|
+
'contains a malformed hook',
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
hook_data = cast('dict[str, object]', hook)
|
|
196
|
+
if hook_data.get('type') != 'command':
|
|
197
|
+
finding = _finding(
|
|
198
|
+
source,
|
|
199
|
+
'claude_hook_type_unsupported',
|
|
200
|
+
'contains a hook whose type is not the documented command form',
|
|
201
|
+
)
|
|
202
|
+
elif not isinstance(hook_data.get('command'), str) or not hook_data.get('command'):
|
|
203
|
+
finding = _finding(
|
|
204
|
+
source,
|
|
205
|
+
'claude_command_malformed',
|
|
206
|
+
'contains a command hook without a string command',
|
|
207
|
+
)
|
|
208
|
+
elif 'args' in hook_data and (
|
|
209
|
+
not isinstance(raw_args := hook_data['args'], list)
|
|
210
|
+
or not all(isinstance(argument, str) for argument in cast('list[object]', raw_args))
|
|
211
|
+
):
|
|
212
|
+
finding = _finding(
|
|
213
|
+
source,
|
|
214
|
+
'claude_args_malformed',
|
|
215
|
+
'contains a command hook with malformed args',
|
|
216
|
+
)
|
|
217
|
+
elif (
|
|
218
|
+
'timeout' in hook_data
|
|
219
|
+
and parse_timeout(
|
|
220
|
+
hook_data['timeout'],
|
|
221
|
+
positive=False,
|
|
222
|
+
)
|
|
223
|
+
is None
|
|
224
|
+
):
|
|
225
|
+
finding = _finding(
|
|
226
|
+
source,
|
|
227
|
+
'claude_timeout_malformed',
|
|
228
|
+
'contains a command hook with a malformed timeout',
|
|
229
|
+
)
|
|
230
|
+
return finding
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _inspection_limitations() -> tuple[str, ...]:
|
|
234
|
+
"""Return Claude sources static inspection cannot establish."""
|
|
235
|
+
return _LIMITATIONS
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _load_source(source: ConfigSource) -> tuple[dict[str, object] | None, Finding | None]:
|
|
239
|
+
"""Load one source without exposing content in diagnostics."""
|
|
240
|
+
try:
|
|
241
|
+
text = source.path.read_text(encoding='utf-8')
|
|
242
|
+
except UnicodeDecodeError:
|
|
243
|
+
return None, _finding(
|
|
244
|
+
source,
|
|
245
|
+
'claude_source_encoding_malformed',
|
|
246
|
+
'contains malformed UTF-8',
|
|
247
|
+
)
|
|
248
|
+
except OSError:
|
|
249
|
+
return None, _finding(source, 'claude_source_unreadable', 'cannot be read')
|
|
250
|
+
try:
|
|
251
|
+
loaded = load_object_text(text)
|
|
252
|
+
except JsonObjectExpectedError:
|
|
253
|
+
return None, _finding(source, 'claude_settings_malformed', 'must contain a JSON object')
|
|
254
|
+
except JsonProtocolError:
|
|
255
|
+
return None, _finding(source, 'claude_json_malformed', 'contains malformed JSON')
|
|
256
|
+
return cast('dict[str, object]', loaded), None
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _managed_directory(system_root: Path) -> Path:
|
|
260
|
+
"""Return the documented managed-settings directory for this platform."""
|
|
261
|
+
if sys.platform == 'darwin':
|
|
262
|
+
return system_root / 'Library/Application Support/ClaudeCode'
|
|
263
|
+
if sys.platform == 'win32':
|
|
264
|
+
return system_root / 'Program Files/ClaudeCode'
|
|
265
|
+
return system_root / 'etc/claude-code'
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _managed_drop_ins(directory: Path) -> tuple[ConfigSource, ...]:
|
|
269
|
+
"""Return documented managed drop-ins or isolate enumeration failure."""
|
|
270
|
+
try:
|
|
271
|
+
if not stat.S_ISDIR(directory.stat().st_mode):
|
|
272
|
+
return ()
|
|
273
|
+
paths = tuple(directory.iterdir())
|
|
274
|
+
except FileNotFoundError:
|
|
275
|
+
return ()
|
|
276
|
+
except OSError:
|
|
277
|
+
return (ConfigSource(directory, SourceKind.MANAGED, exists=True),)
|
|
278
|
+
return tuple(
|
|
279
|
+
ConfigSource(path, SourceKind.MANAGED, exists=True)
|
|
280
|
+
for path in sorted(paths, key=lambda item: item.name)
|
|
281
|
+
if path.is_file() and not path.name.startswith('.') and path.suffix == '.json'
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _discover_sources(
|
|
286
|
+
home: Path,
|
|
287
|
+
cwd: Path,
|
|
288
|
+
system_root: Path,
|
|
289
|
+
) -> tuple[ConfigSource, ...]:
|
|
290
|
+
"""Return Claude sources in deterministic inspection order."""
|
|
291
|
+
paths: list[tuple[Path, SourceKind]] = [(home / '.claude/settings.json', SourceKind.USER)]
|
|
292
|
+
for layer in project_layers(cwd):
|
|
293
|
+
paths.extend(
|
|
294
|
+
(
|
|
295
|
+
(layer / '.claude/settings.json', SourceKind.PROJECT),
|
|
296
|
+
(layer / '.claude/settings.local.json', SourceKind.LOCAL),
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
managed_directory = _managed_directory(system_root)
|
|
300
|
+
paths.append((managed_directory / 'managed-settings.json', SourceKind.MANAGED))
|
|
301
|
+
drop_in_directory = managed_directory / 'managed-settings.d'
|
|
302
|
+
sources = tuple(
|
|
303
|
+
ConfigSource(path, kind, path.exists()) for path, kind in deduplicate_source_paths(paths)
|
|
304
|
+
)
|
|
305
|
+
return (*sources, *_managed_drop_ins(drop_in_directory))
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _registration(
|
|
309
|
+
source: ConfigSource,
|
|
310
|
+
event: str,
|
|
311
|
+
matcher: str | None,
|
|
312
|
+
hook: object,
|
|
313
|
+
*,
|
|
314
|
+
enabled: bool | None,
|
|
315
|
+
) -> tuple[Registration | None, Finding | None]:
|
|
316
|
+
"""Parse one command hook."""
|
|
317
|
+
validation_finding = _hook_validation_finding(source, hook)
|
|
318
|
+
if validation_finding is not None:
|
|
319
|
+
return None, validation_finding
|
|
320
|
+
hook_data = cast('dict[str, object]', hook)
|
|
321
|
+
command = cast('str', hook_data.get('command'))
|
|
322
|
+
raw_args = cast('list[str] | None', hook_data.get('args'))
|
|
323
|
+
raw_timeout = hook_data.get('timeout')
|
|
324
|
+
|
|
325
|
+
args = tuple(raw_args) if isinstance(raw_args, list) else None
|
|
326
|
+
direct = args is not None
|
|
327
|
+
registration = Registration(
|
|
328
|
+
source=source,
|
|
329
|
+
event=event,
|
|
330
|
+
matcher=matcher,
|
|
331
|
+
command=command,
|
|
332
|
+
args=args,
|
|
333
|
+
launch_semantics=(LaunchSemantics.DIRECT_ARGV if direct else LaunchSemantics.POSIX_SHELL),
|
|
334
|
+
command_windows=None,
|
|
335
|
+
windows_launch_semantics=(
|
|
336
|
+
LaunchSemantics.DIRECT_ARGV if direct else LaunchSemantics.HOST_TOKENIZER
|
|
337
|
+
),
|
|
338
|
+
timeout=parse_timeout(raw_timeout, positive=False),
|
|
339
|
+
enabled=enabled,
|
|
340
|
+
type=cast('str', hook_data['type']),
|
|
341
|
+
)
|
|
342
|
+
if direct:
|
|
343
|
+
return registration, None
|
|
344
|
+
if sys.platform != 'win32':
|
|
345
|
+
finding = Finding(
|
|
346
|
+
FindingSeverity.INFO,
|
|
347
|
+
'claude_exec_form_recommended',
|
|
348
|
+
'Claude Code command strings use a POSIX shell',
|
|
349
|
+
'Use the exec form with explicit args.',
|
|
350
|
+
)
|
|
351
|
+
else:
|
|
352
|
+
finding = Finding(
|
|
353
|
+
FindingSeverity.INFO,
|
|
354
|
+
'claude_windows_shell_conditional',
|
|
355
|
+
'Claude Code command strings use Git Bash on Windows when statically '
|
|
356
|
+
'established as available, otherwise PowerShell; shell availability was not probed',
|
|
357
|
+
'Use the exec form with explicit args, especially on Windows.',
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
return replace(registration, configuration_findings=(finding,)), None
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _event_registrations(
|
|
364
|
+
source: ConfigSource,
|
|
365
|
+
event: object,
|
|
366
|
+
groups: object,
|
|
367
|
+
*,
|
|
368
|
+
enabled: bool | None,
|
|
369
|
+
) -> tuple[list[Registration], list[Finding]]:
|
|
370
|
+
"""Parse one event layer."""
|
|
371
|
+
if not isinstance(event, str) or not isinstance(groups, list):
|
|
372
|
+
return [], [_finding(source, 'claude_event_malformed', 'contains a malformed event')]
|
|
373
|
+
registrations: list[Registration] = []
|
|
374
|
+
findings: list[Finding] = []
|
|
375
|
+
for group in cast('list[object]', groups):
|
|
376
|
+
if not isinstance(group, dict):
|
|
377
|
+
findings.append(
|
|
378
|
+
_finding(
|
|
379
|
+
source, 'claude_matcher_group_malformed', 'contains a malformed matcher group'
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
continue
|
|
383
|
+
group_data = cast('dict[str, object]', group)
|
|
384
|
+
matcher = group_data.get('matcher')
|
|
385
|
+
if 'matcher' in group_data and not isinstance(matcher, str):
|
|
386
|
+
findings.append(
|
|
387
|
+
_finding(source, 'claude_matcher_malformed', 'contains a malformed matcher')
|
|
388
|
+
)
|
|
389
|
+
continue
|
|
390
|
+
parsed_matcher = cast('str | None', matcher)
|
|
391
|
+
hooks = group_data.get('hooks')
|
|
392
|
+
if not isinstance(hooks, list):
|
|
393
|
+
findings.append(
|
|
394
|
+
_finding(source, 'claude_hooks_malformed', 'contains a malformed hooks list')
|
|
395
|
+
)
|
|
396
|
+
continue
|
|
397
|
+
for hook in cast('list[object]', hooks):
|
|
398
|
+
registration, finding = _registration(
|
|
399
|
+
source,
|
|
400
|
+
event,
|
|
401
|
+
parsed_matcher,
|
|
402
|
+
hook,
|
|
403
|
+
enabled=enabled,
|
|
404
|
+
)
|
|
405
|
+
if registration is not None:
|
|
406
|
+
registrations.append(registration)
|
|
407
|
+
if finding is not None:
|
|
408
|
+
findings.append(finding)
|
|
409
|
+
return registrations, findings
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _load_registrations(
|
|
413
|
+
sources: Iterable[ConfigSource],
|
|
414
|
+
) -> tuple[tuple[Registration, ...], tuple[Finding, ...]]:
|
|
415
|
+
"""Extract registrations and findings in source order."""
|
|
416
|
+
source_list = tuple(sources)
|
|
417
|
+
loaded_sources = tuple(
|
|
418
|
+
(source, *_load_source(source)) if source.exists else (source, None, None)
|
|
419
|
+
for source in source_list
|
|
420
|
+
)
|
|
421
|
+
settings = {
|
|
422
|
+
source.path: _disable_setting(source, loaded)
|
|
423
|
+
for source, loaded, _finding_value in loaded_sources
|
|
424
|
+
}
|
|
425
|
+
activation = _activation(source_list, settings)
|
|
426
|
+
registrations: list[Registration] = []
|
|
427
|
+
findings: list[Finding] = []
|
|
428
|
+
for source, loaded, load_finding in loaded_sources:
|
|
429
|
+
if load_finding is not None:
|
|
430
|
+
findings.append(load_finding)
|
|
431
|
+
continue
|
|
432
|
+
_value, setting_finding, rejected = settings[source.path]
|
|
433
|
+
if setting_finding is not None:
|
|
434
|
+
findings.append(setting_finding)
|
|
435
|
+
if loaded is None or rejected:
|
|
436
|
+
continue
|
|
437
|
+
hooks = loaded.get('hooks')
|
|
438
|
+
if hooks is None:
|
|
439
|
+
continue
|
|
440
|
+
if not isinstance(hooks, dict):
|
|
441
|
+
findings.append(
|
|
442
|
+
_finding(source, 'claude_hooks_malformed', 'contains a malformed hooks object')
|
|
443
|
+
)
|
|
444
|
+
continue
|
|
445
|
+
hook_events = cast('dict[str, object]', hooks)
|
|
446
|
+
for event, groups in hook_events.items():
|
|
447
|
+
event_registrations, event_findings = _event_registrations(
|
|
448
|
+
source,
|
|
449
|
+
event,
|
|
450
|
+
groups,
|
|
451
|
+
enabled=_enabled(source, activation),
|
|
452
|
+
)
|
|
453
|
+
registrations.extend(event_registrations)
|
|
454
|
+
findings.extend(event_findings)
|
|
455
|
+
return tuple(registrations), tuple(findings)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Provide the private Codex adapter implementation."""
|