claude-dev-env 1.29.3 → 1.30.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.
Files changed (43) hide show
  1. package/CLAUDE.md +8 -0
  2. package/agents/code-quality-agent.md +279 -24
  3. package/agents/groq-coder.md +111 -0
  4. package/commands/plan.md +4 -5
  5. package/docs/CODE_RULES.md +40 -0
  6. package/hooks/blocking/code_rules_enforcer.py +775 -8
  7. package/hooks/blocking/destructive_command_blocker.py +149 -12
  8. package/hooks/blocking/test_code_rules_enforcer.py +751 -0
  9. package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +130 -0
  10. package/hooks/blocking/test_code_rules_enforcer_existence_checks.py +134 -0
  11. package/hooks/blocking/test_code_rules_enforcer_skip_decorators.py +150 -0
  12. package/hooks/blocking/test_destructive_command_blocker.py +281 -4
  13. package/hooks/git-hooks/test_config.py +9 -3
  14. package/hooks/git-hooks/test_gate_utils.py +9 -3
  15. package/hooks/git-hooks/test_pre_commit.py +9 -3
  16. package/hooks/git-hooks/test_pre_push.py +9 -3
  17. package/hooks/validators/run_all_validators.py +76 -3
  18. package/hooks/validators/test_output_formatter.py +4 -16
  19. package/hooks/validators/test_run_all_validators.py +22 -0
  20. package/hooks/validators/test_run_all_validators_integration.py +2 -11
  21. package/package.json +1 -1
  22. package/scripts/config/groq_bugteam_config.py +104 -0
  23. package/scripts/config/test_groq_bugteam_config.py +11 -0
  24. package/scripts/config/test_spec_implementer_prompt.py +36 -0
  25. package/scripts/groq_bugteam.README.md +2 -0
  26. package/scripts/groq_bugteam.py +74 -15
  27. package/scripts/groq_bugteam_dotenv.py +40 -0
  28. package/scripts/groq_bugteam_spec.py +226 -0
  29. package/scripts/test_groq_bugteam.py +143 -5
  30. package/scripts/test_groq_bugteam_apply_fix_from_spec.py +426 -0
  31. package/scripts/test_groq_bugteam_dotenv.py +66 -0
  32. package/scripts/test_groq_bugteam_spec.py +346 -0
  33. package/skills/bugteam/SKILL.md +4 -0
  34. package/skills/bugteam/reference/README.md +16 -0
  35. package/skills/bugteam/test_skill_additions.py +30 -0
  36. package/skills/monitor-open-prs/SKILL.md +104 -0
  37. package/skills/monitor-open-prs/scripts/discover_open_prs.py +69 -0
  38. package/skills/monitor-open-prs/scripts/test_discover_open_prs.py +149 -0
  39. package/skills/monitor-open-prs/test_skill_contract.py +43 -0
  40. package/skills/pr-review-responder/SKILL.md +10 -8
  41. package/hooks/github-action/pre-push-review.yml +0 -27
  42. package/hooks/github-action/test_workflow.py +0 -33
  43. package/skills/pr-review-responder/update_skill.py +0 -297
@@ -90,7 +90,7 @@ def test_suppresses_output_on_gh_redirect_deny() -> None:
90
90
 
91
91
 
92
92
  def test_asks_on_rm_rf_still_works() -> None:
93
- payload = _make_bash_payload("rm -rf /tmp/somewhere")
93
+ payload = _make_bash_payload("rm -rf /var/log/myapp")
94
94
  result = _run_hook(payload)
95
95
  response = json.loads(result.stdout)
96
96
  assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
@@ -159,10 +159,287 @@ def test_gh_api_post_comment_is_allowed_when_redirect_env_var_is_unset() -> None
159
159
  assert result.returncode == 0
160
160
 
161
161
 
