claude-dev-env 1.25.2 → 1.26.1
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.
- package/CLAUDE.md +6 -0
- package/agents/clean-coder.md +1 -1
- package/docs/CODE_RULES.md +3 -1
- package/hooks/HOOK_SPECS_PROMPT_WORKFLOW.md +54 -0
- package/hooks/blocking/{code-rules-enforcer.py → code_rules_enforcer.py} +154 -5
- package/hooks/blocking/test_code_rules_enforcer.py +61 -0
- package/hooks/blocking/test_code_rules_enforcer_any_type_ignore.py +2 -2
- package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +2 -2
- package/hooks/blocking/test_code_rules_enforcer_conftest_anchor.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +2 -2
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +183 -0
- package/hooks/blocking/test_code_rules_enforcer_fstring_scan.py +4 -4
- package/hooks/blocking/test_code_rules_enforcer_logger_fstring.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_magic_string_masking.py +104 -0
- package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +2 -2
- package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +2 -2
- package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +1 -1
- package/hooks/blocking/test_destructive_command_blocker.py +1 -1
- package/hooks/blocking/test_gh_body_arg_blocker.py +1 -1
- package/hooks/blocking/test_pr_description_enforcer.py +8 -8
- package/hooks/blocking/test_tdd_enforcer.py +1 -1
- package/hooks/github-action/pre-push-review.yml +27 -0
- package/hooks/hooks.json +28 -28
- package/hooks/lifecycle/{config-change-guard.py → config_change_guard.py} +26 -12
- package/hooks/lifecycle/test_config_change_guard.py +3 -3
- package/hooks/notification/{attention-needed-notify.py → attention_needed_notify.py} +7 -0
- package/hooks/notification/{claude-notification-handler.py → claude_notification_handler.py} +8 -0
- package/hooks/notification/notification_utils.py +56 -0
- package/hooks/notification/subagent_complete_notify.py +381 -0
- package/hooks/notification/test_attention_needed_notify.py +47 -0
- package/hooks/notification/test_claude_notification_handler.py +54 -0
- package/hooks/notification/test_notification_utils.py +45 -0
- package/hooks/notification/test_subagent_complete_notify.py +79 -0
- package/hooks/validators/README.md +5 -1
- package/hooks/validators/abbreviation_checks.py +1 -1
- package/hooks/validators/code_quality_checks.py +1 -1
- package/hooks/validators/config.py +5 -0
- package/hooks/validators/conftest.py +10 -0
- package/hooks/validators/exempt_paths.py +1 -1
- package/hooks/validators/git_checks.py +80 -0
- package/hooks/validators/magic_value_checks.py +2 -2
- package/hooks/validators/pr_reference_checks.py +1 -1
- package/hooks/validators/python_antipattern_checks.py +1 -1
- package/hooks/validators/run_all_validators.py +53 -105
- package/hooks/validators/security_checks.py +1 -1
- package/hooks/validators/test_abbreviation_checks.py +2 -2
- package/hooks/validators/test_code_quality_checks.py +2 -2
- package/hooks/validators/test_file_structure_checks.py +1 -1
- package/hooks/validators/test_git_checks.py +79 -13
- package/hooks/validators/test_health_check.py +1 -1
- package/hooks/validators/test_magic_value_checks.py +2 -2
- package/hooks/validators/test_mypy_integration.py +1 -1
- package/hooks/validators/test_output_formatter.py +3 -1
- package/hooks/validators/test_pr_reference_checks.py +2 -2
- package/hooks/validators/test_python_antipattern_checks.py +2 -2
- package/hooks/validators/test_python_style_checks.py +2 -4
- package/hooks/validators/test_react_checks.py +1 -1
- package/hooks/validators/test_ruff_integration.py +1 -1
- package/hooks/validators/test_run_all_validators.py +75 -43
- package/hooks/validators/test_run_all_validators_integration.py +14 -37
- package/hooks/validators/test_security_checks.py +2 -2
- package/hooks/validators/test_test_safety_checks.py +1 -1
- package/hooks/validators/test_todo_checks.py +2 -2
- package/hooks/validators/test_type_safety_checks.py +2 -2
- package/hooks/validators/test_useless_test_checks.py +2 -2
- package/hooks/validators/test_validator_base.py +1 -1
- package/hooks/validators/test_verify_paths.py +2 -4
- package/hooks/validators/todo_checks.py +1 -1
- package/hooks/validators/type_safety_checks.py +1 -1
- package/hooks/validators/useless_test_checks.py +1 -1
- package/package.json +1 -1
- package/rules/file-global-constants.md +71 -0
- package/rules/gh-body-file.md +1 -1
- package/rules/prompt-workflow-context-controls.md +48 -0
- package/scripts/sync_to_cursor/rules.py +2 -2
- package/scripts/tests/test_sync_to_cursor.py +2 -2
- package/skills/bugteam/CONSTRAINTS.md +37 -0
- package/skills/bugteam/EXAMPLES.md +64 -0
- package/skills/bugteam/PROMPTS.md +175 -0
- package/skills/bugteam/SKILL.md +204 -295
- package/skills/bugteam/SKILL_EVALS.md +346 -0
- package/skills/bugteam/scripts/README.md +37 -0
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +334 -0
- package/skills/bugteam/scripts/bugteam_preflight.py +135 -0
- package/skills/rule-audit/SKILL.md +4 -4
- /package/hooks/advisory/{migration-safety-advisor.py → migration_safety_advisor.py} +0 -0
- /package/hooks/advisory/{refactor-guard.py → refactor_guard.py} +0 -0
- /package/hooks/blocking/{block-main-commit.py → block_main_commit.py} +0 -0
- /package/hooks/blocking/{content-search-to-zoekt-redirector.py → content_search_to_zoekt_redirector.py} +0 -0
- /package/hooks/blocking/{destructive-command-blocker.py → destructive_command_blocker.py} +0 -0
- /package/hooks/blocking/{gh-body-arg-blocker.py → gh_body_arg_blocker.py} +0 -0
- /package/hooks/blocking/{hedging-language-blocker.py → hedging_language_blocker.py} +0 -0
- /package/hooks/blocking/{pr-description-enforcer.py → pr_description_enforcer.py} +0 -0
- /package/hooks/blocking/{sensitive-file-protector.py → sensitive_file_protector.py} +0 -0
- /package/hooks/blocking/{tdd-enforcer.py → tdd_enforcer.py} +0 -0
- /package/hooks/blocking/{test-preflight-check.py → test_preflight_check.py} +0 -0
- /package/hooks/blocking/{write-existing-file-blocker.py → write_existing_file_blocker.py} +0 -0
- /package/hooks/git-hooks/{post-commit.py → post_commit.py} +0 -0
- /package/hooks/lifecycle/{session-end-cleanup.py → session_end_cleanup.py} +0 -0
- /package/hooks/{rewrite-plugin-paths.py → rewrite_plugin_paths.py} +0 -0
- /package/hooks/session/{plugin-data-dir-cleanup.py → plugin_data_dir_cleanup.py} +0 -0
- /package/hooks/validation/{hook-format-validator.py → hook_format_validator.py} +0 -0
- /package/hooks/workflow/{auto-formatter.py → auto_formatter.py} +0 -0
- /package/hooks/workflow/{investigation-tracker-reset.py → investigation_tracker_reset.py} +0 -0
- /package/scripts/{sync-to-cursor.py → sync_to_cursor.py} +0 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Tests that check_magic_values does not flag digits inside string literals.
|
|
2
|
+
|
|
3
|
+
The regex-based magic-value scan operates on stripped source lines. Before
|
|
4
|
+
this fix it matched digits appearing inside string literals (for example the
|
|
5
|
+
``8`` inside ``"utf-8"``), producing false positives on any line that passes
|
|
6
|
+
an encoding, mode, or similar string kwarg containing a digit. The scanner
|
|
7
|
+
must mask string literals before searching for numeric magic values so only
|
|
8
|
+
genuine literal numbers in code are reported.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from types import ModuleType
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_enforcer_module() -> ModuleType:
|
|
19
|
+
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
20
|
+
specification = importlib.util.spec_from_file_location(
|
|
21
|
+
"code_rules_enforcer_for_string_masking_tests",
|
|
22
|
+
module_path,
|
|
23
|
+
)
|
|
24
|
+
assert specification is not None
|
|
25
|
+
assert specification.loader is not None
|
|
26
|
+
module = importlib.util.module_from_spec(specification)
|
|
27
|
+
specification.loader.exec_module(module)
|
|
28
|
+
return module
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
PRODUCTION_FILE_PATH = "packages/claude-dev-env/hooks/blocking/example_production.py"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_check_magic_values_should_not_flag_digits_inside_double_quoted_string() -> (
|
|
38
|
+
None
|
|
39
|
+
):
|
|
40
|
+
source = (
|
|
41
|
+
"def read_configuration(path):\n"
|
|
42
|
+
' text = path.read_text(encoding="utf-8")\n'
|
|
43
|
+
" return text\n"
|
|
44
|
+
)
|
|
45
|
+
issues = code_rules_enforcer.check_magic_values(source, PRODUCTION_FILE_PATH)
|
|
46
|
+
assert issues == [], f"Expected no issues for utf-8 string, got: {issues}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_check_magic_values_should_not_flag_digits_inside_single_quoted_string() -> (
|
|
50
|
+
None
|
|
51
|
+
):
|
|
52
|
+
source = (
|
|
53
|
+
"def read_configuration(path):\n"
|
|
54
|
+
" text = path.read_text(encoding='utf-8')\n"
|
|
55
|
+
" return text\n"
|
|
56
|
+
)
|
|
57
|
+
issues = code_rules_enforcer.check_magic_values(source, PRODUCTION_FILE_PATH)
|
|
58
|
+
assert issues == [], (
|
|
59
|
+
f"Expected no issues for single-quoted utf-8 string, got: {issues}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_check_magic_values_should_not_flag_digits_inside_multiple_string_kwargs() -> (
|
|
64
|
+
None
|
|
65
|
+
):
|
|
66
|
+
source = (
|
|
67
|
+
"def open_log(path):\n"
|
|
68
|
+
' handle = open(path, mode="rb", encoding="utf-8", errors="replace")\n'
|
|
69
|
+
" return handle\n"
|
|
70
|
+
)
|
|
71
|
+
issues = code_rules_enforcer.check_magic_values(source, PRODUCTION_FILE_PATH)
|
|
72
|
+
assert issues == [], f"Expected no issues for string-only kwargs, got: {issues}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_check_magic_values_should_still_flag_real_magic_value_outside_string() -> None:
|
|
76
|
+
source = (
|
|
77
|
+
"def classify_exit(code: int) -> int:\n"
|
|
78
|
+
" if code == 5:\n"
|
|
79
|
+
" return 0\n"
|
|
80
|
+
" return code\n"
|
|
81
|
+
)
|
|
82
|
+
issues = code_rules_enforcer.check_magic_values(source, PRODUCTION_FILE_PATH)
|
|
83
|
+
assert any(
|
|
84
|
+
issue.endswith("Magic value 5 - extract to named constant") for issue in issues
|
|
85
|
+
), f"Expected magic value 5 to be flagged, got: {issues}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_check_magic_values_should_flag_real_number_even_when_line_contains_string() -> (
|
|
89
|
+
None
|
|
90
|
+
):
|
|
91
|
+
source = (
|
|
92
|
+
"def classify_exit(code: int) -> int:\n"
|
|
93
|
+
' marker = "utf-8"\n'
|
|
94
|
+
" if code == 5:\n"
|
|
95
|
+
" return 0\n"
|
|
96
|
+
" return code\n"
|
|
97
|
+
)
|
|
98
|
+
issues = code_rules_enforcer.check_magic_values(source, PRODUCTION_FILE_PATH)
|
|
99
|
+
assert any(
|
|
100
|
+
issue.endswith("Magic value 5 - extract to named constant") for issue in issues
|
|
101
|
+
), f"Expected magic value 5 to be flagged alongside string literal, got: {issues}"
|
|
102
|
+
assert not any("Magic value 8" in issue for issue in issues), (
|
|
103
|
+
f"utf-8 should not produce a magic value 8 issue, got: {issues}"
|
|
104
|
+
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Unit tests for
|
|
1
|
+
"""Unit tests for code_rules_enforcer boolean naming-pattern check."""
|
|
2
2
|
|
|
3
3
|
import importlib.util
|
|
4
4
|
import pathlib
|
|
@@ -11,7 +11,7 @@ if str(_HOOK_DIRECTORY) not in sys.path:
|
|
|
11
11
|
|
|
12
12
|
_hook_spec = importlib.util.spec_from_file_location(
|
|
13
13
|
"code_rules_enforcer",
|
|
14
|
-
_HOOK_DIRECTORY / "
|
|
14
|
+
_HOOK_DIRECTORY / "code_rules_enforcer.py",
|
|
15
15
|
)
|
|
16
16
|
assert _hook_spec is not None
|
|
17
17
|
assert _hook_spec.loader is not None
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Unit tests for TYPE_CHECKING-scoped import exemption in
|
|
1
|
+
"""Unit tests for TYPE_CHECKING-scoped import exemption in code_rules_enforcer."""
|
|
2
2
|
|
|
3
3
|
import importlib.util
|
|
4
4
|
import pathlib
|
|
@@ -10,7 +10,7 @@ if str(_HOOK_DIR) not in sys.path:
|
|
|
10
10
|
|
|
11
11
|
hook_spec = importlib.util.spec_from_file_location(
|
|
12
12
|
"code_rules_enforcer",
|
|
13
|
-
_HOOK_DIR / "
|
|
13
|
+
_HOOK_DIR / "code_rules_enforcer.py",
|
|
14
14
|
)
|
|
15
15
|
assert hook_spec is not None
|
|
16
16
|
assert hook_spec.loader is not None
|
|
@@ -18,7 +18,7 @@ class ContentSearchHookIntegrationTests(unittest.TestCase):
|
|
|
18
18
|
get_zoekt_redirect_reason_brief,
|
|
19
19
|
)
|
|
20
20
|
|
|
21
|
-
hook_path = hook_directory / "
|
|
21
|
+
hook_path = hook_directory / "content_search_to_zoekt_redirector.py"
|
|
22
22
|
destructive_gate_label_prefix = "[destructive-gate]"
|
|
23
23
|
destructive_gate_label_prefix_value = f"{destructive_gate_label_prefix} "
|
|
24
24
|
expected_decision = "deny"
|
|
@@ -7,7 +7,7 @@ import sys
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
SCRIPT_PATH = Path(__file__).parent / "
|
|
10
|
+
SCRIPT_PATH = Path(__file__).parent / "destructive_command_blocker.py"
|
|
11
11
|
GH_GATE_USER_FACING_PREFIX = "[gh-gate]"
|
|
12
12
|
GH_REDIRECT_ACTIVE_ENV_VAR = "CLAUDE_GH_REDIRECT_ACTIVE"
|
|
13
13
|
GH_REDIRECT_ACTIVE_VALUE = "1"
|
|
@@ -10,7 +10,7 @@ if str(_HOOK_DIR) not in sys.path:
|
|
|
10
10
|
|
|
11
11
|
hook_spec = importlib.util.spec_from_file_location(
|
|
12
12
|
"gh_body_arg_blocker",
|
|
13
|
-
_HOOK_DIR / "
|
|
13
|
+
_HOOK_DIR / "gh_body_arg_blocker.py",
|
|
14
14
|
)
|
|
15
15
|
assert hook_spec is not None
|
|
16
16
|
assert hook_spec.loader is not None
|
|
@@ -15,7 +15,7 @@ from _gh_body_arg_utils import get_logical_first_line
|
|
|
15
15
|
|
|
16
16
|
hook_spec = importlib.util.spec_from_file_location(
|
|
17
17
|
"pr_description_enforcer",
|
|
18
|
-
_HOOK_DIR / "
|
|
18
|
+
_HOOK_DIR / "pr_description_enforcer.py",
|
|
19
19
|
)
|
|
20
20
|
assert hook_spec is not None
|
|
21
21
|
assert hook_spec.loader is not None
|
|
@@ -213,7 +213,7 @@ def test_read_body_file_rejects_relative_path_traversal(tmp_path) -> None:
|
|
|
213
213
|
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
214
214
|
if str(_HOOK_DIR) not in sys.path:
|
|
215
215
|
sys.path.insert(0, str(_HOOK_DIR))
|
|
216
|
-
spec = importlib.util.spec_from_file_location('pde', _HOOK_DIR / '
|
|
216
|
+
spec = importlib.util.spec_from_file_location('pde', _HOOK_DIR / 'pr_description_enforcer.py')
|
|
217
217
|
m = importlib.util.module_from_spec(spec)
|
|
218
218
|
spec.loader.exec_module(m)
|
|
219
219
|
import os, pytest
|
|
@@ -229,7 +229,7 @@ def test_read_body_file_rejects_relative_path_traversal(tmp_path) -> None:
|
|
|
229
229
|
def test_read_body_file_allows_absolute_path_outside_cwd(tmp_path) -> None:
|
|
230
230
|
import importlib.util, pathlib, sys
|
|
231
231
|
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
232
|
-
spec = importlib.util.spec_from_file_location('pde2', _HOOK_DIR / '
|
|
232
|
+
spec = importlib.util.spec_from_file_location('pde2', _HOOK_DIR / 'pr_description_enforcer.py')
|
|
233
233
|
m = importlib.util.module_from_spec(spec)
|
|
234
234
|
spec.loader.exec_module(m)
|
|
235
235
|
body_file = tmp_path / 'body.md'
|
|
@@ -241,7 +241,7 @@ def test_read_body_file_allows_absolute_path_outside_cwd(tmp_path) -> None:
|
|
|
241
241
|
def test_reassemble_split_quoted_value_returns_none_for_unclosed_quote() -> None:
|
|
242
242
|
import importlib.util, pathlib, sys
|
|
243
243
|
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
244
|
-
spec = importlib.util.spec_from_file_location('pde3', _HOOK_DIR / '
|
|
244
|
+
spec = importlib.util.spec_from_file_location('pde3', _HOOK_DIR / 'pr_description_enforcer.py')
|
|
245
245
|
m = importlib.util.module_from_spec(spec)
|
|
246
246
|
spec.loader.exec_module(m)
|
|
247
247
|
result = m._reassemble_split_quoted_value("'unclosed", [])
|
|
@@ -272,7 +272,7 @@ def test_body_file_path_traversal_returns_none() -> None:
|
|
|
272
272
|
import importlib.util
|
|
273
273
|
import pathlib
|
|
274
274
|
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
275
|
-
spec = importlib.util.spec_from_file_location('pde_t', _HOOK_DIR / '
|
|
275
|
+
spec = importlib.util.spec_from_file_location('pde_t', _HOOK_DIR / 'pr_description_enforcer.py')
|
|
276
276
|
m = importlib.util.module_from_spec(spec)
|
|
277
277
|
spec.loader.exec_module(m)
|
|
278
278
|
result = m._resolve_body_file_value("../../../etc/passwd")
|
|
@@ -303,7 +303,7 @@ def test_read_body_file_rejects_absolute_symlink_outside_cwd(tmp_path: pathlib.P
|
|
|
303
303
|
import importlib.util
|
|
304
304
|
import pytest
|
|
305
305
|
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
306
|
-
spec = importlib.util.spec_from_file_location('pde_sym', _HOOK_DIR / '
|
|
306
|
+
spec = importlib.util.spec_from_file_location('pde_sym', _HOOK_DIR / 'pr_description_enforcer.py')
|
|
307
307
|
m = importlib.util.module_from_spec(spec)
|
|
308
308
|
spec.loader.exec_module(m)
|
|
309
309
|
target_file = tmp_path / "secret.txt"
|
|
@@ -321,7 +321,7 @@ def test_read_body_file_allows_real_absolute_file_inside_cwd(tmp_path: pathlib.P
|
|
|
321
321
|
"""Real absolute file path that exists must be read successfully."""
|
|
322
322
|
import importlib.util
|
|
323
323
|
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
324
|
-
spec = importlib.util.spec_from_file_location('pde_abs', _HOOK_DIR / '
|
|
324
|
+
spec = importlib.util.spec_from_file_location('pde_abs', _HOOK_DIR / 'pr_description_enforcer.py')
|
|
325
325
|
m = importlib.util.module_from_spec(spec)
|
|
326
326
|
spec.loader.exec_module(m)
|
|
327
327
|
body_file = tmp_path / "body.md"
|
|
@@ -334,7 +334,7 @@ def test_read_body_file_allows_in_cwd_symlink_pointing_into_cwd(tmp_path: pathli
|
|
|
334
334
|
"""Symlink inside cwd pointing to another file inside cwd must be readable."""
|
|
335
335
|
import importlib.util
|
|
336
336
|
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
337
|
-
spec = importlib.util.spec_from_file_location('pde_inlink', _HOOK_DIR / '
|
|
337
|
+
spec = importlib.util.spec_from_file_location('pde_inlink', _HOOK_DIR / 'pr_description_enforcer.py')
|
|
338
338
|
m = importlib.util.module_from_spec(spec)
|
|
339
339
|
spec.loader.exec_module(m)
|
|
340
340
|
real_file = tmp_path / "real.md"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Pre-Push Review
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
validate:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- name: Checkout code
|
|
13
|
+
uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Set up Python
|
|
16
|
+
uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.13"
|
|
19
|
+
|
|
20
|
+
- name: Install dependencies
|
|
21
|
+
run: |
|
|
22
|
+
python -m pip install --upgrade pip
|
|
23
|
+
pip install pyyaml pytest
|
|
24
|
+
|
|
25
|
+
- name: Run validators
|
|
26
|
+
working-directory: packages/claude-dev-env/hooks/validators
|
|
27
|
+
run: python run_all_validators.py --json
|
package/hooks/hooks.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
10
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/
|
|
10
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/content_search_to_zoekt_redirector.py",
|
|
11
11
|
"timeout": 10
|
|
12
12
|
}
|
|
13
13
|
]
|
|
@@ -17,42 +17,42 @@
|
|
|
17
17
|
"hooks": [
|
|
18
18
|
{
|
|
19
19
|
"type": "command",
|
|
20
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/
|
|
20
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/write_existing_file_blocker.py",
|
|
21
21
|
"timeout": 10
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"type": "command",
|
|
25
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/
|
|
25
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/sensitive_file_protector.py",
|
|
26
26
|
"timeout": 10
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"type": "command",
|
|
30
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/
|
|
30
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/pyautogui_scroll_blocker.py",
|
|
31
31
|
"timeout": 10
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
"type": "command",
|
|
35
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/validation/
|
|
35
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/validation/hook_format_validator.py",
|
|
36
36
|
"timeout": 15
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"type": "command",
|
|
40
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks
|
|
40
|
+
"command": "python3 -c \"import sys; sys.path.insert(0, r'${CLAUDE_PLUGIN_ROOT}/hooks'); from validators.run_all_validators import main; sys.exit(main())\"",
|
|
41
41
|
"timeout": 15
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
"type": "command",
|
|
45
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/
|
|
45
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/code_rules_enforcer.py",
|
|
46
46
|
"timeout": 15
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
"type": "command",
|
|
50
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/
|
|
50
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/tdd_enforcer.py",
|
|
51
51
|
"timeout": 10
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
54
|
"type": "command",
|
|
55
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/validation/
|
|
55
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/validation/code_style_validator.py",
|
|
56
56
|
"timeout": 15
|
|
57
57
|
}
|
|
58
58
|
]
|
|
@@ -62,12 +62,12 @@
|
|
|
62
62
|
"hooks": [
|
|
63
63
|
{
|
|
64
64
|
"type": "command",
|
|
65
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/advisory/
|
|
65
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/advisory/refactor_guard.py",
|
|
66
66
|
"timeout": 15
|
|
67
67
|
},
|
|
68
68
|
{
|
|
69
69
|
"type": "command",
|
|
70
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/advisory/
|
|
70
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/advisory/migration_safety_advisor.py",
|
|
71
71
|
"timeout": 15
|
|
72
72
|
}
|
|
73
73
|
]
|
|
@@ -77,27 +77,27 @@
|
|
|
77
77
|
"hooks": [
|
|
78
78
|
{
|
|
79
79
|
"type": "command",
|
|
80
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/
|
|
80
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/destructive_command_blocker.py",
|
|
81
81
|
"timeout": 10
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
84
|
"type": "command",
|
|
85
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/
|
|
85
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/block_main_commit.py",
|
|
86
86
|
"timeout": 15
|
|
87
87
|
},
|
|
88
88
|
{
|
|
89
89
|
"type": "command",
|
|
90
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/
|
|
90
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/pr_description_enforcer.py",
|
|
91
91
|
"timeout": 10
|
|
92
92
|
},
|
|
93
93
|
{
|
|
94
94
|
"type": "command",
|
|
95
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/
|
|
95
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/test_preflight_check.py",
|
|
96
96
|
"timeout": 10
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
99
|
"type": "command",
|
|
100
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/
|
|
100
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/content_search_to_zoekt_redirector.py",
|
|
101
101
|
"timeout": 10
|
|
102
102
|
}
|
|
103
103
|
]
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
"hooks": [
|
|
108
108
|
{
|
|
109
109
|
"type": "command",
|
|
110
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/
|
|
110
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/parallel_task_blocker.py",
|
|
111
111
|
"timeout": 10
|
|
112
112
|
}
|
|
113
113
|
]
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
"hooks": [
|
|
118
118
|
{
|
|
119
119
|
"type": "command",
|
|
120
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/notification/
|
|
120
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/notification/attention_needed_notify.py",
|
|
121
121
|
"timeout": 15
|
|
122
122
|
}
|
|
123
123
|
]
|
|
@@ -129,12 +129,12 @@
|
|
|
129
129
|
"hooks": [
|
|
130
130
|
{
|
|
131
131
|
"type": "command",
|
|
132
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/
|
|
132
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/bulk_edit_reminder.py",
|
|
133
133
|
"timeout": 15
|
|
134
134
|
},
|
|
135
135
|
{
|
|
136
136
|
"type": "command",
|
|
137
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/
|
|
137
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/code_rules_reminder.py",
|
|
138
138
|
"timeout": 15
|
|
139
139
|
}
|
|
140
140
|
]
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
"hooks": [
|
|
147
147
|
{
|
|
148
148
|
"type": "command",
|
|
149
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/
|
|
149
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/plugin_data_dir_cleanup.py",
|
|
150
150
|
"timeout": 10
|
|
151
151
|
}
|
|
152
152
|
]
|
|
@@ -158,12 +158,12 @@
|
|
|
158
158
|
"hooks": [
|
|
159
159
|
{
|
|
160
160
|
"type": "command",
|
|
161
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/notification/
|
|
161
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/notification/attention_needed_notify.py",
|
|
162
162
|
"timeout": 15
|
|
163
163
|
},
|
|
164
164
|
{
|
|
165
165
|
"type": "command",
|
|
166
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/
|
|
166
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/hedging_language_blocker.py",
|
|
167
167
|
"timeout": 10
|
|
168
168
|
}
|
|
169
169
|
]
|
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
"hooks": [
|
|
176
176
|
{
|
|
177
177
|
"type": "command",
|
|
178
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/lifecycle/
|
|
178
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/lifecycle/session_end_cleanup.py",
|
|
179
179
|
"timeout": 15
|
|
180
180
|
}
|
|
181
181
|
]
|
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
"hooks": [
|
|
188
188
|
{
|
|
189
189
|
"type": "command",
|
|
190
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/lifecycle/
|
|
190
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/lifecycle/config_change_guard.py",
|
|
191
191
|
"timeout": 10
|
|
192
192
|
}
|
|
193
193
|
]
|
|
@@ -209,7 +209,7 @@
|
|
|
209
209
|
},
|
|
210
210
|
{
|
|
211
211
|
"type": "command",
|
|
212
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/workflow/
|
|
212
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/workflow/auto_formatter.py",
|
|
213
213
|
"timeout": 30
|
|
214
214
|
}
|
|
215
215
|
]
|
|
@@ -219,7 +219,7 @@
|
|
|
219
219
|
"hooks": [
|
|
220
220
|
{
|
|
221
221
|
"type": "command",
|
|
222
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/workflow/
|
|
222
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/workflow/investigation_tracker_reset.py",
|
|
223
223
|
"timeout": 10
|
|
224
224
|
}
|
|
225
225
|
]
|
|
@@ -231,7 +231,7 @@
|
|
|
231
231
|
"hooks": [
|
|
232
232
|
{
|
|
233
233
|
"type": "command",
|
|
234
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/notification/
|
|
234
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/notification/claude_notification_handler.py",
|
|
235
235
|
"timeout": 15
|
|
236
236
|
}
|
|
237
237
|
]
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
# pragma: no-tdd-gate
|
|
2
3
|
from datetime import datetime
|
|
3
4
|
import json
|
|
4
5
|
import os
|
|
5
6
|
import sys
|
|
6
7
|
|
|
7
8
|
AUDIT_LOG = os.path.expanduser("~/.claude/cache/config-change-audit.log")
|
|
8
|
-
|
|
9
|
+
# pragma: no-tdd-gate
|
|
10
|
+
DEFAULT_KNOWN_HOOK_COUNT_FILE = os.path.expanduser("~/.claude/cache/known-hook-count.txt")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_known_hook_count_file() -> str:
|
|
14
|
+
return os.environ.get("KNOWN_HOOK_COUNT_FILE", DEFAULT_KNOWN_HOOK_COUNT_FILE)
|
|
9
15
|
|
|
10
16
|
|
|
11
17
|
def count_hooks_in_settings(file_path: str) -> int:
|
|
@@ -32,36 +38,44 @@ def write_audit_entry(source: str, file_path: str) -> None:
|
|
|
32
38
|
pass
|
|
33
39
|
|
|
34
40
|
|
|
41
|
+
# pragma: no-tdd-gate
|
|
35
42
|
def guard_hook_injection(file_path: str) -> None:
|
|
36
43
|
current_count = count_hooks_in_settings(file_path)
|
|
44
|
+
known_hook_count_file = get_known_hook_count_file()
|
|
37
45
|
|
|
38
|
-
if not os.path.exists(
|
|
46
|
+
if not os.path.exists(known_hook_count_file):
|
|
39
47
|
try:
|
|
40
|
-
with open(
|
|
48
|
+
with open(known_hook_count_file, "w") as count_file:
|
|
41
49
|
count_file.write(str(current_count))
|
|
42
50
|
except OSError:
|
|
43
51
|
pass
|
|
44
52
|
return
|
|
45
53
|
|
|
46
54
|
try:
|
|
47
|
-
with open(
|
|
55
|
+
with open(known_hook_count_file) as count_file:
|
|
48
56
|
stored_count = int(count_file.read().strip())
|
|
49
57
|
except (OSError, ValueError):
|
|
50
58
|
stored_count = current_count
|
|
51
59
|
|
|
60
|
+
# pragma: no-tdd-gate
|
|
61
|
+
if current_count > stored_count:
|
|
62
|
+
block_reason = (
|
|
63
|
+
f"Hook count increased from {stored_count} to {current_count}. "
|
|
64
|
+
f"Review the added hook entries before proceeding. "
|
|
65
|
+
f"Delete known-hook-count.txt to reset."
|
|
66
|
+
)
|
|
67
|
+
block_payload = {
|
|
68
|
+
"decision": "block",
|
|
69
|
+
"reason": block_reason,
|
|
70
|
+
}
|
|
71
|
+
print(json.dumps(block_payload))
|
|
72
|
+
|
|
52
73
|
try:
|
|
53
|
-
with open(
|
|
74
|
+
with open(known_hook_count_file, "w") as count_file:
|
|
54
75
|
count_file.write(str(current_count))
|
|
55
76
|
except OSError:
|
|
56
77
|
pass
|
|
57
78
|
|
|
58
|
-
if current_count > stored_count:
|
|
59
|
-
block_decision = {
|
|
60
|
-
"decision": "block",
|
|
61
|
-
"reason": f"Hook count changed {stored_count} -> {current_count}. Delete known-hook-count.txt to reset.",
|
|
62
|
-
}
|
|
63
|
-
print(json.dumps(block_decision))
|
|
64
|
-
|
|
65
79
|
|
|
66
80
|
def main() -> None:
|
|
67
81
|
try:
|
|
@@ -5,7 +5,7 @@ import sys
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
HOOK_PATH = Path(__file__).parent / "
|
|
8
|
+
HOOK_PATH = Path(__file__).parent / "config_change_guard.py"
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def _run_hook(
|
|
@@ -52,8 +52,8 @@ def test_hook_count_increase_emits_user_visible_output(tmp_path: Path) -> None:
|
|
|
52
52
|
block_payload = json.loads(hook_run.stdout)
|
|
53
53
|
assert block_payload["decision"] == "block"
|
|
54
54
|
assert "2" in block_payload["reason"] and "5" in block_payload["reason"]
|
|
55
|
-
assert
|
|
56
|
-
assert "
|
|
55
|
+
assert "hook" in block_payload["reason"].lower()
|
|
56
|
+
assert "hookSpecificOutput" not in block_payload
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def test_hook_count_stable_produces_no_output(tmp_path: Path) -> None:
|
|
@@ -12,6 +12,7 @@ import sys
|
|
|
12
12
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
13
13
|
from notification_utils import (
|
|
14
14
|
notify_ntfy,
|
|
15
|
+
notify_discord,
|
|
15
16
|
is_wsl,
|
|
16
17
|
notify_windows,
|
|
17
18
|
notify_wsl,
|
|
@@ -23,6 +24,7 @@ from notification_utils import (
|
|
|
23
24
|
)
|
|
24
25
|
|
|
25
26
|
DEFAULT_MESSAGE = "Input needed"
|
|
27
|
+
ATTENTION_WEBHOOK_SECRET_ID = os.environ.get("BWS_DISCORD_ATTENTION_SECRET_ID", "")
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
def get_question_from_stdin() -> str:
|
|
@@ -46,6 +48,11 @@ def main() -> None:
|
|
|
46
48
|
question_text = get_question_from_stdin()
|
|
47
49
|
|
|
48
50
|
notify_ntfy(title=project_name, message=question_text)
|
|
51
|
+
notify_discord(
|
|
52
|
+
title=project_name,
|
|
53
|
+
message=question_text,
|
|
54
|
+
webhook_secret_id=ATTENTION_WEBHOOK_SECRET_ID,
|
|
55
|
+
)
|
|
49
56
|
|
|
50
57
|
if system == "Windows":
|
|
51
58
|
sound_windows()
|
package/hooks/notification/{claude-notification-handler.py → claude_notification_handler.py}
RENAMED
|
@@ -8,6 +8,7 @@ import sys
|
|
|
8
8
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
9
9
|
from notification_utils import (
|
|
10
10
|
notify_ntfy,
|
|
11
|
+
notify_discord,
|
|
11
12
|
is_wsl,
|
|
12
13
|
notify_windows,
|
|
13
14
|
notify_wsl,
|
|
@@ -16,6 +17,8 @@ from notification_utils import (
|
|
|
16
17
|
get_project_name,
|
|
17
18
|
)
|
|
18
19
|
|
|
20
|
+
ATTENTION_WEBHOOK_SECRET_ID = os.environ.get("BWS_DISCORD_ATTENTION_SECRET_ID", "")
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
def send_desktop_and_push_notification(
|
|
21
24
|
project_name: str,
|
|
@@ -23,6 +26,11 @@ def send_desktop_and_push_notification(
|
|
|
23
26
|
ntfy_priority: str,
|
|
24
27
|
) -> None:
|
|
25
28
|
notify_ntfy(title=project_name, message=notification_message, priority=ntfy_priority)
|
|
29
|
+
notify_discord(
|
|
30
|
+
title=project_name,
|
|
31
|
+
message=notification_message,
|
|
32
|
+
webhook_secret_id=ATTENTION_WEBHOOK_SECRET_ID,
|
|
33
|
+
)
|
|
26
34
|
system = platform.system()
|
|
27
35
|
if system == "Windows":
|
|
28
36
|
sound_windows()
|