claude-dev-env 1.93.1 → 1.95.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 (262) hide show
  1. package/_shared/advisor/CLAUDE.md +2 -2
  2. package/_shared/advisor/advisor-protocol.md +35 -27
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +3 -2
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +23 -15
  5. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +81 -17
  6. package/_shared/pr-loop/CLAUDE.md +1 -0
  7. package/_shared/pr-loop/audit-contract.md +1 -1
  8. package/_shared/pr-loop/gh-payloads.md +3 -3
  9. package/_shared/pr-loop/post-audit-thread-contract.md +51 -0
  10. package/_shared/pr-loop/scripts/README.md +2 -2
  11. package/_shared/pr-loop/scripts/fix_hookspath.py +47 -12
  12. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -69
  13. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -374
  14. package/_shared/pr-loop/state-schema.md +3 -22
  15. package/bin/CLAUDE.md +11 -2
  16. package/bin/ever-shipped-skills.mjs +70 -0
  17. package/bin/install.mjs +138 -13
  18. package/bin/install.prune.test.mjs +457 -0
  19. package/bin/install.test.mjs +10 -4
  20. package/docs/CODE_RULES.md +1 -1
  21. package/hooks/blocking/CLAUDE.md +2 -0
  22. package/hooks/blocking/code_rules_enforcer.py +4 -0
  23. package/hooks/blocking/code_rules_shared.py +82 -0
  24. package/hooks/blocking/code_rules_test_layout.py +9 -3
  25. package/hooks/blocking/pii_commit_command.py +398 -0
  26. package/hooks/blocking/pii_payload_scan.py +133 -0
  27. package/hooks/blocking/pii_prevention_blocker.py +151 -449
  28. package/hooks/blocking/plain_language_blocker.py +138 -4
  29. package/hooks/blocking/sensitive_file_protector.py +114 -48
  30. package/hooks/blocking/tdd_enforcer.py +9 -2
  31. package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +105 -0
  32. package/hooks/blocking/test_code_rules_shared.py +181 -0
  33. package/hooks/blocking/test_pii_prevention_blocker.py +299 -0
  34. package/hooks/blocking/test_plain_language_blocker_allowlist.py +184 -0
  35. package/hooks/blocking/test_sensitive_file_protector.py +185 -0
  36. package/hooks/blocking/test_tdd_enforcer_scratchpad.py +105 -0
  37. package/hooks/blocking/test_verdict_directory_write_blocker.py +42 -7
  38. package/hooks/hooks_constants/CLAUDE.md +4 -2
  39. package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -2
  40. package/hooks/hooks_constants/harness_scratchpad_constants.py +17 -0
  41. package/hooks/hooks_constants/local_identity.py +65 -15
  42. package/hooks/hooks_constants/pii_prevention_constants.py +16 -0
  43. package/hooks/hooks_constants/plain_language_blocker_constants.py +5 -0
  44. package/hooks/hooks_constants/pr_description_enforcer_constants.py +1 -1
  45. package/hooks/hooks_constants/sensitive_file_protector_constants.py +42 -0
  46. package/hooks/hooks_constants/test_local_identity.py +68 -0
  47. package/hooks/pyproject.toml +75 -4
  48. package/hooks/validators/CLAUDE.md +1 -1
  49. package/hooks/validators/README.md +2 -0
  50. package/hooks/validators/python_style_checks.py +114 -136
  51. package/hooks/validators/python_style_helpers.py +95 -0
  52. package/hooks/validators/test_python_style_checks.py +0 -164
  53. package/hooks/validators/test_python_style_checks_decorator_gap.py +119 -0
  54. package/hooks/validators/test_python_style_fixes.py +251 -0
  55. package/hooks/validators/test_python_style_helpers.py +125 -0
  56. package/package.json +1 -1
  57. package/rules/CLAUDE.md +1 -0
  58. package/rules/anti-corollary-tests.md +69 -0
  59. package/rules/bdd.md +1 -3
  60. package/rules/code-reviews.md +1 -1
  61. package/rules/gh-paginate.md +1 -1
  62. package/rules/plain-language.md +2 -0
  63. package/scripts/CLAUDE.md +10 -1
  64. package/scripts/Get-SessionAccount.ps1 +210 -0
  65. package/scripts/tests/CLAUDE.md +15 -1
  66. package/scripts/tests/Get-SessionAccount.Tests.ps1 +350 -0
  67. package/skills/CLAUDE.md +6 -27
  68. package/skills/anthropic-plan/SKILL.md +1 -13
  69. package/skills/autoconverge/CLAUDE.md +5 -2
  70. package/skills/autoconverge/SKILL.md +112 -387
  71. package/skills/autoconverge/reference/CLAUDE.md +4 -1
  72. package/skills/autoconverge/reference/closing-report.md +9 -2
  73. package/skills/autoconverge/reference/convergence.md +26 -22
  74. package/skills/autoconverge/reference/headless-safety.md +44 -0
  75. package/skills/autoconverge/reference/multi-pr.md +88 -0
  76. package/skills/autoconverge/reference/self-closing-loop.md +84 -0
  77. package/skills/autoconverge/reference/stop-conditions.md +21 -13
  78. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +87 -27
  79. package/skills/autoconverge/workflow/converge.contract.test.mjs +40 -10
  80. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +179 -47
  81. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  82. package/skills/autoconverge/workflow/converge.mjs +286 -96
  83. package/skills/bugteam/CLAUDE.md +1 -1
  84. package/skills/bugteam/CONSTRAINTS.md +3 -3
  85. package/skills/bugteam/PROMPTS.md +8 -19
  86. package/skills/bugteam/SKILL.md +13 -14
  87. package/skills/bugteam/reference/CLAUDE.md +1 -1
  88. package/skills/bugteam/reference/README.md +2 -3
  89. package/skills/bugteam/reference/audit-and-teammates.md +2 -2
  90. package/skills/bugteam/reference/copilot-gap-analysis.md +2 -0
  91. package/skills/bugteam/reference/github-pr-reviews.md +56 -38
  92. package/skills/bugteam/reference/team-setup.md +11 -11
  93. package/skills/bugteam/scripts/CLAUDE.md +6 -6
  94. package/skills/bugteam/scripts/README.md +20 -8
  95. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +13 -1463
  96. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +23 -358
  97. package/skills/bugteam/scripts/bugteam_preflight.py +13 -315
  98. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +2 -3
  99. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +34 -1053
  100. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +30 -347
  101. package/skills/bugteam/scripts/test_bugteam_preflight.py +30 -305
  102. package/skills/closeout/SKILL.md +153 -0
  103. package/skills/closeout/reference/handoff-prompt-template.md +72 -0
  104. package/skills/closeout/reference/issue-body-templates.md +108 -0
  105. package/skills/closeout/reference/pii-redaction-checklist.md +36 -0
  106. package/skills/copilot-finding-triage/SKILL.md +80 -28
  107. package/skills/copilot-finding-triage/reference/tier-rubric.md +70 -11
  108. package/skills/copilot-finding-triage/templates/notification.md +8 -3
  109. package/skills/everything-search/SKILL.md +1 -2
  110. package/skills/grokify/SKILL.md +58 -0
  111. package/skills/grokify/templates/handoff-template.md +92 -0
  112. package/skills/orchestrator/SKILL.md +35 -25
  113. package/skills/orchestrator-refresh/SKILL.md +15 -11
  114. package/skills/pr-converge/CLAUDE.md +1 -1
  115. package/skills/pr-converge/SKILL.md +132 -233
  116. package/skills/pr-converge/reference/convergence-gates.md +102 -66
  117. package/skills/pr-converge/reference/examples.md +37 -25
  118. package/skills/pr-converge/reference/fix-protocol.md +7 -7
  119. package/skills/pr-converge/reference/multi-pr-orchestration.md +10 -4
  120. package/skills/pr-converge/reference/per-tick.md +64 -41
  121. package/skills/pr-converge/reference/state-schema.md +27 -2
  122. package/skills/pr-converge/scripts/CLAUDE.md +8 -0
  123. package/skills/pr-converge/scripts/README.md +4 -3
  124. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +23 -0
  125. package/skills/pr-converge/scripts/check_convergence.py +189 -645
  126. package/skills/pr-converge/scripts/check_convergence_gates.py +235 -0
  127. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +219 -0
  128. package/skills/pr-converge/scripts/conftest.py +8 -0
  129. package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -0
  130. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -0
  131. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +26 -0
  132. package/skills/pr-converge/scripts/test_check_convergence.py +73 -228
  133. package/skills/pr-converge/scripts/test_check_convergence_contract.py +321 -0
  134. package/skills/pr-converge/scripts/test_check_convergence_gates.py +68 -0
  135. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +75 -0
  136. package/skills/pr-fix-protocol/SKILL.md +20 -20
  137. package/skills/privacy-hygiene/SKILL.md +4 -3
  138. package/skills/reviewer-gates/SKILL.md +26 -8
  139. package/skills/session-log/SKILL.md +11 -12
  140. package/skills/session-tidy/SKILL.md +40 -30
  141. package/skills/skill-builder/CLAUDE.md +13 -10
  142. package/skills/skill-builder/SKILL.md +31 -31
  143. package/skills/skill-builder/references/CLAUDE.md +10 -6
  144. package/skills/skill-builder/references/delegation-map.md +13 -4
  145. package/skills/skill-builder/references/description-field.md +113 -0
  146. package/skills/skill-builder/references/self-audit-checklist.md +21 -4
  147. package/skills/skill-builder/references/skill-modularity.md +113 -0
  148. package/skills/skill-builder/references/skill-types.md +4 -0
  149. package/skills/skill-builder/templates/CLAUDE.md +4 -4
  150. package/skills/skill-builder/templates/gap-analysis.md +25 -1
  151. package/skills/skill-builder/workflows/CLAUDE.md +6 -4
  152. package/skills/skill-builder/workflows/improve-skill.md +17 -16
  153. package/skills/skill-builder/workflows/new-skill.md +34 -10
  154. package/skills/skill-builder/workflows/polish-skill.md +59 -43
  155. package/skills/team-advisor/SKILL.md +15 -11
  156. package/skills/usage-pause/SKILL.md +10 -14
  157. package/system-prompts/software-engineer.xml +7 -6
  158. package/hooks/validators/test_verify_paths.py +0 -32
  159. package/hooks/validators/verify_paths.py +0 -57
  160. package/skills/bdd-protocol/CLAUDE.md +0 -26
  161. package/skills/bdd-protocol/SKILL.md +0 -30
  162. package/skills/bdd-protocol/references/CLAUDE.md +0 -10
  163. package/skills/bdd-protocol/references/anti-patterns.md +0 -26
  164. package/skills/bdd-protocol/references/example-mapping.md +0 -23
  165. package/skills/bg-agent/CLAUDE.md +0 -17
  166. package/skills/bg-agent/SKILL.md +0 -69
  167. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +0 -55
  168. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_fix_hookspath_constants.py +0 -26
  169. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_preflight_constants.py +0 -35
  170. package/skills/caveman/CLAUDE.md +0 -15
  171. package/skills/caveman/SKILL.md +0 -38
  172. package/skills/code/CLAUDE.md +0 -17
  173. package/skills/code/SKILL.md +0 -178
  174. package/skills/deep-research/CLAUDE.md +0 -17
  175. package/skills/deep-research/SKILL.md +0 -80
  176. package/skills/findbugs/CLAUDE.md +0 -20
  177. package/skills/findbugs/SKILL.md +0 -241
  178. package/skills/fixbugs/CLAUDE.md +0 -19
  179. package/skills/fixbugs/SKILL.md +0 -142
  180. package/skills/gh-paginate/CLAUDE.md +0 -18
  181. package/skills/gh-paginate/SKILL.md +0 -84
  182. package/skills/gotcha/CLAUDE.md +0 -33
  183. package/skills/gotcha/SKILL.md +0 -73
  184. package/skills/implement/CLAUDE.md +0 -27
  185. package/skills/implement/SKILL.md +0 -66
  186. package/skills/implement/scripts/CLAUDE.md +0 -22
  187. package/skills/implement/scripts/append_note.py +0 -133
  188. package/skills/implement/scripts/implement_scripts_constants/CLAUDE.md +0 -22
  189. package/skills/implement/scripts/implement_scripts_constants/__init__.py +0 -0
  190. package/skills/implement/scripts/implement_scripts_constants/notes_constants.py +0 -12
  191. package/skills/implement/scripts/test_append_note.py +0 -191
  192. package/skills/log-audit/CLAUDE.md +0 -20
  193. package/skills/log-audit/SKILL.md +0 -66
  194. package/skills/log-audit/reference/CLAUDE.md +0 -9
  195. package/skills/log-audit/reference/charter.md +0 -52
  196. package/skills/log-audit/scripts/CLAUDE.md +0 -27
  197. package/skills/log-audit/scripts/cluster_recurrences.py +0 -261
  198. package/skills/log-audit/scripts/collect_log_window.py +0 -199
  199. package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +0 -12
  200. package/skills/log-audit/scripts/log_audit_constants/__init__.py +0 -0
  201. package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +0 -23
  202. package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +0 -24
  203. package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +0 -49
  204. package/skills/log-audit/scripts/mine_copilot_findings.py +0 -302
  205. package/skills/log-audit/scripts/test_cluster_recurrences.py +0 -160
  206. package/skills/log-audit/scripts/test_collect_log_window.py +0 -111
  207. package/skills/log-audit/scripts/test_mine_copilot_findings.py +0 -126
  208. package/skills/logifix/CLAUDE.md +0 -36
  209. package/skills/logifix/SKILL.md +0 -69
  210. package/skills/logifix/scripts/CLAUDE.md +0 -16
  211. package/skills/logifix/scripts/logifix.ps1 +0 -205
  212. package/skills/monitor-open-prs/CLAUDE.md +0 -34
  213. package/skills/monitor-open-prs/SKILL.md +0 -88
  214. package/skills/monitor-open-prs/scripts/CLAUDE.md +0 -17
  215. package/skills/monitor-open-prs/scripts/discover_open_prs.py +0 -69
  216. package/skills/monitor-open-prs/scripts/test_discover_open_prs.py +0 -149
  217. package/skills/monitor-open-prs/test_skill_contract.py +0 -27
  218. package/skills/post-audit-findings/SKILL.md +0 -80
  219. package/skills/pr-consistency-audit/CLAUDE.md +0 -34
  220. package/skills/pr-consistency-audit/SKILL.md +0 -116
  221. package/skills/pr-consistency-audit/reference/CLAUDE.md +0 -16
  222. package/skills/pr-consistency-audit/reference/detection-rules.md +0 -96
  223. package/skills/pr-consistency-audit/reference/illustrations.md +0 -78
  224. package/skills/pr-review-responder/CLAUDE.md +0 -35
  225. package/skills/pr-review-responder/EXAMPLES.md +0 -590
  226. package/skills/pr-review-responder/PRINCIPLES.md +0 -533
  227. package/skills/pr-review-responder/README.md +0 -168
  228. package/skills/pr-review-responder/SKILL.md +0 -203
  229. package/skills/pr-review-responder/TESTING.md +0 -350
  230. package/skills/pr-scope-resolve/SKILL.md +0 -45
  231. package/skills/pre-compact/CLAUDE.md +0 -24
  232. package/skills/pre-compact/SKILL.md +0 -134
  233. package/skills/qbug/CLAUDE.md +0 -40
  234. package/skills/qbug/SKILL.md +0 -387
  235. package/skills/qbug/test_qbug_skill_audit_schema.py +0 -152
  236. package/skills/qbug/test_qbug_skill_post_fix_audit.py +0 -103
  237. package/skills/refine/CLAUDE.md +0 -44
  238. package/skills/refine/SKILL.md +0 -262
  239. package/skills/refine/templates/CLAUDE.md +0 -17
  240. package/skills/refine/templates/implementation-notes-template.html +0 -56
  241. package/skills/refine/templates/plan-template.md +0 -60
  242. package/skills/research-mode/CLAUDE.md +0 -35
  243. package/skills/research-mode/SKILL.md +0 -53
  244. package/skills/structure-prompt/CLAUDE.md +0 -42
  245. package/skills/structure-prompt/SKILL.md +0 -44
  246. package/skills/structure-prompt/reference/CLAUDE.md +0 -28
  247. package/skills/structure-prompt/reference/adversarial-tuning.md +0 -62
  248. package/skills/structure-prompt/reference/block-classification.md +0 -27
  249. package/skills/structure-prompt/reference/canonical-case.md +0 -48
  250. package/skills/structure-prompt/reference/citation-depth.md +0 -70
  251. package/skills/structure-prompt/reference/cleanup.md +0 -33
  252. package/skills/structure-prompt/reference/constraints.md +0 -33
  253. package/skills/structure-prompt/reference/directives.md +0 -37
  254. package/skills/structure-prompt/reference/examples.md +0 -72
  255. package/skills/structure-prompt/reference/instantiation.md +0 -51
  256. package/skills/structure-prompt/reference/output-contract.md +0 -72
  257. package/skills/structure-prompt/reference/per-category.md +0 -23
  258. package/skills/structure-prompt/reference/persona.md +0 -38
  259. package/skills/structure-prompt/reference/research.md +0 -33
  260. package/skills/structure-prompt/reference/structure.md +0 -28
  261. package/skills/verified-build/CLAUDE.md +0 -33
  262. package/skills/verified-build/SKILL.md +0 -33