162
- def test_rm_rf_still_asks_when_redirect_env_var_is_unset() -> None:
163
- payload = _make_bash_payload("rm -rf /tmp/somewhere")
162
+ def _run_rm_hook(payload: dict) -> subprocess.CompletedProcess[str]:
163
+ child_environment = os.environ.copy()
164
+ child_environment.pop(GH_REDIRECT_ACTIVE_ENV_VAR, None)
165
+ child_environment.pop("CLAUDE_DESTRUCTIVE_DISABLE_EPHEMERAL_AUTO_ALLOW", None)
166
+ return subprocess.run(
167
+ [sys.executable, str(SCRIPT_PATH)],
168
+ input=json.dumps(payload),
169
+ text=True,
170
+ capture_output=True,
171
+ check=False,
172
+ env=child_environment,
173
+ )
164
174
 
165
- result = _run_hook(payload, gh_redirect_active=False)
175
+
176
+ def test_rm_rf_asks_when_target_is_non_ephemeral_path() -> None:
177
+ payload = _make_bash_payload("rm -rf /var/log/myapp")
178
+
179
+ result = _run_rm_hook(payload)
180
+
181
+ response = json.loads(result.stdout)
182
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
183
+
184
+
185
+ def test_rm_rf_allowed_when_target_is_under_tmp_segment() -> None:
186
+ payload = _make_bash_payload("rm -rf /tmp/some_scratch_dir")
187
+
188
+ result = _run_rm_hook(payload)
189
+
190
+ assert result.stdout.strip() == ""
191
+ assert result.returncode == 0
192
+
193
+
194
+ def test_rm_rf_allowed_when_target_is_under_os_temp_directory() -> None:
195
+ system_temp_subdirectory = Path(tempfile.mkdtemp(prefix="rm_target_"))
196
+ forward_slash_temp_path = str(system_temp_subdirectory).replace("\\", "/")
197
+ payload = _make_bash_payload(f"rm -rf {forward_slash_temp_path}")
198
+
199
+ result = _run_rm_hook(payload)
200
+
201
+ assert result.stdout.strip() == ""
202
+ assert result.returncode == 0
203
+
204
+
205
+ def test_rm_rf_allowed_when_target_is_under_worktrees_segment() -> None:
206
+ payload = _make_bash_payload("rm -rf /Users/me/repo/worktrees/feature_branch/build")
207
+
208
+ result = _run_rm_hook(payload)
209
+
210
+ assert result.stdout.strip() == ""
211
+ assert result.returncode == 0
212
+
213
+
214
+ def test_rm_rf_asks_when_target_is_bare_worktrees_directory() -> None:
215
+ payload = _make_bash_payload("rm -rf /Users/me/repo/worktrees")
216
+
217
+ result = _run_rm_hook(payload)
218
+
219
+ response = json.loads(result.stdout)
220
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
221
+
222
+
223
+ def test_rm_rf_asks_when_rm_includes_option_with_equals_sign() -> None:
224
+ payload = _make_bash_payload("rm -rf --files0-from=/tmp/list /tmp/scratch")
225
+
226
+ result = _run_rm_hook(payload)
227
+
228
+ response = json.loads(result.stdout)
229
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
230
+
231
+
232
+ def test_rm_rf_allowed_when_both_targets_are_ephemeral() -> None:
233
+ payload = _make_bash_payload("rm -rf /tmp/first_dir /tmp/second_dir")
234
+
235
+ result = _run_rm_hook(payload)
236
+
237
+ assert result.stdout.strip() == ""
238
+ assert result.returncode == 0
239
+
240
+
241
+ def test_rm_rf_asks_when_any_target_is_non_ephemeral() -> None:
242
+ payload = _make_bash_payload("rm -rf /tmp/scratch /etc/passwd")
243
+
244
+ result = _run_rm_hook(payload)
245
+
246
+ response = json.loads(result.stdout)
247
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
248
+
249
+
250
+ def test_rm_rf_asks_when_double_dash_includes_hyphen_prefixed_non_ephemeral_target() -> None:
251
+ payload = _make_bash_payload("rm -rf -- /tmp/scratch -non_ephemeral")
252
+
253
+ result = _run_rm_hook(payload)
254
+
255
+ response = json.loads(result.stdout)
256
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
257
+
258
+
259
+ def test_rm_rf_asks_when_command_is_compound_with_ampersand() -> None:
260
+ payload = _make_bash_payload("rm -rf /tmp/reply && gh pr checks 19")
261
+
262
+ result = _run_rm_hook(payload)
263
+
264
+ response = json.loads(result.stdout)
265
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
266
+
267
+
268
+ def test_rm_rf_asks_when_target_is_bare_tmp_root() -> None:
269
+ payload = _make_bash_payload("rm -rf /tmp")
270
+
271
+ result = _run_rm_hook(payload)
272
+
273
+ response = json.loads(result.stdout)
274
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
275
+
276
+
277
+ def test_rm_rf_asks_when_target_is_double_slash_tmp_root() -> None:
278
+ payload = _make_bash_payload("rm -rf //tmp")
279
+
280
+ result = _run_rm_hook(payload)
281
+
282
+ response = json.loads(result.stdout)
283
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
284
+
285
+
286
+ def test_rm_rf_asks_when_target_is_bare_os_temp_root() -> None:
287
+ payload = _make_bash_payload(f"rm -rf {tempfile.gettempdir()}")
288
+
289
+ result = _run_rm_hook(payload)
290
+
291
+ response = json.loads(result.stdout)
292
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
293
+
294
+
295
+ def test_rm_rf_asks_when_ephemeral_auto_allow_disabled_via_env_var() -> None:
296
+ payload = _make_bash_payload("rm -rf /tmp/scratch")
297
+
298
+ child_environment = os.environ.copy()
299
+ child_environment.pop(GH_REDIRECT_ACTIVE_ENV_VAR, None)
300
+ child_environment["CLAUDE_DESTRUCTIVE_DISABLE_EPHEMERAL_AUTO_ALLOW"] = "1"
301
+ result = subprocess.run(
302
+ [sys.executable, str(SCRIPT_PATH)],
303
+ input=json.dumps(payload),
304
+ text=True,
305
+ capture_output=True,
306
+ check=False,
307
+ env=child_environment,
308
+ )
309
+
310
+ response = json.loads(result.stdout)
311
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
312
+
313
+
314
+ def test_rm_recursive_force_long_flags_allowed_under_tmp() -> None:
315
+ payload = _make_bash_payload("rm --recursive --force /tmp/long_flag_scratch")
316
+
317
+ result = _run_rm_hook(payload)
318
+
319
+ assert result.stdout.strip() == ""
320
+ assert result.returncode == 0
321
+
322
+
323
+ def test_rm_rf_asks_when_quoted_dotdot_traverses_out_of_ephemeral_root() -> None:
324
+ payload = _make_bash_payload('rm -rf /tmp/".."/etc')
325
+
326
+ result = _run_rm_hook(payload)
327
+
328
+ response = json.loads(result.stdout)
329
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
330
+
331
+
332
+ def test_rm_rf_allowed_when_quoted_path_is_legitimate_ephemeral() -> None:
333
+ payload = _make_bash_payload('rm -rf "/tmp/some scratch dir"')
334
+
335
+ result = _run_rm_hook(payload)
336
+
337
+ assert result.stdout.strip() == ""
338
+ assert result.returncode == 0
339
+
340
+
341
+ def test_rm_rf_asks_when_single_quoted_dotdot_traverses_out_of_ephemeral() -> None:
342
+ payload = _make_bash_payload("rm -rf /tmp/'..'/etc")
343
+
344
+ result = _run_rm_hook(payload)
345
+
346
+ response = json.loads(result.stdout)
347
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
348
+
349
+
350
+ def test_rm_rf_asks_when_target_is_glob_wildcard_under_tmp() -> None:
351
+ payload = _make_bash_payload("rm -rf /tmp/*")
352
+
353
+ result = _run_rm_hook(payload)
354
+
355
+ response = json.loads(result.stdout)
356
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
357
+
358
+
359
+ def test_rm_rf_asks_when_target_is_question_mark_glob_under_tmp() -> None:
360
+ payload = _make_bash_payload("rm -rf /tmp/?")
361
+
362
+ result = _run_rm_hook(payload)
363
+
364
+ response = json.loads(result.stdout)
365
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
366
+
367
+
368
+ def test_rm_rf_asks_when_target_is_bracket_glob_under_tmp() -> None:
369
+ payload = _make_bash_payload("rm -rf /tmp/[abc]")
370
+
371
+ result = _run_rm_hook(payload)
372
+
373
+ response = json.loads(result.stdout)
374
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
375
+
376
+
377
+ def test_rm_rf_asks_when_target_is_worktrees_glob() -> None:
378
+ payload = _make_bash_payload("rm -rf /worktrees/*")
379
+
380
+ result = _run_rm_hook(payload)
381
+
382
+ response = json.loads(result.stdout)
383
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
384
+
385
+
386
+ def test_rm_rf_asks_when_target_is_os_temp_root_glob() -> None:
387
+ system_temporary_root = tempfile.gettempdir()
388
+ forward_slash_temp_root = system_temporary_root.replace("\\", "/")
389
+ payload = _make_bash_payload(f"rm -rf {forward_slash_temp_root}/*")
390
+
391
+ result = _run_rm_hook(payload)
392
+
393
+ response = json.loads(result.stdout)
394
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
395
+
396
+
397
+ def test_rm_rf_allowed_when_unquoted_windows_backslash_target_is_ephemeral() -> None:
398
+ system_temporary_root = tempfile.gettempdir()
399
+ windows_style_target = system_temporary_root.replace("/", "\\") + "\\scratch"
400
+ payload = _make_bash_payload(f"rm -rf {windows_style_target}")
401
+
402
+ result = _run_rm_hook(payload)
403
+
404
+ assert result.stdout.strip() == ""
405
+ assert result.returncode == 0
406
+
407
+
408
+ def test_rm_rf_allowed_when_unquoted_windows_backslash_target_contains_worktrees_segment() -> None:
409
+ payload = _make_bash_payload(
410
+ r"rm -rf C:\Users\developer\project\worktrees\feature_branch\build"
411
+ )
412
+
413
+ result = _run_rm_hook(payload)
414
+
415
+ assert result.stdout.strip() == ""
416
+ assert result.returncode == 0
417
+
418
+
419
+ def test_rm_rf_allowed_when_finding_example_windows_backslash_ephemeral_target() -> None:
420
+ payload = _make_bash_payload(
421
+ r"rm -rf C:\Users\jon\AppData\Local\Temp\scratch"
422
+ )
423
+
424
+ result = _run_rm_hook(payload)
425
+
426
+ assert result.stdout.strip() == ""
427
+ assert result.returncode == 0
428
+
429
+
430
+ def test_rm_rf_asks_when_target_is_literal_tmp_star_finding_example() -> None:
431
+ payload = _make_bash_payload("rm -rf /tmp/*")
432
+
433
+ result = _run_rm_hook(payload)
434
+
435
+ response = json.loads(result.stdout)
436
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
437
+
438
+
439
+ def test_rm_rf_asks_when_target_basename_is_wildcard_with_prefix_under_tmp() -> None:
440
+ payload = _make_bash_payload("rm -rf /tmp/foo*")
441
+
442
+ result = _run_rm_hook(payload)
166
443
 
