claude-dev-env 1.25.2 → 1.26.0

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.
Files changed (105) hide show
  1. package/CLAUDE.md +6 -0
  2. package/agents/clean-coder.md +1 -1
  3. package/docs/CODE_RULES.md +3 -1
  4. package/hooks/HOOK_SPECS_PROMPT_WORKFLOW.md +54 -0
  5. package/hooks/blocking/{code-rules-enforcer.py → code_rules_enforcer.py} +150 -5
  6. package/hooks/blocking/test_code_rules_enforcer_any_type_ignore.py +2 -2
  7. package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +2 -2
  8. package/hooks/blocking/test_code_rules_enforcer_conftest_anchor.py +1 -1
  9. package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +2 -2
  10. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +181 -0
  11. package/hooks/blocking/test_code_rules_enforcer_fstring_scan.py +4 -4
  12. package/hooks/blocking/test_code_rules_enforcer_logger_fstring.py +1 -1
  13. package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
  14. package/hooks/blocking/test_code_rules_enforcer_magic_string_masking.py +104 -0
  15. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +2 -2
  16. package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +2 -2
  17. package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +1 -1
  18. package/hooks/blocking/test_destructive_command_blocker.py +1 -1
  19. package/hooks/blocking/test_gh_body_arg_blocker.py +1 -1
  20. package/hooks/blocking/test_pr_description_enforcer.py +8 -8
  21. package/hooks/blocking/test_tdd_enforcer.py +1 -1
  22. package/hooks/github-action/pre-push-review.yml +27 -0
  23. package/hooks/hooks.json +28 -28
  24. package/hooks/lifecycle/{config-change-guard.py → config_change_guard.py} +27 -12
  25. package/hooks/lifecycle/test_config_change_guard.py +3 -3
  26. package/hooks/notification/{attention-needed-notify.py → attention_needed_notify.py} +7 -0
  27. package/hooks/notification/{claude-notification-handler.py → claude_notification_handler.py} +8 -0
  28. package/hooks/notification/notification_utils.py +56 -0
  29. package/hooks/notification/subagent_complete_notify.py +381 -0
  30. package/hooks/notification/test_attention_needed_notify.py +47 -0
  31. package/hooks/notification/test_claude_notification_handler.py +54 -0
  32. package/hooks/notification/test_notification_utils.py +45 -0
  33. package/hooks/notification/test_subagent_complete_notify.py +72 -0
  34. package/hooks/validators/README.md +5 -1
  35. package/hooks/validators/abbreviation_checks.py +1 -1
  36. package/hooks/validators/code_quality_checks.py +1 -1
  37. package/hooks/validators/config.py +5 -0
  38. package/hooks/validators/conftest.py +10 -0
  39. package/hooks/validators/exempt_paths.py +1 -1
  40. package/hooks/validators/git_checks.py +80 -0
  41. package/hooks/validators/magic_value_checks.py +2 -2
  42. package/hooks/validators/pr_reference_checks.py +1 -1
  43. package/hooks/validators/python_antipattern_checks.py +1 -1
  44. package/hooks/validators/run_all_validators.py +53 -105
  45. package/hooks/validators/security_checks.py +1 -1
  46. package/hooks/validators/test_abbreviation_checks.py +2 -2
  47. package/hooks/validators/test_code_quality_checks.py +2 -2
  48. package/hooks/validators/test_file_structure_checks.py +1 -1
  49. package/hooks/validators/test_git_checks.py +79 -13
  50. package/hooks/validators/test_health_check.py +1 -1
  51. package/hooks/validators/test_magic_value_checks.py +2 -2
  52. package/hooks/validators/test_mypy_integration.py +1 -1
  53. package/hooks/validators/test_output_formatter.py +3 -1
  54. package/hooks/validators/test_pr_reference_checks.py +2 -2
  55. package/hooks/validators/test_python_antipattern_checks.py +2 -2
  56. package/hooks/validators/test_python_style_checks.py +2 -4
  57. package/hooks/validators/test_react_checks.py +1 -1
  58. package/hooks/validators/test_ruff_integration.py +1 -1
  59. package/hooks/validators/test_run_all_validators.py +75 -43
  60. package/hooks/validators/test_run_all_validators_integration.py +14 -37
  61. package/hooks/validators/test_security_checks.py +2 -2
  62. package/hooks/validators/test_test_safety_checks.py +1 -1
  63. package/hooks/validators/test_todo_checks.py +2 -2
  64. package/hooks/validators/test_type_safety_checks.py +2 -2
  65. package/hooks/validators/test_useless_test_checks.py +2 -2
  66. package/hooks/validators/test_validator_base.py +1 -1
  67. package/hooks/validators/test_verify_paths.py +2 -4
  68. package/hooks/validators/todo_checks.py +1 -1
  69. package/hooks/validators/type_safety_checks.py +1 -1
  70. package/hooks/validators/useless_test_checks.py +1 -1
  71. package/package.json +1 -1
  72. package/rules/file-global-constants.md +71 -0
  73. package/rules/gh-body-file.md +1 -1
  74. package/rules/prompt-workflow-context-controls.md +48 -0
  75. package/scripts/sync_to_cursor/rules.py +2 -2
  76. package/scripts/tests/test_sync_to_cursor.py +2 -2
  77. package/skills/bugteam/CONSTRAINTS.md +37 -0
  78. package/skills/bugteam/EXAMPLES.md +64 -0
  79. package/skills/bugteam/PROMPTS.md +175 -0
  80. package/skills/bugteam/SKILL.md +204 -295
  81. package/skills/bugteam/SKILL_EVALS.md +346 -0
  82. package/skills/bugteam/scripts/README.md +37 -0
  83. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +334 -0
  84. package/skills/bugteam/scripts/bugteam_preflight.py +135 -0
  85. package/skills/rule-audit/SKILL.md +4 -4
  86. /package/hooks/advisory/{migration-safety-advisor.py → migration_safety_advisor.py} +0 -0
  87. /package/hooks/advisory/{refactor-guard.py → refactor_guard.py} +0 -0
  88. /package/hooks/blocking/{block-main-commit.py → block_main_commit.py} +0 -0
  89. /package/hooks/blocking/{content-search-to-zoekt-redirector.py → content_search_to_zoekt_redirector.py} +0 -0
  90. /package/hooks/blocking/{destructive-command-blocker.py → destructive_command_blocker.py} +0 -0
  91. /package/hooks/blocking/{gh-body-arg-blocker.py → gh_body_arg_blocker.py} +0 -0
  92. /package/hooks/blocking/{hedging-language-blocker.py → hedging_language_blocker.py} +0 -0
  93. /package/hooks/blocking/{pr-description-enforcer.py → pr_description_enforcer.py} +0 -0
  94. /package/hooks/blocking/{sensitive-file-protector.py → sensitive_file_protector.py} +0 -0
  95. /package/hooks/blocking/{tdd-enforcer.py → tdd_enforcer.py} +0 -0
  96. /package/hooks/blocking/{test-preflight-check.py → test_preflight_check.py} +0 -0
  97. /package/hooks/blocking/{write-existing-file-blocker.py → write_existing_file_blocker.py} +0 -0
  98. /package/hooks/git-hooks/{post-commit.py → post_commit.py} +0 -0
  99. /package/hooks/lifecycle/{session-end-cleanup.py → session_end_cleanup.py} +0 -0
  100. /package/hooks/{rewrite-plugin-paths.py → rewrite_plugin_paths.py} +0 -0
  101. /package/hooks/session/{plugin-data-dir-cleanup.py → plugin_data_dir_cleanup.py} +0 -0
  102. /package/hooks/validation/{hook-format-validator.py → hook_format_validator.py} +0 -0
  103. /package/hooks/workflow/{auto-formatter.py → auto_formatter.py} +0 -0
  104. /package/hooks/workflow/{investigation-tracker-reset.py → investigation_tracker_reset.py} +0 -0
  105. /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 code-rules-enforcer boolean naming-pattern check."""
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 / "code-rules-enforcer.py",
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 code-rules-enforcer."""
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 / "code-rules-enforcer.py",
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 / "content-search-to-zoekt-redirector.py"
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 / "destructive-command-blocker.py"
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 / "gh-body-arg-blocker.py",
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 / "pr-description-enforcer.py",
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 / 'pr-description-enforcer.py')
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 / 'pr-description-enforcer.py')
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 / 'pr-description-enforcer.py')
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 / 'pr-description-enforcer.py')
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 / 'pr-description-enforcer.py')
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 / 'pr-description-enforcer.py')
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 / 'pr-description-enforcer.py')
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"
@@ -9,7 +9,7 @@ import time
9
9
  from pathlib import Path