@@ -0,0 +1,105 @@
1
+ """Tests for the session-scratchpad exemption wired into tdd_enforcer.main."""
2
+
3
+ import importlib.util
4
+ import io
5
+ import json
6
+ import os
7
+ import sys
8
+ import tempfile
9
+ from pathlib import Path
10
+ from types import ModuleType
11
+
12
+ import pytest
13
+
14
+ TDD_ENFORCER_PATH = Path(__file__).parent / "tdd_enforcer.py"
15
+ FIXED_USER_ID = 5150
16
+ WORKING_DIRECTORY = "/home/user/project"
17
+ SESSION_ID = "tdd-session-987"
18
+ PRODUCTION_CONTENT = "def fulfill_order():\n return True\n"
19
+
20
+
21
+ def _load_tdd_enforcer() -> ModuleType:
22
+ module_spec = importlib.util.spec_from_file_location(
23
+ "tdd_enforcer_scratchpad_under_test", TDD_ENFORCER_PATH
24
+ )
25
+ assert module_spec is not None and module_spec.loader is not None
26
+ loaded_module = importlib.util.module_from_spec(module_spec)
27
+ module_spec.loader.exec_module(loaded_module)
28
+ return loaded_module
29
+
30
+
31
+ _TDD_ENFORCER = _load_tdd_enforcer()
32
+
33
+
34
+ def _install_scratchpad_signals(monkeypatch: pytest.MonkeyPatch, temporary_root: Path) -> Path:
35
+ monkeypatch.setattr(os, "getuid", lambda: FIXED_USER_ID, raising=False)
36
+ monkeypatch.setattr(tempfile, "gettempdir", lambda: str(temporary_root))
37
+ mangled_working_directory = WORKING_DIRECTORY.replace("/", "-")
38
+ scratchpad_directory = (
39
+ temporary_root
40
+ / f"claude-{FIXED_USER_ID}"
41
+ / mangled_working_directory
42
+ / SESSION_ID
43
+ / "scratchpad"
44
+ )
45
+ scratchpad_directory.mkdir(parents=True)
46
+ return scratchpad_directory
47
+
48
+
49
+ def _write_payload(target: Path) -> dict[str, object]:
50
+ return {
51
+ "tool_name": "Write",
52
+ "cwd": WORKING_DIRECTORY,
53
+ "session_id": SESSION_ID,
54
+ "tool_input": {"file_path": str(target), "content": PRODUCTION_CONTENT},
55
+ }
56
+
57
+
58
+ def _run_main(
59
+ monkeypatch: pytest.MonkeyPatch, payload: dict[str, object]
60
+ ) -> tuple[int | None, str]:
61
+ monkeypatch.setattr(sys, "stdin", io.StringIO(json.dumps(payload)))
62
+ captured_stdout = io.StringIO()
63
+ monkeypatch.setattr(sys, "stdout", captured_stdout)
64
+ exit_code: int | None = None
65
+ try:
66
+ _TDD_ENFORCER.main()
67
+ except SystemExit as raised_exit:
68
+ raw_code = raised_exit.code
69
+ exit_code = raw_code if isinstance(raw_code, int) else None
70
+ return exit_code, captured_stdout.getvalue()
71
+
72
+
73
+ def _decision_from(stdout_text: str) -> str | None:
74
+ if not stdout_text.strip():
75
+ return None
76
+ parsed = json.loads(stdout_text)
77
+ return parsed.get("hookSpecificOutput", {}).get("permissionDecision")
78
+
79
+
80
+ def test_scratchpad_production_write_is_exempt_from_tdd_gate(
81
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
82
+ ) -> None:
83
+ scratchpad_directory = _install_scratchpad_signals(monkeypatch, tmp_path)
84
+ monkeypatch.setenv("CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT", "1")
85
+ throwaway_script = scratchpad_directory / "one_off_probe.py"
86
+
87
+ exit_code, stdout_text = _run_main(monkeypatch, _write_payload(throwaway_script))
88
+
89
+ assert exit_code == 0
90
+ assert _decision_from(stdout_text) != "deny"
91
+
92
+
93
+ def test_identical_non_scratchpad_write_is_still_blocked(
94
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
95
+ ) -> None:
96
+ _install_scratchpad_signals(monkeypatch, tmp_path)
97
+ monkeypatch.setenv("CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT", "1")
98
+ outside_directory = tmp_path / "project" / "orders"
99
+ outside_directory.mkdir(parents=True)
100
+ production_module = outside_directory / "one_off_probe.py"
101
+
102
+ exit_code, stdout_text = _run_main(monkeypatch, _write_payload(production_module))
103
+
104
+ assert exit_code == 0
105
+ assert _decision_from(stdout_text) == "deny"
@@ -16,8 +16,18 @@ import subprocess
16
16
  import sys