167
444
  response = json.loads(result.stdout)
168
445
  assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
@@ -1,13 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import importlib
3
4
  import sys
4
5
  from pathlib import Path
5
6
 
6
7
 
7
8
  SCRIPT_DIRECTORY = Path(__file__).resolve().parent
8
- if str(SCRIPT_DIRECTORY) not in sys.path:
9
- sys.path.insert(0, str(SCRIPT_DIRECTORY))
10
- sys.modules.pop("config", None)
9
+ _git_hooks_directory_string = str(SCRIPT_DIRECTORY)
10
+ while _git_hooks_directory_string in sys.path:
11
+ sys.path.remove(_git_hooks_directory_string)
12
+ sys.path.insert(0, _git_hooks_directory_string)
13
+ for each_module_name in list(sys.modules):
14
+ if each_module_name == "config" or each_module_name.startswith("config."):
15
+ del sys.modules[each_module_name]
16
+ importlib.invalidate_caches()
11
17
 
12
18
  import config
13
19
 
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import importlib
3
4
  import sys
4
5
  from pathlib import Path
5
6
 
@@ -7,9 +8,14 @@ import pytest
7
8
 
8
9
 
9
10
  SCRIPT_DIRECTORY = Path(__file__).resolve().parent
10
- if str(SCRIPT_DIRECTORY) not in sys.path:
11
- sys.path.insert(0, str(SCRIPT_DIRECTORY))
12
- sys.modules.pop("config", None)
11
+ _git_hooks_directory_string = str(SCRIPT_DIRECTORY)
12
+ while _git_hooks_directory_string in sys.path:
13
+ sys.path.remove(_git_hooks_directory_string)
14
+ sys.path.insert(0, _git_hooks_directory_string)
15
+ for each_module_name in list(sys.modules):
16
+ if each_module_name == "config" or each_module_name.startswith("config."):
17
+ del sys.modules[each_module_name]
18
+ importlib.invalidate_caches()
13
19
 