10
10
 
11
11
 
12
- SCRIPT_PATH = Path(__file__).parent / "tdd-enforcer.py"
12
+ SCRIPT_PATH = Path(__file__).parent / "tdd_enforcer.py"
13
13
 
14
14
 
15
15
  def _load_production_module():
@@ -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/content-search-to-zoekt-redirector.py",
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/write-existing-file-blocker.py",
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/sensitive-file-protector.py",
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/pyautogui-scroll-blocker.py",
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/hook-format-validator.py",
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/validators/run_all_validators.py",
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/code-rules-enforcer.py",
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/tdd-enforcer.py",
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/code-style-validator.py",
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/refactor-guard.py",
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/migration-safety-advisor.py",
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/destructive-command-blocker.py",
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/block-main-commit.py",
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/pr-description-enforcer.py",
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/test-preflight-check.py",
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/content-search-to-zoekt-redirector.py",
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/parallel-task-blocker.py",
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/attention-needed-notify.py",
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/bulk-edit-reminder.py",
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/code-rules-reminder.py",
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/plugin-data-dir-cleanup.py",
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/attention-needed-notify.py",
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/hedging-language-blocker.py",
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/session-end-cleanup.py",
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/config-change-guard.py",
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/auto-formatter.py",
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/investigation-tracker-reset.py",
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/claude-notification-handler.py",
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
- KNOWN_HOOK_COUNT_FILE = os.path.expanduser("~/.claude/cache/known-hook-count.txt")
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,45 @@ 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(KNOWN_HOOK_COUNT_FILE):
46
+ if not os.path.exists(known_hook_count_file):
39
47
  try:
40
- with open(KNOWN_HOOK_COUNT_FILE, "w") as count_file:
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(KNOWN_HOOK_COUNT_FILE) as count_file:
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
+ return
73
+
52
74
  try:
53
- with open(KNOWN_HOOK_COUNT_FILE, "w") as count_file:
75
+ with open(known_hook_count_file, "w") as count_file:
54
76
  count_file.write(str(current_count))
55
77
  except OSError:
56
78
  pass
57
79
 
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
80
 
66
81
  def main() -> None:
67
82
  try:
@@ -5,7 +5,7 @@ import sys
5
5
  from pathlib import Path
6
6
 
7
7
 
8
- HOOK_PATH = Path(__file__).parent / "config-change-guard.py"
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 block_payload["hookSpecificOutput"]["hookEventName"] == "ConfigChange"
56
- assert "hook" in block_payload["hookSpecificOutput"]["additionalContext"].lower()
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()
@@ -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()