17
17
 
18
18
  _HOOK_DIR = pathlib.Path(__file__).parent
19
+ _HOOKS_DIR = _HOOK_DIR.parent
19
20
  if str(_HOOK_DIR) not in sys.path:
20
21
  sys.path.insert(0, str(_HOOK_DIR))
22
+ if str(_HOOKS_DIR) not in sys.path:
23
+ sys.path.insert(0, str(_HOOKS_DIR))
24
+
25
+ from hooks_constants.bash_pre_tool_use_dispatcher_constants import ( # noqa: E402
26
+ ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
27
+ ALL_BASH_HOSTED_HOOK_ENTRIES,
28
+ BASH_TOOL_NAME,
29
+ POWERSHELL_TOOL_NAME,
30
+ )
21
31
 
22
32
  guard_spec = importlib.util.spec_from_file_location(
23
33
  "verdict_directory_write_blocker",
@@ -708,18 +718,43 @@ def _pretooluse_commands_for_matcher(matcher_substring: str) -> list[str]:
708
718
  return matching_commands
709
719
 
710
720
 
711
- def test_guard_is_registered_on_bash() -> None:
721
+ _VERDICT_GUARD_SCRIPT_RELATIVE_PATH = "blocking/verdict_directory_write_blocker.py"
722
+ _BASH_DISPATCHER_BASENAME = "bash_pre_tool_use_dispatcher.py"
723
+
724
+
725
+ def _verdict_guard_hosted_entries() -> list:
726
+ return [
727
+ each_entry
728
+ for each_entry in ALL_BASH_HOSTED_HOOK_ENTRIES
729
+ if each_entry.script_relative_path == _VERDICT_GUARD_SCRIPT_RELATIVE_PATH
730
+ ]
731
+
732
+
733
+ def _assert_bash_dispatcher_registered_for_matcher(matcher_substring: str) -> None:
734
+ matching_commands = _pretooluse_commands_for_matcher(matcher_substring)
712
735
  assert any(
713
- "verdict_directory_write_blocker.py" in each_command
714
- for each_command in _pretooluse_commands_for_matcher("Bash")
736
+ _BASH_DISPATCHER_BASENAME in each_command
737
+ for each_command in matching_commands
738
+ ), (
739
+ f"bash_pre_tool_use_dispatcher.py must be registered on matcher containing "
740
+ f"{matcher_substring!r}; commands={matching_commands!r}"
715
741
  )
716
742
 
717
743
 
744
+ def test_guard_is_registered_on_bash() -> None:
745
+ _assert_bash_dispatcher_registered_for_matcher("Bash")
746
+ matching_entries = _verdict_guard_hosted_entries()
747
+ assert len(matching_entries) == 1
748
+ assert matching_entries[0].applicable_tool_names == ALL_BASH_AND_POWERSHELL_TOOL_NAMES
749
+ assert BASH_TOOL_NAME in matching_entries[0].applicable_tool_names
750
+
751
+
718
752
  def test_guard_is_registered_on_powershell() -> None:
719
- assert any(
720
- "verdict_directory_write_blocker.py" in each_command
721
- for each_command in _pretooluse_commands_for_matcher("PowerShell")
722
- )
753
+ _assert_bash_dispatcher_registered_for_matcher("PowerShell")
754
+ matching_entries = _verdict_guard_hosted_entries()
755
+ assert len(matching_entries) == 1
756
+ assert matching_entries[0].applicable_tool_names == ALL_BASH_AND_POWERSHELL_TOOL_NAMES
757
+ assert POWERSHELL_TOOL_NAME in matching_entries[0].applicable_tool_names
723
758
 
724
759
 
725
760
  def test_hook_subprocess_imports_real_config_when_parent_holds_shadowing_config(
@@ -32,13 +32,14 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
32
32
  | `dynamic_stderr_handler.py` | `DynamicStderrHandler` — a logging handler that resolves `sys.stderr` at emit time (for testability) |
33
33
  | `gh_pr_author_swap_constants.py` | Constants for the PR-author swap enforcement hooks |
34
34
  | `hardcoded_user_path_constants.py` | Patterns for detecting hardcoded home-directory paths |
35
+ | `harness_scratchpad_constants.py` | Fixed path components (`claude-` user-directory prefix, separator replacement, `scratchpad` leaf name) and payload keys that rebuild the harness session scratchpad directory for the code-rules and TDD gate exemptions |
35
36
  | `hook_block_logger.py` | `log_hook_block()` — shared fail-safe logger every blocking hook calls to append a JSON record of each block decision to `~/.claude/logs/hook-blocks.log` |
36
37
  | `hook_log_extractor_constants.py` | Neon table name, offset state file path, timeouts, and outcome-type mapping for the hook-log extractor |
37
38
  | `hook_prose_detector_consistency_constants.py` | Trigger patterns and corrective messages for the hook-prose consistency checker |
38
39
  | `hosted_hook_runner.py` | `run_hook_capturing_output()` — shared runner that runs one dispatcher-hosted hook in-process via runpy and returns its captured stdout and crash flag |
39
40
  | `inline_tuple_string_magic_constants.py` | Patterns for detecting magic strings in inline tuple literals |
40
41
  | `js_conventions_constants.py` | Banned identifier set, boolean-prefix pattern, and declaration/JSDoc patterns for the JavaScript convention checks |
41
- | `local_identity.py` | Loader for the NAS host, ssh user, and ssh port the ssh enforcer guards, resolved from the `CLAUDE_NAS_*` env vars or `~/.claude/local-identity.json` with placeholder defaults; also composes the enforcer's two deny messages that quote those values |
42
+ | `local_identity.py` | Loader for local machine values: NAS host, ssh user, and ssh port the ssh enforcer guards (`CLAUDE_NAS_*` / `~/.claude/local-identity.json` with placeholder defaults), plus the PII commit-scan exempt-repo slug set (`CLAUDE_PII_EXEMPT_REPOS` / `pii_exempt_repositories`); also composes the ssh enforcer's two deny messages that quote the NAS values |
42
43
  | `messages.py` | Short user-facing notice strings shown when a Stop hook redirects agent behavior |
43
44
  | `multi_edit_reconstruction.py` | `apply_edits()` / `edits_for_tool()` — shared helpers that reconstruct the post-edit content of an Edit or MultiEdit, imported by the blockers that judge post-edit content |
44
45
  | `nas_ssh_binary_enforcer_constants.py` | Bash tool name, ssh-family basenames, OpenSSH binary path suffixes, launcher-wrapper set, shell control-operator tokens and split pattern, leading-assignment and duration patterns, and the batch-mode pattern for the NAS ssh binary enforcer |
@@ -60,6 +61,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
60
61
  | `python_style_checks_constants.py` | Command-line argument count and blank-line count between top-level functions for the style validator |
61
62
  | `reviewer_spawn_gate_constants.py` | Bash tool name, the sentinel marker, the Copilot and Bugbot trigger markers, the availability-script relative path and override env-var name, and the deny-message template for the reviewer-spawn gate |
62
63
  | `send_user_file_open_locally_blocker_constants.py` | Tool name, proactive status, and the block message for the open-locally attach blocker |
64
+ | `sensitive_file_protector_constants.py` | Sensitive filename patterns, the committed-template suffixes that earn an exemption, the write/edit tool names, and the deny decision and message template for `sensitive_file_protector` |
63
65
  | `session_edit_stage_gate_constants.py` | Tracker filename prefix/suffix, JSON payload key, edit tool name set, session-id sanitize pattern, lock filename suffix and lock-acquire timing, git diff command, commit flag escapes, and deny-message template shared by the session edit stage gate trio |
64
66
  | `session_env_cleanup_constants.py` | Stale-age threshold and directory names for the session-env cleanup hook |
65
67
  | `session_handoff_blocker_constants.py` | Trigger phrases for the session-handoff blocker |
@@ -85,5 +87,5 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
85
87
  - Hooks import from this package with `from hooks_constants.<module> import <CONSTANT>`.
86
88
  - Tests for these modules live beside them as `test_<module>.py`. Run with `python -m pytest hooks_constants/test_<name>.py`.
87
89
  - `dynamic_stderr_handler.py`, `pre_tool_use_stdin.py`, `multi_edit_reconstruction.py`, `hosted_hook_runner.py`, and `text_stripping.py` are utility modules (not pure constants) but live here because they are shared across many hooks.
88
- - `local_identity.py` is a loader: it reads the environment or `~/.claude/local-identity.json` to resolve the private NAS values, keeping a committed placeholder default.
90
+ - `local_identity.py` is a loader: it reads the environment or `~/.claude/local-identity.json` to resolve private NAS values (with committed placeholder defaults) and the PII commit-scan exempt-repo slug set (`CLAUDE_PII_EXEMPT_REPOS` / `pii_exempt_repositories`).
89
91
 
@@ -22,8 +22,6 @@ ALL_CONFIG_DIRECTORY_NAMES = frozenset(
22
22
  "pr_converge_skill_constants",
23
23
  "pr_converge_scripts_constants",
24
24
  "bugteam_scripts_constants",
25
- "implement_scripts_constants",
26
- "log_audit_constants",
27
25
  "usage_pause_constants",
28
26
  "dev_env_scripts_constants",
29
27
  }
@@ -0,0 +1,17 @@
1
+ """Constants for resolving the Claude Code harness session scratchpad directory.
2
+
3
+ The harness gives each session a throwaway scratch directory it announces in
4
+ the session's system prompt, laid out as
5
+ ``<tempdir>/claude-<uid>/<mangled-cwd>/<session-id>/scratchpad``. These constants
6
+ name the fixed path components the code-rules and TDD gates read to rebuild that
7
+ directory from the signals a hook process can see.
8
+
9
+ The exemption is POSIX-only: without ``os.getuid`` (Windows) it never fires,
10
+ so full enforcement stays in place there.
11
+ """
12
+
13
+ HARNESS_SCRATCHPAD_USER_DIRECTORY_PREFIX: str = "claude-"
14
+ HARNESS_SCRATCHPAD_PATH_SEPARATOR_REPLACEMENT: str = "-"
15
+ HARNESS_SCRATCHPAD_LEAF_DIRECTORY_NAME: str = "scratchpad"
16
+ HOOK_PAYLOAD_SESSION_ID_KEY: str = "session_id"
17
+ HOOK_PAYLOAD_WORKING_DIRECTORY_KEY: str = "cwd"
@@ -1,19 +1,23 @@
1
- """Resolve the NAS host, ssh user, and ssh port for the NAS ssh enforcer hook.
1
+ """Resolve local identity values for NAS ssh and PII exempt-repo hooks.
2
2
 
3
- The enforcer ships into ``~/.claude/`` and cannot read a repository file, so it
4
- reads the real NAS values from the environment or a git-ignored file in the
5
- Claude home directory, and it composes the two deny messages that quote those
6
- values. The committed defaults are placeholders.
3
+ The enforcer hooks ship into ``~/.claude/`` and cannot read a repository file,
4
+ so they read real machine values from the environment or a git-ignored file in
5
+ the Claude home directory. This module supplies NAS host/user/port, the two
6
+ NAS deny messages that quote those values, and the owner/repo slug set used by
7
+ ``pii_exempt_repository_slugs`` (``CLAUDE_PII_EXEMPT_REPOS`` /
8
+ ``pii_exempt_repositories``). The committed NAS defaults are placeholders.
7
9
 
8
10
  ::
9
11
 
10
- CLAUDE_NAS_HOST set to a host -> nas_host() returns that host
11
- CLAUDE_NAS_SSH_PORT set to 2200 -> nas_ssh_port() returns 2200
12
- (env unset, no file) -> nas_host() == "nas.example.local"
13
- nas_ssh_port() == 22
12
+ CLAUDE_NAS_HOST set to a host -> nas_host() returns that host
13
+ CLAUDE_NAS_SSH_PORT set to 2200 -> nas_ssh_port() returns 2200
14
+ CLAUDE_PII_EXEMPT_REPOS="Owner/repo" -> {"owner/repo"}
15
+ (env unset, no file) -> nas_host() == "nas.example.local"
16
+ nas_ssh_port() == 22
17
+ pii_exempt_repository_slugs() == frozenset()
14
18
 
15
- Each value comes from its ``CLAUDE_NAS_*`` environment variable, then
16
- ``~/.claude/local-identity.json``, then the placeholder default.
19
+ Each value comes from its environment variable, then
20
+ ``~/.claude/local-identity.json``, then the placeholder default (NAS only).
17
21
  """
18
22
 
19
23
  from __future__ import annotations
@@ -23,6 +27,9 @@ import os
23
27
  from pathlib import Path
24
28
 
25
29
  NAS_HOST_ENV_VAR = "CLAUDE_NAS_HOST"
30
+ PII_EXEMPT_REPOS_ENV_VAR = "CLAUDE_PII_EXEMPT_REPOS"
31
+ PII_EXEMPT_REPOS_JSON_KEY = "pii_exempt_repositories"
32
+ PII_EXEMPT_REPOS_SEPARATOR = ","
26
33
  NAS_SSH_USER_ENV_VAR = "CLAUDE_NAS_SSH_USER"
27
34
  NAS_SSH_PORT_ENV_VAR = "CLAUDE_NAS_SSH_PORT"
28
35
  NAS_JSON_KEY = "nas"
@@ -42,7 +49,7 @@ def _local_identity_file_path() -> Path:
42
49
  return Path.home() / CLAUDE_HOME_DIRECTORY_NAME / LOCAL_IDENTITY_FILE_NAME
43
50
 
44
51
 
45
- def _nas_section_from_local_file() -> dict[str, object]:
52
+ def _identity_dictionary_from_local_file() -> dict[str, object]:
46
53
  identity_path = _local_identity_file_path()
47
54
  if not identity_path.is_file():
48
55
  return {}
@@ -50,12 +57,55 @@ def _nas_section_from_local_file() -> dict[str, object]:
50
57
  parsed_identity = json.loads(identity_path.read_text(encoding="utf-8"))
51
58
  except (OSError, ValueError, UnicodeDecodeError):
52
59
  return {}
53
- if not isinstance(parsed_identity, dict):
54
- return {}
55
- nas_section = parsed_identity.get(NAS_JSON_KEY, {})
60
+ return parsed_identity if isinstance(parsed_identity, dict) else {}
61
+
62
+
63
+ def _nas_section_from_local_file() -> dict[str, object]:
64
+ nas_section = _identity_dictionary_from_local_file().get(NAS_JSON_KEY, {})
56
65
  return nas_section if isinstance(nas_section, dict) else {}
57
66
 
58
67
 
68
+ def _normalized_slug_set(all_raw_slugs: list[object]) -> frozenset[str]:
69
+ return frozenset(
70
+ each_slug.strip().lower()
71
+ for each_slug in all_raw_slugs
72
+ if isinstance(each_slug, str) and each_slug.strip()
73
+ )
74
+
75
+
76
+ def pii_exempt_repository_slugs() -> frozenset[str]:
77
+ """Return owner/repo slugs whose commits skip the staged PII scan.
78
+
79
+ ::
80
+
81
+ CLAUDE_PII_EXEMPT_REPOS="Owner/repo-a, Other/b" -> {"owner/repo-a", "other/b"}
82
+ CLAUDE_PII_EXEMPT_REPOS=" , , " + file lists A -> {"a"} (whitespace falls through)
83
+ (env unset, file lists ["Owner/repo-a"]) -> {"owner/repo-a"}
84
+ (env unset, no file) -> frozenset()
85
+
86
+ A non-empty environment variable (comma-separated) wins. When the variable
87
+ is unset or normalizes to no slugs, the ``pii_exempt_repositories`` list in
88
+ the git-ignored ``~/.claude/local-identity.json`` is used. Slugs are
89
+ lowercased so matching is case-insensitive.
90
+
91
+ Returns:
92
+ Lowercased ``owner/repo`` slugs exempt from staged-commit PII scanning.
93
+ """
94
+ slugs_from_environment = os.environ.get(PII_EXEMPT_REPOS_ENV_VAR)
95
+ if slugs_from_environment is not None:
96
+ normalized_environment_slugs = _normalized_slug_set(
97
+ list(slugs_from_environment.split(PII_EXEMPT_REPOS_SEPARATOR))
98
+ )
99
+ if normalized_environment_slugs:
100
+ return normalized_environment_slugs
101
+ stored_slugs = _identity_dictionary_from_local_file().get(
102
+ PII_EXEMPT_REPOS_JSON_KEY
103
+ )
104
+ if not isinstance(stored_slugs, list):
105
+ return frozenset()
106
+ return _normalized_slug_set(stored_slugs)
107
+
108
+
59
109
  def nas_host() -> str:
60
110
  """Return the NAS host the ssh enforcer guards.
61
111
 
@@ -187,6 +187,22 @@ ALL_STAGED_FILES_COMMAND: tuple[str, ...] = (
187
187
  ALL_STAGED_BLOB_SHOW_COMMAND_PREFIX: tuple[str, ...] = ("git", "show")
188
188
  STAGED_BLOB_PREFIX: str = ":"
189
189
 
190
+ ALL_GIT_ORIGIN_URL_COMMAND: tuple[str, ...] = (
191
+ "git",
192
+ "config",
193
+ "--get",
194
+ "remote.origin.url",
195
+ )
196
+ GIT_URL_SUFFIX: str = ".git"
197
+ GITHUB_COM_HOST: str = "github.com"
198
+ ALL_NETWORK_GIT_URL_SCHEMES: frozenset[str] = frozenset({"http", "https", "ssh"})
199
+ SCP_STYLE_PATH_SEPARATOR: str = ":"
200
+ USERINFO_HOST_SEPARATOR: str = "@"
201
+ URL_SCHEME_SEPARATOR: str = "://"
202
+ WINDOWS_PATH_SEPARATOR: str = "\\"
203
+ POSIX_PATH_SEPARATOR: str = "/"
204
+ MINIMUM_OWNER_REPO_SEGMENT_COUNT: int = 2
205
+
190
206
  BODY_FILE_ENCODING: str = "utf-8"
191
207
  NULL_BYTE_MARKER: bytes = b"\x00"
192
208
  MESSAGE_LINE_SEPARATOR: str = "\n"
@@ -289,6 +289,11 @@ MARKDOWN_EXTENSION: str = ".md"
289
289
  ASK_USER_QUESTION_TOOL_NAME: str = "AskUserQuestion"
290
290
  ALL_WRITE_EDIT_TOOL_NAMES: frozenset[str] = frozenset({"Write", "Edit", "MultiEdit"})
291
291
 
292
+ DOT_CLAUDE_DIRECTORY_NAME: str = ".claude"
293
+ PROJECT_ALLOWLIST_FILENAME: str = "plain-language-allow.json"
294
+ PROJECT_ROOT_WALK_LIMIT: int = 40
295
+ REPOSITORY_MARKER_NAME: str = ".git"
296
+
292
297
  USER_FACING_PLAIN_LANGUAGE_NOTICE: str = (
293
298
  "Plain-language check: replace each flagged formal word with the simpler "
294
299
  "word named in the reason."
@@ -121,6 +121,7 @@ DEFAULT_READABILITY_THRESHOLDS: ReadabilityThresholds = ReadabilityThresholds(
121
121
  __all__ = [
122
122
  "ALL_HEAVY_OPENING_HEADERS",
123
123
  "ALL_HEAVY_TESTING_HEADERS",
124
+ "ALL_PR_NUMBER_BEARING_GH_PR_SUBCOMMANDS",
124
125
  "ALL_READABILITY_CLI_FLAG_TOKENS",
125
126
  "ATOMIC_WRITE_TEMP_SUFFIX",
126
127
  "BLOCKQUOTE_LINE_PATTERN",
@@ -135,7 +136,6 @@ __all__ = [
135
136
  "FLESCH_SYLLABLES_PER_WORD_COEFFICIENT",
136
137
  "FLESCH_WORDS_PER_SENTENCE_COEFFICIENT",
137
138
  "GH_PR_COMMAND_MIN_TOKEN_COUNT",
138
- "ALL_PR_NUMBER_BEARING_GH_PR_SUBCOMMANDS",
139
139
  "HEADING_LINE_PATTERN",
140
140
  "HEAVY_MIN_BODY_CHARS_FOR_CLASSIFICATION",
141
141
  "HEAVY_SHAPE",
@@ -0,0 +1,42 @@
1
+ """Constants for the ``sensitive_file_protector`` PreToolUse hook.
2
+
3
+ The hook denies a Write or Edit whose target basename names a live secret or a
4
+ lock file, and steps aside for a placeholders-only committed template.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ ALL_SENSITIVE_PATTERNS = (
10
+ ".env",
11
+ ".env.*",
12
+ "*.env",
13
+ "*.pem",
14
+ "*.key",
15
+ "*.p12",
16
+ "*.pfx",
17
+ "credentials.json",
18
+ "secrets.json",
19
+ "id_rsa",
20
+ "id_ed25519",
21
+ "package-lock.json",
22
+ "yarn.lock",
23
+ "Pipfile.lock",
24
+ "poetry.lock",
25
+ "pnpm-lock.yaml",
26
+ "composer.lock",
27
+ )
28
+
29
+ ALL_TEMPLATE_SUFFIXES = (".example", ".sample", ".template")
30
+
31
+ ALL_WRITE_EDIT_TOOLS = ("Write", "Edit")
32
+
33
+ HOOK_SCRIPT_NAME = "sensitive_file_protector.py"
34
+
35
+ HOOK_EVENT_NAME = "PreToolUse"
36
+
37
+ DENY_DECISION = "deny"
38
+
39
+ DENY_REASON_TEMPLATE = (
40
+ "BLOCKED: Sensitive file '{filename}' (pattern: '{matched_pattern}'). "
41
+ "Edit manually outside Claude Code."
42
+ )
@@ -66,6 +66,74 @@ class TestNasValuesPlaceholderDefault:
66
66
  assert local_identity.nas_ssh_port() == 22
67
67
 
68
68
 
69
+ class TestPiiExemptRepositorySlugs:
70
+ def should_read_slugs_from_the_environment_lowercased(self) -> None:
71
+ with patch.dict(
72
+ os.environ,
73
+ {"CLAUDE_PII_EXEMPT_REPOS": "ExemptOwner/exempt-repo, Other/Repo"},
74
+ clear=False,
75
+ ):
76
+ slugs = local_identity.pii_exempt_repository_slugs()
77
+ assert slugs == frozenset({"exemptowner/exempt-repo", "other/repo"})
78
+
79
+ def should_read_slugs_from_the_local_identity_file(self, tmp_path: Path) -> None:
80
+ claude_home = tmp_path / ".claude"
81
+ claude_home.mkdir()
82
+ (claude_home / "local-identity.json").write_text(
83
+ json.dumps({"pii_exempt_repositories": ["FileOwner/file-repo"]}),
84
+ encoding="utf-8",
85
+ )
86
+ with patch.dict(os.environ, {}, clear=False):
87
+ os.environ.pop("CLAUDE_PII_EXEMPT_REPOS", None)
88
+ with patch.object(Path, "home", return_value=tmp_path):
89
+ slugs = local_identity.pii_exempt_repository_slugs()
90
+ assert slugs == frozenset({"fileowner/file-repo"})
91
+
92
+ def should_return_an_empty_set_when_no_env_and_no_file(
93
+ self, tmp_path: Path
94
+ ) -> None:
95
+ with patch.dict(os.environ, {}, clear=False):
96
+ os.environ.pop("CLAUDE_PII_EXEMPT_REPOS", None)
97
+ with patch.object(Path, "home", return_value=tmp_path):
98
+ assert local_identity.pii_exempt_repository_slugs() == frozenset()
99
+
100
+ def should_prefer_environment_slugs_over_local_identity_file(
101
+ self, tmp_path: Path
102
+ ) -> None:
103
+ claude_home = tmp_path / ".claude"
104
+ claude_home.mkdir()
105
+ (claude_home / "local-identity.json").write_text(
106
+ json.dumps({"pii_exempt_repositories": ["FileOwner/file-repo"]}),
107
+ encoding="utf-8",
108
+ )
109
+ with patch.dict(
110
+ os.environ,
111
+ {"CLAUDE_PII_EXEMPT_REPOS": "EnvOwner/env-repo"},
112
+ clear=False,
113
+ ):
114
+ with patch.object(Path, "home", return_value=tmp_path):
115
+ slugs = local_identity.pii_exempt_repository_slugs()
116
+ assert slugs == frozenset({"envowner/env-repo"})
117
+
118
+ def should_fall_through_to_json_when_environment_is_whitespace_only(
119
+ self, tmp_path: Path
120
+ ) -> None:
121
+ claude_home = tmp_path / ".claude"
122
+ claude_home.mkdir()
123
+ (claude_home / "local-identity.json").write_text(
124
+ json.dumps({"pii_exempt_repositories": ["FileOwner/file-repo"]}),
125
+ encoding="utf-8",
126
+ )
127
+ with patch.dict(
128
+ os.environ,
129
+ {"CLAUDE_PII_EXEMPT_REPOS": " , , "},
130
+ clear=False,
131
+ ):
132
+ with patch.object(Path, "home", return_value=tmp_path):
133
+ slugs = local_identity.pii_exempt_repository_slugs()
134
+ assert slugs == frozenset({"fileowner/file-repo"})
135
+
136
+
69
137
  class TestDenyMessagesQuoteTheResolvedHost:
70
138
  def should_include_the_resolved_host_and_port_in_the_bare_binary_message(
71
139
  self,
@@ -9,10 +9,24 @@
9
9
  mypy_path = "."
10
10
  python_version = "3.13"
11
11
  ignore_missing_imports = true
12
- warn_unused_ignores = true
12
+ warn_unused_ignores = false
13
13
  warn_redundant_casts = true
14
14
  no_implicit_optional = true
15
15
  strict_equality = true
16
+ exclude = [
17
+ "validators/test_files/",
18
+ "test_.*\\.py$",
19
+ ".*/test_.*\\.py$",
20
+ # One-line mypy fixes re-surface whole-file CODE_RULES debt at pre-commit (#51).
21
+ "abbreviation_checks\\.py$",
22
+ "git_checks\\.py$",
23
+ "exempt_paths\\.py$",
24
+ "code_rules_typeddict_stub\\.py$",
25
+ "validators/exempt_paths\\.py$",
26
+ "validators/abbreviation_checks\\.py$",
27
+ "validators/git_checks\\.py$",
28
+ "blocking/code_rules_typeddict_stub\\.py$",
29
+ ]
16
30
 
17
31
  [[tool.mypy.overrides]]
18
32
  module = "tests.*"
@@ -26,6 +40,7 @@ extend-exclude = [
26
40
  ".claude",
27
41
  "node_modules",
28
42
  "__pycache__",
43
+ "validators/test_files",
29
44
  ]
30
45
 
31
46
  [tool.ruff.lint]
@@ -39,16 +54,72 @@ select = [
39
54
  "SIM",
40
55
  "PL",
41
56
  ]
57
+ # Gate-restore (#51): principled + pure-style ignores. Non-negotiable defect
58
+ # classes stay selected; residual hits live under per-file-ignores where a
59
+ # one-line fix is blocked by whole-file CODE_RULES pre-commit debt.
42
60
  ignore = [
43
61
  "E501",
62
+ "E402",
63
+ "UP038",
64
+ "UP006",
65
+ "UP007",
66
+ "UP015",
67
+ "UP017",
68
+ "UP020",
69
+ "UP024",
70
+ "UP035",
71
+ "UP045",
72
+ "I001",
73
+ "B009",
74
+ "F541",
75
+ "PLR0911",
76
+ "PLR0912",
44
77
  "PLR0913",
78
+ "PLR0915",
79
+ "PLR1714",
45
80
  "PLR2004",
81
+ "PLR5501",
82
+ "PLC0414",
83
+ "PLC0415",
84
+ "SIM101",
85
+ "SIM102",
86
+ "SIM103",
87
+ "SIM105",
88
+ "SIM109",
89
+ "SIM110",
90
+ "SIM113",
91
+ "SIM114",
92
+ "SIM117",
93
+ "SIM300",
46
94
  ]
47
95
 
48
96
  [tool.ruff.lint.per-file-ignores]
49
- "test_*.py" = ["B011", "PLR2004"]
50
- "*_test.py" = ["B011", "PLR2004"]
51
- "conftest.py" = ["B011"]
97
+ "advisory/refactor_guard.py" = ["F401", "PLW1510"]
98
+ "blocking/block_main_commit.py" = ["PLW1510"]
99
+ "blocking/code_rules_boolean_mustcheck.py" = ["B905"]
100
+ "blocking/code_rules_mock_completeness.py" = ["B007"]
101
+ "blocking/code_rules_optional_params.py" = ["B905"]
102
+ "blocking/code_rules_shared.py" = ["B905"]
103
+ "blocking/code_rules_test_assertions.py" = ["B905"]
104
+ "blocking/code_rules_typeddict_stub.py" = ["B905"]
105
+ "blocking/pii_prevention_blocker.py" = ["PLW1510", "B018"]
106
+ "blocking/pr_description_command_parser.py" = ["B904"]
107
+ "blocking/precommit_code_rules_gate.py" = ["PLW1510"]
108
+ "diagnostic/hook_log_extractor.py" = ["SIM115"]
109
+ "git-hooks/post_commit.py" = ["F401", "PLW1510"]
110
+ "hooks_constants/banned_identifiers_constants.py" = ["B033"]
111
+ "session/plugin_data_dir_cleanup.py" = ["F401"]
112
+ "validation/mypy_validator.py" = ["PLW1510"]
113
+ "validators/exempt_paths.py" = ["F401"]
114
+ "validators/health_check.py" = ["PLW1510"]
115
+ "validators/mypy_integration.py" = ["PLW1510"]
116
+ "validators/python_style_checks.py" = ["B905"]
117
+ "validators/ruff_integration.py" = ["PLW1510"]
118
+ "validators/run_all_validators.py" = ["PLW1510"]
119
+ "workflow/auto_formatter.py" = ["PLW1510"]
120
+ "test_*.py" = ["B011", "PLR2004", "F401", "F841", "PLW1510", "B905", "B033", "SIM115", "B007", "B018", "B904"]
121
+ "*_test.py" = ["B011", "PLR2004", "F401", "F841", "PLW1510", "B905", "B033", "SIM115", "B007", "B018", "B904"]
122
+ "conftest.py" = ["B011", "F401", "F841", "PLW1510"]
52
123
 
53
124
  [tool.coverage.run]
54
125
  branch = true
@@ -12,6 +12,7 @@ A library of check modules used by the validation hooks. Each module focuses on
12
12
  | `output_formatter.py` | Formats `Violation` lists into human-readable output |
13
13
  | `run_all_validators.py` | Entry point — runs every check module and aggregates results |
14
14
  | `health_check.py` | Verifies that all validator dependencies (ruff, mypy) are reachable |
15
+ | `python_style_helpers.py` | Shared source-line splitting and function-discovery helpers imported by `python_style_checks.py` |
15
16
 
16
17
  ## Check modules
17
18
 
@@ -33,7 +34,6 @@ A library of check modules used by the validation hooks. Each module focuses on
33
34
  | `todo_checks.py` | TODO/FIXME markers without an associated issue reference |
34
35
  | `type_safety_checks.py` | Type-safety rules (no `Any`, no `cast`, no `# type: ignore`) |
35
36
  | `useless_test_checks.py` | Tests that check only existence or constant values |
36
- | `verify_paths.py` | Validates that file paths referenced in code actually exist |
37
37
 
38
38
  ## Subdirectory
39
39
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  AST-based Python style checks for code quality enforcement.
4
4
 
5
+ The checks live in `python_style_checks.py`; the shared source-line splitting and function-discovery helpers they build on live in `python_style_helpers.py`.
6
+
5
7
  ## Checks Implemented
6
8
 
7
9
  1. **Imports at top** - All import statements must be at the top of the file