14
20
  import gate_utils
15
21
 
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import importlib
3
4
  import sys
4
5
  from pathlib import Path
5
6
 
@@ -7,9 +8,14 @@ import pytest
7
8
 
8
9
 
9
10
  SCRIPT_DIRECTORY = Path(__file__).resolve().parent
10
- if str(SCRIPT_DIRECTORY) not in sys.path:
11
- sys.path.insert(0, str(SCRIPT_DIRECTORY))
12
- sys.modules.pop("config", None)
11
+ _git_hooks_directory_string = str(SCRIPT_DIRECTORY)
12
+ while _git_hooks_directory_string in sys.path:
13
+ sys.path.remove(_git_hooks_directory_string)
14
+ sys.path.insert(0, _git_hooks_directory_string)
15
+ for each_module_name in list(sys.modules):
16
+ if each_module_name == "config" or each_module_name.startswith("config."):
17
+ del sys.modules[each_module_name]
18
+ importlib.invalidate_caches()
13
19
 
14
20
  import pre_commit
15
21
 
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import importlib
3
4
  import io
4
5
  import sys
5
6
  from pathlib import Path
@@ -8,9 +9,14 @@ import pytest
8
9
 
9
10
 
10
11
  SCRIPT_DIRECTORY = Path(__file__).resolve().parent
