claude-dev-env 1.25.1 → 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/{destructive-command-blocker.py → destructive_command_blocker.py} +12 -4
  7. package/hooks/blocking/{tdd-enforcer.py → tdd_enforcer.py} +12 -0
  8. package/hooks/blocking/test_code_rules_enforcer_any_type_ignore.py +2 -2
  9. package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +2 -2
  10. package/hooks/blocking/test_code_rules_enforcer_conftest_anchor.py +1 -1
  11. package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +2 -2
  12. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +181 -0
  13. package/hooks/blocking/test_code_rules_enforcer_fstring_scan.py +4 -4
  14. package/hooks/blocking/test_code_rules_enforcer_logger_fstring.py +1 -1
  15. package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
  16. package/hooks/blocking/test_code_rules_enforcer_magic_string_masking.py +104 -0
  17. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +2 -2
  18. package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +2 -2
  19. package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +1 -1
  20. package/hooks/blocking/test_destructive_command_blocker.py +63 -4
  21. package/hooks/blocking/test_gh_body_arg_blocker.py +1 -1
  22. package/hooks/blocking/test_pr_description_enforcer.py +8 -8
  23. package/hooks/blocking/test_tdd_enforcer.py +53 -1
  24. package/hooks/github-action/pre-push-review.yml +27 -0
  25. package/hooks/hooks.json +28 -28
  26. package/hooks/lifecycle/{config-change-guard.py → config_change_guard.py} +27 -12
  27. package/hooks/lifecycle/test_config_change_guard.py +3 -3
  28. package/hooks/notification/{attention-needed-notify.py → attention_needed_notify.py} +7 -0
  29. package/hooks/notification/{claude-notification-handler.py → claude_notification_handler.py} +8 -0
  30. package/hooks/notification/notification_utils.py +60 -2
  31. package/hooks/notification/subagent_complete_notify.py +381 -0
  32. package/hooks/notification/test_attention_needed_notify.py +47 -0
  33. package/hooks/notification/test_claude_notification_handler.py +54 -0
  34. package/hooks/notification/test_notification_utils.py +91 -0
  35. package/hooks/notification/test_subagent_complete_notify.py +72 -0
  36. package/hooks/validators/README.md +5 -1
  37. package/hooks/validators/abbreviation_checks.py +1 -1
  38. package/hooks/validators/code_quality_checks.py +1 -1
  39. package/hooks/validators/config.py +5 -0
  40. package/hooks/validators/conftest.py +10 -0
  41. package/hooks/validators/exempt_paths.py +1 -1
  42. package/hooks/validators/git_checks.py +80 -0
  43. package/hooks/validators/magic_value_checks.py +2 -2
  44. package/hooks/validators/pr_reference_checks.py +1 -1
  45. package/hooks/validators/python_antipattern_checks.py +1 -1
  46. package/hooks/validators/run_all_validators.py +53 -105
  47. package/hooks/validators/security_checks.py +1 -1
  48. package/hooks/validators/test_abbreviation_checks.py +2 -2
  49. package/hooks/validators/test_code_quality_checks.py +2 -2
  50. package/hooks/validators/test_file_structure_checks.py +1 -1
  51. package/hooks/validators/test_git_checks.py +79 -13
  52. package/hooks/validators/test_health_check.py +1 -1
  53. package/hooks/validators/test_magic_value_checks.py +2 -2
  54. package/hooks/validators/test_mypy_integration.py +1 -1
  55. package/hooks/validators/test_output_formatter.py +3 -1
  56. package/hooks/validators/test_pr_reference_checks.py +2 -2
  57. package/hooks/validators/test_python_antipattern_checks.py +2 -2
  58. package/hooks/validators/test_python_style_checks.py +2 -4
  59. package/hooks/validators/test_react_checks.py +1 -1
  60. package/hooks/validators/test_ruff_integration.py +1 -1
  61. package/hooks/validators/test_run_all_validators.py +75 -43
  62. package/hooks/validators/test_run_all_validators_integration.py +14 -37
  63. package/hooks/validators/test_security_checks.py +2 -2
  64. package/hooks/validators/test_test_safety_checks.py +1 -1
  65. package/hooks/validators/test_todo_checks.py +2 -2
  66. package/hooks/validators/test_type_safety_checks.py +2 -2
  67. package/hooks/validators/test_useless_test_checks.py +2 -2
  68. package/hooks/validators/test_validator_base.py +1 -1
  69. package/hooks/validators/test_verify_paths.py +2 -4
  70. package/hooks/validators/todo_checks.py +1 -1
  71. package/hooks/validators/type_safety_checks.py +1 -1
  72. package/hooks/validators/useless_test_checks.py +1 -1
  73. package/package.json +1 -1
  74. package/rules/file-global-constants.md +71 -0
  75. package/rules/gh-body-file.md +1 -1
  76. package/rules/prompt-workflow-context-controls.md +48 -0
  77. package/scripts/sync_to_cursor/rules.py +2 -2
  78. package/scripts/tests/test_sync_to_cursor.py +2 -2
  79. package/skills/bugteam/CONSTRAINTS.md +37 -0
  80. package/skills/bugteam/EXAMPLES.md +64 -0
  81. package/skills/bugteam/PROMPTS.md +175 -0
  82. package/skills/bugteam/SKILL.md +204 -295
  83. package/skills/bugteam/SKILL_EVALS.md +346 -0
  84. package/skills/bugteam/scripts/README.md +37 -0
  85. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +334 -0
  86. package/skills/bugteam/scripts/bugteam_preflight.py +135 -0
  87. package/skills/rule-audit/SKILL.md +4 -4
  88. /package/hooks/advisory/{migration-safety-advisor.py → migration_safety_advisor.py} +0 -0
  89. /package/hooks/advisory/{refactor-guard.py → refactor_guard.py} +0 -0
  90. /package/hooks/blocking/{block-main-commit.py → block_main_commit.py} +0 -0
  91. /package/hooks/blocking/{content-search-to-zoekt-redirector.py → content_search_to_zoekt_redirector.py} +0 -0
  92. /package/hooks/blocking/{gh-body-arg-blocker.py → gh_body_arg_blocker.py} +0 -0
  93. /package/hooks/blocking/{hedging-language-blocker.py → hedging_language_blocker.py} +0 -0
  94. /package/hooks/blocking/{pr-description-enforcer.py → pr_description_enforcer.py} +0 -0
  95. /package/hooks/blocking/{sensitive-file-protector.py → sensitive_file_protector.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"
@@ -1,22 +1,34 @@
1
1
  """Tests for destructive-command-blocker hook."""
2
2
 
3
3
  import json
4
+ import os
4
5
  import subprocess
5
6
  import sys
6
7
  from pathlib import Path
7
8
 
8
9
 
9
- SCRIPT_PATH = Path(__file__).parent / "destructive-command-blocker.py"
10
+ SCRIPT_PATH = Path(__file__).parent / "destructive_command_blocker.py"
10
11
  GH_GATE_USER_FACING_PREFIX = "[gh-gate]"
11
-
12
-
13
- def _run_hook(payload: dict) -> subprocess.CompletedProcess[str]:
12
+ GH_REDIRECT_ACTIVE_ENV_VAR = "CLAUDE_GH_REDIRECT_ACTIVE"
13
+ GH_REDIRECT_ACTIVE_VALUE = "1"
14
+
15
+
16
+ def _run_hook(
17
+ payload: dict,
18
+ gh_redirect_active: bool = True,
19
+ ) -> subprocess.CompletedProcess[str]:
20
+ child_environment = os.environ.copy()
21
+ if gh_redirect_active:
22
+ child_environment[GH_REDIRECT_ACTIVE_ENV_VAR] = GH_REDIRECT_ACTIVE_VALUE
23
+ else:
24
+ child_environment.pop(GH_REDIRECT_ACTIVE_ENV_VAR, None)
14
25
  return subprocess.run(
15
26
  [sys.executable, str(SCRIPT_PATH)],
16
27
  input=json.dumps(payload),
17
28
  text=True,
18
29
  capture_output=True,
19
30
  check=False,
31
+ env=child_environment,
20
32
  )
21
33
 
22
34
 
@@ -106,3 +118,50 @@ def test_ignores_non_bash_tool() -> None:
106
118
  result = _run_hook(payload)
107
119
  assert result.stdout.strip() == ""
108
120
  assert result.returncode == 0
121
+
122
+
123
+ def test_gh_issue_comment_is_allowed_when_redirect_env_var_is_unset() -> None:
124
+ payload = _make_bash_payload('gh issue comment 83 --body "hello"')
125
+
126
+ result = _run_hook(payload, gh_redirect_active=False)
127
+
128
+ assert result.stdout.strip() == ""
129
+ assert result.returncode == 0
130
+
131
+
132
+ def test_gh_pr_comment_is_allowed_when_redirect_env_var_is_unset() -> None:
133
+ payload = _make_bash_payload('gh pr comment 42 --body "ok"')
134
+
135
+ result = _run_hook(payload, gh_redirect_active=False)
136
+
137
+ assert result.stdout.strip() == ""
138
+ assert result.returncode == 0
139
+
140
+
141
+ def test_gh_pr_review_is_allowed_when_redirect_env_var_is_unset() -> None:
142
+ payload = _make_bash_payload("gh pr review 42 --approve")
143
+
144
+ result = _run_hook(payload, gh_redirect_active=False)
145
+
146
+ assert result.stdout.strip() == ""
147
+ assert result.returncode == 0
148
+
149
+
150
+ def test_gh_api_post_comment_is_allowed_when_redirect_env_var_is_unset() -> None:
151
+ payload = _make_bash_payload(
152
+ "gh api /repos/owner/name/issues/1/comments -X POST -f body=hello"
153
+ )
154
+
155
+ result = _run_hook(payload, gh_redirect_active=False)
156
+
157
+ assert result.stdout.strip() == ""
158
+ assert result.returncode == 0
159
+
160
+
161
+ def test_rm_rf_still_asks_when_redirect_env_var_is_unset() -> None:
162
+ payload = _make_bash_payload("rm -rf /tmp/somewhere")
163
+
164
+ result = _run_hook(payload, gh_redirect_active=False)
165
+
166
+ response = json.loads(result.stdout)
167
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
@@ -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():
@@ -247,3 +247,55 @@ def test_directory_skip_components_excludes_pluralized_conftest() -> None:
247
247
  actual_directory_skip_components = _PRODUCTION_MODULE._directory_skip_components()
248
248
 
249
249
  assert "conftests" not in actual_directory_skip_components
250
+
251
+
252
+ def test_should_skip_silently_when_posix_path_has_dotclaude_segment(tmp_path: Path) -> None:
253
+ dotclaude_production_file = tmp_path / ".claude" / "agents" / "reviewer.py"
254
+
255
+ completed = _run_hook_with_payload(
256
+ _make_write_payload(dotclaude_production_file, "def review(): pass\n")
257
+ )
258
+
259
+ assert completed.returncode == 0
260
+ assert completed.stdout.strip() == ""
261
+
262
+
263
+ def test_should_skip_silently_when_windows_backslash_path_has_dotclaude_segment() -> None:
264
+ windows_style_path = "C:\\Users\\dev\\.claude\\agents\\reviewer.py"
265
+
266
+ completed = _run_hook_with_payload({
267
+ "tool_name": "Write",
268
+ "tool_input": {"file_path": windows_style_path, "content": "def review(): pass\n"},
269
+ })
270
+
271
+ assert completed.returncode == 0
272
+ assert completed.stdout.strip() == ""
273
+
274
+
275
+ def test_should_skip_silently_when_mixed_separator_path_has_dotclaude_segment() -> None:
276
+ mixed_separator_path = "C:/Users/dev\\.claude/agents\\reviewer.py"
277
+
278
+ completed = _run_hook_with_payload({
279
+ "tool_name": "Write",
280
+ "tool_input": {"file_path": mixed_separator_path, "content": "def review(): pass\n"},
281
+ })
282
+
283
+ assert completed.returncode == 0
284
+ assert completed.stdout.strip() == ""
285
+
286
+
287
+ def test_should_not_skip_when_dotclaude_is_only_a_filename_substring(tmp_path: Path) -> None:
288
+ sandbox = _sandbox(tmp_path)
289
+ substring_production_file = sandbox / "my.claude.helpers.py"
290
+ substring_production_file.write_text("def help(): pass\n")
291
+
292
+ completed = _run_hook_with_payload(_make_write_payload(substring_production_file))
293
+
294
+ assert _decision_from(completed) == "deny"
295
+
296
+
297
+ def test_is_inside_dotclaude_segment_helper_matches_only_exact_segments() -> None:
298
+ assert _PRODUCTION_MODULE._is_inside_dotclaude_segment("/home/user/.claude/agent.py") is True
299
+ assert _PRODUCTION_MODULE._is_inside_dotclaude_segment("C:\\Users\\dev\\.claude\\agent.py") is True
300
+ assert _PRODUCTION_MODULE._is_inside_dotclaude_segment("/src/my.claude.helpers.py") is False
301
+ assert _PRODUCTION_MODULE._is_inside_dotclaude_segment("/src/app/service.py") is False
@@ -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
  ]