11
- if str(SCRIPT_DIRECTORY) not in sys.path:
12
- sys.path.insert(0, str(SCRIPT_DIRECTORY))
13
- sys.modules.pop("config", None)
12
+ _git_hooks_directory_string = str(SCRIPT_DIRECTORY)
13
+ while _git_hooks_directory_string in sys.path:
14
+ sys.path.remove(_git_hooks_directory_string)
15
+ sys.path.insert(0, _git_hooks_directory_string)
16
+ for each_module_name in list(sys.modules):
17
+ if each_module_name == "config" or each_module_name.startswith("config."):
18
+ del sys.modules[each_module_name]
19
+ importlib.invalidate_caches()
14
20
 
15
21
  import pre_push
16
22
  import config
@@ -6,8 +6,10 @@ Exit code 0 = all checks pass, 1 = violations found.
6
6
  # pragma: no-tdd-gate
7
7
 
8
8
  import argparse
9
+ import os
9
10
  import subprocess
10
11
  import sys
12
+ import tempfile
11
13
  import time
12
14
  from dataclasses import dataclass
13
15
  from datetime import datetime
@@ -26,18 +28,89 @@ hooks_dir = VALIDATORS_DIR.parent
26
28
  package_name = VALIDATORS_DIR.name
27
29
 
28
30
 
31
+ def _windows_non_unc_working_directory_string(
32
+ candidate_directory_strings: list[str | None],
33
+ ) -> str:
34
+ """Return the first candidate cwd that is not a UNC path (Windows only)."""
35
+ for each_candidate in candidate_directory_strings:
36
+ if each_candidate is None:
37
+ continue
38
+ expanded_candidate = str(Path(each_candidate).expanduser())
39
+ if expanded_candidate.startswith("\\\\"):
40
+ continue
41
+ return expanded_candidate
42
+ current_working_directory = os.getcwd()
43
+ expanded_current_working_directory = str(Path(current_working_directory).expanduser())
44
+ if not expanded_current_working_directory.startswith("\\\\"):
45
+ return expanded_current_working_directory
46
+ raise RuntimeError(
47
+ "Cannot find a non-UNC working directory for hook validator subprocesses."
48
+ )
49
+
50
+
51
+ def _hooks_subprocess_working_directory_and_environment() -> tuple[str, dict[str, str]]:
52
+ """Return cwd and env for validator subprocesses.
53
+
54
+ On Windows, ``CreateProcess`` rejects some UNC working directories (invalid
55
+ directory name). When the hooks tree resolves to UNC, use a local temp cwd
56
+ and put the hooks directory on ``PYTHONPATH`` so ``python -m validators.*``
57
+ still resolves.
58
+ """
59
+ hooks_directory_string = str(hooks_dir.resolve())
60
+ environment = os.environ.copy()
61
+ previous_pythonpath = environment.get("PYTHONPATH", "")
62
+ environment["PYTHONPATH"] = (
63
+ hooks_directory_string
64
+ + (os.pathsep + previous_pythonpath if previous_pythonpath else "")
65
+ )
66
+ working_directory_string = hooks_directory_string
67
+ if sys.platform == "win32" and working_directory_string.startswith("\\\\"):
68
+ windows_temp_fallback_directory = str(Path(r"C:\Windows\Temp"))
69
+ working_directory_string = _windows_non_unc_working_directory_string(
70
+ [
71
+ os.environ.get("TEMP"),
72
+ os.environ.get("TMP"),
73
+ tempfile.gettempdir(),
74
+ windows_temp_fallback_directory,
75
+ ]
76
+ )
77
+ return working_directory_string, environment
78
+
79
+
29
80
  def invoke_validator_module(module_stem: str, forwarded_file_paths: List[str]) -> subprocess.CompletedProcess[str]: # pragma: no-tdd-gate
30
81
  """Run a sibling validator as ``python -m validators.<module_stem>``.
31
82
 
32
- The subprocess is launched with ``cwd`` set to the hooks directory so the
33
- ``validators`` package qualifier resolves without requiring PYTHONPATH.
83
+ The subprocess uses the hooks tree on ``PYTHONPATH`` (and normally ``cwd``
84
+ there). On Windows, if that path is UNC, ``cwd`` falls back to a local temp
85
+ directory so ``CreateProcess`` succeeds.
34
86
  """
35
87
  qualified_module = ".".join([package_name, module_stem])
88
+ working_directory_string, environment = (
89
+ _hooks_subprocess_working_directory_and_environment()
90
+ )
36
91
  return subprocess.run(
37
92
  [sys.executable, "-m", qualified_module, *forwarded_file_paths],
38
93
  capture_output=True,
39
94
  text=True,
40
- cwd=str(hooks_dir),
95
+ cwd=working_directory_string,
96
+ env=environment,
97
+ )
98
+
99
+
100
+ def run_validators_entrypoint_subprocess(
101
+ extra_arguments: List[str],
102
+ ) -> subprocess.CompletedProcess[str]:
103
+ """Run ``python -m validators.run_all_validators`` with a Windows-safe cwd."""
104
+ working_directory_string, environment = (
105
+ _hooks_subprocess_working_directory_and_environment()
106
+ )
107
+ entry_module = f"{package_name}.run_all_validators"
108
+ return subprocess.run(
109
+ [sys.executable, "-m", entry_module, *extra_arguments],
110
+ capture_output=True,
111
+ text=True,
112
+ cwd=working_directory_string,
113
+ env=environment,
41
114
  )
42
115
 
43
116
 
@@ -1,7 +1,6 @@
1
1
  """Tests for output formatting."""
2
2
 
3
- import os
4
- import sys
3
+ import json
5
4
 
6
5
  import pytest
7
6
 
@@ -15,6 +14,7 @@ from .output_formatter import (
15
14
  ViolationDict,
16
15
  ValidatorResultDict,
17
16
  )
17
+ from .run_all_validators import run_validators_entrypoint_subprocess
18
18
 
19
19
 
20
20
  class TestColorize:
@@ -93,21 +93,9 @@ class TestOutputFormatter:
93
93
 
94
94
  class TestJsonFlag:
95
95
  def test_json_flag_produces_valid_json(self) -> None:
96
- import json
97
- import subprocess
96
+ completed_validation_run = run_validators_entrypoint_subprocess(["--json"])
98
97
 
99
- validators_directory = os.path.dirname(os.path.abspath(__file__))
100
- hooks_directory = os.path.normpath(
101
- os.path.join(validators_directory, os.pardir)
102
- )
103
- result = subprocess.run(
104
- [sys.executable, "-m", "validators.run_all_validators", "--json"],
105
- capture_output=True,
106
- text=True,
107
- cwd=hooks_directory,
108
- )
109
-
110
- output = result.stdout.strip()
98
+ output = completed_validation_run.stdout.strip()
111
99
  parsed = json.loads(output)
112
100
  assert "results" in parsed
113
101
  assert isinstance(parsed["results"], list)
@@ -1,6 +1,8 @@
1
1
  """Tests for run_all_validators.py."""
2
2
 
3
+ import os
3
4
  import sys
5
+ import tempfile
4
6
  from pathlib import Path
5
7
  from unittest.mock import MagicMock, patch
6
8
 
@@ -8,6 +10,7 @@ import pytest
8
10
 
9
11
  from .run_all_validators import (
10
12
  ValidatorResult,
13
+ _hooks_subprocess_working_directory_and_environment,
11
14
  add_timing,
12
15
  build_json_output,
13
16
  create_timing_metrics,
@@ -258,3 +261,22 @@ class TestVersionHeader:
258
261
  assert "version" in json_output
259
262
  assert "timestamp" in json_output
260
263
  assert isinstance(json_output["version"], str)
264
+
265
+
266
+ class TestHooksSubprocessWorkingDirectory:
267
+ def test_unc_path_fallback_on_windows_uses_tempdir_when_temp_unset(self) -> None:
268
+ mock_hooks_directory = MagicMock()
269
+ mock_hooks_directory.resolve.return_value = Path("\\\\server\\share\\hooks")
270
+ environment_without_temp = {
271
+ each_key: each_value
272
+ for each_key, each_value in os.environ.items()
273
+ if each_key not in ("TEMP", "TMP")
274
+ }
275
+ with patch("validators.run_all_validators.hooks_dir", mock_hooks_directory), patch(
276
+ "validators.run_all_validators.sys.platform", "win32"
277
+ ), patch("validators.run_all_validators.os.environ", environment_without_temp):
278
+ working_directory_string, _environment = (
279
+ _hooks_subprocess_working_directory_and_environment()
280
+ )
281
+ assert working_directory_string == tempfile.gettempdir()
282
+ assert not working_directory_string.startswith("\\\\")
@@ -1,21 +1,12 @@
1
1
  """Integration test for new validators in run_all_validators.py"""
2
2
 
3
3
  import subprocess
4
- import sys
5
- from pathlib import Path
6
4
 
7
- VALIDATORS_DIR = Path(__file__).parent
8
- HOOKS_DIR = VALIDATORS_DIR.parent
9
- PACKAGE_MODULE = f"{VALIDATORS_DIR.name}.run_all_validators"
5
+ from .run_all_validators import run_validators_entrypoint_subprocess
10
6
 
11
7
 
12
8
  def run_validators_help() -> subprocess.CompletedProcess[str]:
13
- return subprocess.run(
14
- [sys.executable, "-m", PACKAGE_MODULE, "--help"],
15
- capture_output=True,
16
- text=True,
17
- cwd=str(HOOKS_DIR),
18
- )
9
+ return run_validators_entrypoint_subprocess(["--help"])
19
10
 
20
11
 
21
12
  class TestNewValidatorsIntegration:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.29.3",
3
+ "version": "1.30.1",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {