claude-dev-env 1.0.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 (215) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/agents/agent-writer.md +157 -0
  4. package/agents/clasp-deployment-orchestrator.md +609 -0
  5. package/agents/clean-coder.md +295 -0
  6. package/agents/code-quality-agent.md +40 -0
  7. package/agents/code-standards-agent.md +93 -0
  8. package/agents/config-centralizer.md +686 -0
  9. package/agents/config-extraction-agent.md +225 -0
  10. package/agents/doc-orchestrator.md +47 -0
  11. package/agents/docs-agent.md +112 -0
  12. package/agents/docx-agent.md +211 -0
  13. package/agents/git-commit-crafter.md +100 -0
  14. package/agents/magic-value-eliminator-agent.md +72 -0
  15. package/agents/mandatory-agent-workflow-agent.md +88 -0
  16. package/agents/parallel-workflow-coordinator.md +779 -0
  17. package/agents/pdf-agent.md +302 -0
  18. package/agents/plan-executor.md +226 -0
  19. package/agents/pr-description-writer.md +87 -0
  20. package/agents/project-context-loader.md +238 -0
  21. package/agents/project-docs-analyzer.md +54 -0
  22. package/agents/project-structure-organizer-agent.md +72 -0
  23. package/agents/readability-review-agent.md +76 -0
  24. package/agents/refactoring-specialist.md +69 -0
  25. package/agents/right-sized-engineer.md +129 -0
  26. package/agents/session-continuity-manager.md +53 -0
  27. package/agents/skill-to-agent-converter.md +371 -0
  28. package/agents/skill-writer-agent.md +470 -0
  29. package/agents/stub-detector-agent.md +140 -0
  30. package/agents/tdd-test-writer.md +62 -0
  31. package/agents/test-data-builder.md +68 -0
  32. package/agents/tooling-builder.md +78 -0
  33. package/agents/user-docs-writer.md +67 -0
  34. package/agents/validation-expert.md +71 -0
  35. package/agents/workflow-visual-documenter.md +82 -0
  36. package/agents/xlsx-agent.md +169 -0
  37. package/bin/install.mjs +256 -0
  38. package/commands/commit.md +28 -0
  39. package/commands/docupdate.md +322 -0
  40. package/commands/implement.md +102 -0
  41. package/commands/initialize.md +91 -0
  42. package/commands/plan.md +63 -0
  43. package/commands/pr-comments.md +47 -0
  44. package/commands/readability-review.md +20 -0
  45. package/commands/review-plan.md +7 -0
  46. package/commands/right-size.md +15 -0
  47. package/commands/stubcheck.md +89 -0
  48. package/commands/sum.md +30 -0
  49. package/docs/CODE_RULES.md +186 -0
  50. package/docs/DJANGO_PATTERNS.md +80 -0
  51. package/docs/REACT_PATTERNS.md +185 -0
  52. package/docs/TEST_QUALITY.md +104 -0
  53. package/hooks/advisory/migration-safety-advisor.py +49 -0
  54. package/hooks/advisory/refactor-guard.py +205 -0
  55. package/hooks/blocking/block-main-commit.py +168 -0
  56. package/hooks/blocking/code-rules-enforcer.py +549 -0
  57. package/hooks/blocking/destructive-command-blocker.py +107 -0
  58. package/hooks/blocking/docker-settings-guard.py +44 -0
  59. package/hooks/blocking/hedging-language-blocker.py +130 -0
  60. package/hooks/blocking/parallel-task-blocker.py +69 -0
  61. package/hooks/blocking/pr-description-enforcer.py +87 -0
  62. package/hooks/blocking/pyautogui-scroll-blocker.py +74 -0
  63. package/hooks/blocking/sensitive-file-protector.py +70 -0
  64. package/hooks/blocking/tdd-enforcer.py +62 -0
  65. package/hooks/blocking/test-preflight-check.py +343 -0
  66. package/hooks/blocking/write-existing-file-blocker.py +63 -0
  67. package/hooks/git-hooks/post-commit.py +103 -0
  68. package/hooks/github-action/test_workflow.py +33 -0
  69. package/hooks/hooks.json +246 -0
  70. package/hooks/lifecycle/config-change-guard.py +84 -0
  71. package/hooks/lifecycle/session-end-cleanup.py +59 -0
  72. package/hooks/notification/attention-needed-notify.py +63 -0
  73. package/hooks/notification/claude-notification-handler.py +59 -0
  74. package/hooks/notification/notification_utils.py +206 -0
  75. package/hooks/rewrite-plugin-paths.py +116 -0
  76. package/hooks/session/bulk-edit-reminder.py +30 -0
  77. package/hooks/session/code-rules-reminder.py +97 -0
  78. package/hooks/session/compact-context-reinject.py +39 -0
  79. package/hooks/session/hook-structure-context.py +140 -0
  80. package/hooks/session/plugin-data-dir-cleanup.py +39 -0
  81. package/hooks/validation/code-style-validator.py +145 -0
  82. package/hooks/validation/e2e-test-validator.py +142 -0
  83. package/hooks/validation/hook-format-validator.py +66 -0
  84. package/hooks/validation/mypy_validator.py +180 -0
  85. package/hooks/validators/README.md +125 -0
  86. package/hooks/validators/VALIDATION_REPORT.md +287 -0
  87. package/hooks/validators/__init__.py +19 -0
  88. package/hooks/validators/abbreviation_checks.py +82 -0
  89. package/hooks/validators/code_quality_checks.py +133 -0
  90. package/hooks/validators/comment_checks.py +188 -0
  91. package/hooks/validators/file_structure_checks.py +182 -0
  92. package/hooks/validators/git_checks.py +107 -0
  93. package/hooks/validators/health_check.py +214 -0
  94. package/hooks/validators/magic_value_checks.py +81 -0
  95. package/hooks/validators/mypy_integration.py +52 -0
  96. package/hooks/validators/output_formatter.py +266 -0
  97. package/hooks/validators/pr_reference_checks.py +72 -0
  98. package/hooks/validators/python_antipattern_checks.py +110 -0
  99. package/hooks/validators/python_style_checks.py +364 -0
  100. package/hooks/validators/react_checks.py +90 -0
  101. package/hooks/validators/ruff_integration.py +80 -0
  102. package/hooks/validators/run_all_validators.py +772 -0
  103. package/hooks/validators/security_checks.py +135 -0
  104. package/hooks/validators/test_abbreviation_checks.py +76 -0
  105. package/hooks/validators/test_bad.tsx +7 -0
  106. package/hooks/validators/test_code_quality_checks.py +129 -0
  107. package/hooks/validators/test_file_structure_checks.py +307 -0
  108. package/hooks/validators/test_files/01_basic_component.tsx +10 -0
  109. package/hooks/validators/test_files/02_component_without_react.tsx +10 -0
  110. package/hooks/validators/test_files/03_pure_component.tsx +10 -0
  111. package/hooks/validators/test_files/04_pure_component_import.tsx +10 -0
  112. package/hooks/validators/test_files/05_typescript_generics.tsx +14 -0
  113. package/hooks/validators/test_files/06_typescript_two_generics.tsx +18 -0
  114. package/hooks/validators/test_files/07_multiline_declaration.tsx +11 -0
  115. package/hooks/validators/test_files/08_error_boundary_valid.tsx +14 -0
  116. package/hooks/validators/test_files/09_error_boundary_with_other_class.tsx +20 -0
  117. package/hooks/validators/test_files/10_inheritance_chain.tsx +16 -0
  118. package/hooks/validators/test_files/11_ts_file.ts +10 -0
  119. package/hooks/validators/test_files/12_non_react_class.tsx +14 -0
  120. package/hooks/validators/test_files/13_functional_component.tsx +8 -0
  121. package/hooks/validators/test_files/14_indented_class.tsx +13 -0
  122. package/hooks/validators/test_files/15_getDerivedStateFromError.tsx +14 -0
  123. package/hooks/validators/test_files/16_mixed_components.tsx +20 -0
  124. package/hooks/validators/test_files/EXECUTIVE_SUMMARY.md +175 -0
  125. package/hooks/validators/test_files/TEST_RESULTS_TABLE.txt +60 -0
  126. package/hooks/validators/test_files/VALIDATION_REPORT.md +201 -0
  127. package/hooks/validators/test_files/async_views.py +23 -0
  128. package/hooks/validators/test_files/async_with_imports.py +14 -0
  129. package/hooks/validators/test_files/bad_inline_imports.py +37 -0
  130. package/hooks/validators/test_files/management/commands/cmd_01_no_debug_check.py +10 -0
  131. package/hooks/validators/test_files/management/commands/cmd_02_proper_debug_check.py +14 -0
  132. package/hooks/validators/test_files/management/commands/cmd_03_debug_check_with_return.py +14 -0
  133. package/hooks/validators/test_files/management/commands/cmd_04_imported_DEBUG.py +14 -0
  134. package/hooks/validators/test_files/management/commands/cmd_05_debug_check_in_helper.py +16 -0
  135. package/hooks/validators/test_files/management/commands/cmd_06_debug_check_late.py +22 -0
  136. package/hooks/validators/test_files/management/commands/cmd_07_positive_debug_check.py +15 -0
  137. package/hooks/validators/test_files/management/commands/cmd_08_debug_with_and.py +14 -0
  138. package/hooks/validators/test_files/not_management_command.py +10 -0
  139. package/hooks/validators/test_files/skip_decorators/test_01_simple_skip.py +8 -0
  140. package/hooks/validators/test_files/skip_decorators/test_02_pytest_skipif.py +8 -0
  141. package/hooks/validators/test_files/skip_decorators/test_03_unittest_skipIf.py +8 -0
  142. package/hooks/validators/test_files/skip_decorators/test_04_skip_with_parens.py +8 -0
  143. package/hooks/validators/test_files/skip_decorators/test_05_xfail.py +7 -0
  144. package/hooks/validators/test_files/skip_decorators/test_06_custom_skip.py +11 -0
  145. package/hooks/validators/test_files/skip_decorators/test_07_capital_Skip.py +8 -0
  146. package/hooks/validators/test_files/skip_decorators/test_08_skipUnless.py +7 -0
  147. package/hooks/validators/test_files/skip_decorators/test_09_pytest_mark_skip_simple.py +7 -0
  148. package/hooks/validators/test_files/test_async_functions.py +45 -0
  149. package/hooks/validators/test_files/test_purecomponent/PureComponentExample.tsx +7 -0
  150. package/hooks/validators/test_files/test_purecomponent/ReactPureComponentExample.tsx +7 -0
  151. package/hooks/validators/test_git_checks.py +295 -0
  152. package/hooks/validators/test_good.tsx +5 -0
  153. package/hooks/validators/test_health_check.py +57 -0
  154. package/hooks/validators/test_magic_value_checks.py +63 -0
  155. package/hooks/validators/test_mypy_integration.py +27 -0
  156. package/hooks/validators/test_output_formatter.py +150 -0
  157. package/hooks/validators/test_pr_reference_checks.py +41 -0
  158. package/hooks/validators/test_python_antipattern_checks.py +113 -0
  159. package/hooks/validators/test_python_style_checks.py +439 -0
  160. package/hooks/validators/test_react_checks.py +213 -0
  161. package/hooks/validators/test_results.txt +25 -0
  162. package/hooks/validators/test_ruff_integration.py +27 -0
  163. package/hooks/validators/test_run_all_validators.py +228 -0
  164. package/hooks/validators/test_run_all_validators_integration.py +48 -0
  165. package/hooks/validators/test_safety_checks.py +243 -0
  166. package/hooks/validators/test_security_checks.py +105 -0
  167. package/hooks/validators/test_test_safety_checks.py +321 -0
  168. package/hooks/validators/test_todo_checks.py +39 -0
  169. package/hooks/validators/test_type_safety_checks.py +85 -0
  170. package/hooks/validators/test_useless_test_checks.py +55 -0
  171. package/hooks/validators/test_validator_base.py +26 -0
  172. package/hooks/validators/test_verify_paths.py +34 -0
  173. package/hooks/validators/todo_checks.py +59 -0
  174. package/hooks/validators/type_safety_checks.py +101 -0
  175. package/hooks/validators/useless_test_checks.py +92 -0
  176. package/hooks/validators/validator_base.py +19 -0
  177. package/hooks/validators/verify_paths.py +57 -0
  178. package/hooks/workflow/auto-formatter.py +114 -0
  179. package/hooks/workflow/investigation-tracker-reset.py +46 -0
  180. package/package.json +30 -0
  181. package/rules/agent-spawn-protocol.md +47 -0
  182. package/rules/cleanup-temp-files.md +27 -0
  183. package/rules/code-reviews.md +11 -0
  184. package/rules/code-standards.md +43 -0
  185. package/rules/conservative-action.md +20 -0
  186. package/rules/context7.md +12 -0
  187. package/rules/explore-thoroughly.md +27 -0
  188. package/rules/git-workflow.md +42 -0
  189. package/rules/parallel-tools.md +23 -0
  190. package/rules/research-mode.md +23 -0
  191. package/rules/right-sized-engineering.md +28 -0
  192. package/rules/tdd.md +7 -0
  193. package/rules/testing.md +12 -0
  194. package/skills/agent-prompt/SKILL.md +102 -0
  195. package/skills/anthropic-plan/SKILL.md +107 -0
  196. package/skills/everything-search/SKILL.md +144 -0
  197. package/skills/ingest/SKILL.md +40 -0
  198. package/skills/npm-creator/SKILL.md +183 -0
  199. package/skills/pr-review-responder/EXAMPLES.md +590 -0
  200. package/skills/pr-review-responder/PRINCIPLES.md +539 -0
  201. package/skills/pr-review-responder/README.md +209 -0
  202. package/skills/pr-review-responder/SKILL.md +202 -0
  203. package/skills/pr-review-responder/TESTING.md +407 -0
  204. package/skills/pr-review-responder/scripts/respond_to_reviews.py +376 -0
  205. package/skills/pr-review-responder/update_skill.py +297 -0
  206. package/skills/prompt-generator/REFERENCE.md +150 -0
  207. package/skills/prompt-generator/SKILL.md +154 -0
  208. package/skills/readability-review/SKILL.md +127 -0
  209. package/skills/recall/SKILL.md +27 -0
  210. package/skills/remember/SKILL.md +63 -0
  211. package/skills/rule-audit/SKILL.md +307 -0
  212. package/skills/rule-creator/SKILL.md +150 -0
  213. package/skills/skill-writer/REFERENCE.md +246 -0
  214. package/skills/skill-writer/SKILL.md +270 -0
  215. package/skills/tdd-team/SKILL.md +128 -0
@@ -0,0 +1,549 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CODE_RULES.md enforcer - blocks code that violates mandatory rules.
4
+
5
+ Checks (all blocking):
6
+ 1. No comments (# or // in code, excluding shebangs/type: ignore)
7
+ 2. Imports at top (no imports inside functions)
8
+ 3. Logging f-strings (log_* calls must use format args)
9
+ 4. File line count (>400 blocks)
10
+ 5. Windows API None (win32gui calls with None parameter)
11
+ 6. Magic values (literals in function bodies)
12
+ 7. E2E test naming (no online/offline in test names)
13
+ 8. Constants outside config (UPPER_SNAKE = in non-config files)
14
+ """
15
+ import json
16
+ import re
17
+ import sys
18
+ from typing import Optional
19
+
20
+ PYTHON_EXTENSIONS = {".py"}
21
+ JAVASCRIPT_EXTENSIONS = {".js", ".ts", ".tsx", ".jsx"}
22
+ ALL_CODE_EXTENSIONS = PYTHON_EXTENSIONS | JAVASCRIPT_EXTENSIONS
23
+
24
+ CONFIG_PATH_PATTERNS = {"config/", "config\\", "/config.", "\\config.", "settings.py"}
25
+ TEST_PATH_PATTERNS = {"test_", "_test.", ".spec.", "conftest", "/tests/", "\\tests\\", "/tests.py", "\\tests.py"}
26
+ HOOK_INFRASTRUCTURE_PATTERNS = {"/.claude/hooks/", "\\.claude\\hooks\\", "\\.claude/hooks/"}
27
+ WORKFLOW_REGISTRY_PATTERNS = {"/workflow/", "\\workflow\\", "_tab.py", "/states.py", "\\states.py", "/modules.py", "\\modules.py"}
28
+ MIGRATION_PATH_PATTERNS = {"/migrations/", "\\migrations\\"}
29
+
30
+
31
+ def get_file_extension(file_path: str) -> str:
32
+ """Extract lowercase file extension."""
33
+ dot_index = file_path.rfind(".")
34
+ if dot_index == -1:
35
+ return ""
36
+ return file_path[dot_index:].lower()
37
+
38
+
39
+ def is_hook_infrastructure(file_path: str) -> bool:
40
+ """Check if file is a Claude Code hook (standalone infrastructure, not project code)."""
41
+ path_lower = file_path.lower().replace("\\", "/")
42
+ return any(pattern.replace("\\", "/") in path_lower for pattern in HOOK_INFRASTRUCTURE_PATTERNS)
43
+
44
+
45
+ def is_config_file(file_path: str) -> bool:
46
+ """Check if file is in a config directory or is a config file."""
47
+ path_lower = file_path.lower()
48
+ return any(pattern in path_lower for pattern in CONFIG_PATH_PATTERNS)
49
+
50
+
51
+ def is_test_file(file_path: str) -> bool:
52
+ """Check if file is a test file."""
53
+ path_lower = file_path.lower()
54
+ return any(pattern in path_lower for pattern in TEST_PATH_PATTERNS)
55
+
56
+
57
+ def is_workflow_registry_file(file_path: str) -> bool:
58
+ """Check if file is a workflow state/module registry file.
59
+
60
+ Workflow tab files and state/module registry files use UPPER_SNAKE naming
61
+ for StateDefinition and WorkflowModule instances by architectural convention.
62
+ These are module-level singletons, not misplaced literal constants.
63
+ """
64
+ path_lower = file_path.lower().replace("\\", "/")
65
+ return any(pattern.replace("\\", "/") in path_lower for pattern in WORKFLOW_REGISTRY_PATTERNS)
66
+
67
+
68
+ def is_spec_file(file_path: str) -> bool:
69
+ """Check if file is an E2E spec file."""
70
+ return ".spec." in file_path.lower()
71
+
72
+
73
+ def check_comments_python(content: str) -> list[str]:
74
+ """Check for comments in Python code."""
75
+ issues = []
76
+ lines = content.split("\n")
77
+
78
+ for line_number, line in enumerate(lines, 1):
79
+ stripped = line.strip()
80
+
81
+ if not stripped:
82
+ continue
83
+
84
+ if stripped.startswith("#!"):
85
+ continue
86
+
87
+ if stripped.startswith("# type:"):
88
+ continue
89
+
90
+ if stripped.startswith("# noqa"):
91
+ continue
92
+
93
+ if stripped.startswith("# pylint:"):
94
+ continue
95
+
96
+ if stripped.startswith("# pragma:"):
97
+ continue
98
+
99
+ comment_index = line.find("#")
100
+ if comment_index != -1:
101
+ before_comment = line[:comment_index]
102
+ if not before_comment.strip().startswith(("'", '"')):
103
+ in_string = False
104
+ quote_char = None
105
+ for i, char in enumerate(before_comment):
106
+ if char in ("'", '"') and (i == 0 or before_comment[i - 1] != "\\"):
107
+ if not in_string:
108
+ in_string = True
109
+ quote_char = char
110
+ elif char == quote_char:
111
+ in_string = False
112
+
113
+ if not in_string:
114
+ comment_text = line[comment_index + 1 :].strip()
115
+ if comment_text and not comment_text.startswith(("type:", "noqa", "pylint:", "pragma:")):
116
+ issues.append(f"Line {line_number}: Comment found - refactor to self-documenting code")
117
+
118
+ if len(issues) >= 3:
119
+ break
120
+
121
+ return issues
122
+
123
+
124
+ def check_comments_javascript(content: str) -> list[str]:
125
+ """Check for comments in JavaScript/TypeScript code."""
126
+ issues = []
127
+ lines = content.split("\n")
128
+ in_multiline_comment = False
129
+
130
+ for line_number, line in enumerate(lines, 1):
131
+ stripped = line.strip()
132
+
133
+ if not stripped:
134
+ continue
135
+
136
+ if in_multiline_comment:
137
+ if "*/" in stripped:
138
+ in_multiline_comment = False
139
+ continue
140
+
141
+ if stripped.startswith("/*"):
142
+ in_multiline_comment = "*/" not in stripped
143
+ if not stripped.startswith("/**"):
144
+ issues.append(f"Line {line_number}: Block comment found - refactor to self-documenting code")
145
+ continue
146
+
147
+ if stripped.startswith("//"):
148
+ if not stripped.startswith(("// @ts-", "// eslint-", "// prettier-", "/// ")):
149
+ issues.append(f"Line {line_number}: Comment found - refactor to self-documenting code")
150
+
151
+ if len(issues) >= 3:
152
+ break
153
+
154
+ return issues
155
+
156
+
157
+ def extract_comment_texts(content: str, file_path: str) -> tuple[set[str], set[str]]:
158
+ """Extract normalized comment text strings from content for comparison.
159
+
160
+ Returns:
161
+ Tuple of (inline_comments, standalone_comments).
162
+ Inline comments appear after code on the same line.
163
+ Standalone comments are lines where the entire line is a comment.
164
+ """
165
+ extension = get_file_extension(file_path)
166
+ inline_comments: set[str] = set()
167
+ standalone_comments: set[str] = set()
168
+ if not content:
169
+ return inline_comments, standalone_comments
170
+
171
+ lines = content.split("\n")
172
+
173
+ if extension in PYTHON_EXTENSIONS:
174
+ for line in lines:
175
+ stripped = line.strip()
176
+ if not stripped:
177
+ continue
178
+ if stripped.startswith("#"):
179
+ if stripped.startswith(("#!", "# type:", "# noqa", "# pylint:", "# pragma:")):
180
+ continue
181
+ standalone_comments.add(stripped)
182
+ elif "#" in line:
183
+ comment_index = line.find("#")
184
+ before_comment = line[:comment_index]
185
+ if not before_comment.strip().startswith(("'", '"')):
186
+ in_string = False
187
+ quote_char = None
188
+ for i, char in enumerate(before_comment):
189
+ if char in ("'", '"') and (i == 0 or before_comment[i - 1] != "\\"):
190
+ if not in_string:
191
+ in_string = True
192
+ quote_char = char
193
+ elif char == quote_char:
194
+ in_string = False
195
+ if not in_string:
196
+ comment_text = line[comment_index + 1 :].strip()
197
+ if comment_text and not comment_text.startswith(("type:", "noqa", "pylint:", "pragma:")):
198
+ inline_comments.add(line[comment_index:].strip())
199
+
200
+ elif extension in JAVASCRIPT_EXTENSIONS:
201
+ in_multiline = False
202
+ for line in lines:
203
+ stripped = line.strip()
204
+ if not stripped:
205
+ continue
206
+ if in_multiline:
207
+ if "*/" in stripped:
208
+ in_multiline = False
209
+ continue
210
+ if stripped.startswith("/*"):
211
+ in_multiline = "*/" not in stripped
212
+ if not stripped.startswith("/**"):
213
+ standalone_comments.add(stripped)
214
+ continue
215
+ if stripped.startswith("//"):
216
+ if not stripped.startswith(("// @ts-", "// eslint-", "// prettier-", "/// ")):
217
+ standalone_comments.add(stripped)
218
+ elif "//" in line:
219
+ before_slash = line[:line.index("//")]
220
+ if before_slash.strip():
221
+ inline_comments.add(stripped[stripped.index("//"):])
222
+
223
+ return inline_comments, standalone_comments
224
+
225
+
226
+ def check_comment_changes(old_content: str, new_content: str, file_path: str) -> list[str]:
227
+ """Check for comment additions or removals between old and new content.
228
+
229
+ Inline comments (after code on same line): BLOCK when added.
230
+ Standalone comment lines: NUDGE (print advisory) when added.
231
+ Existing comments being removed: BLOCK (comment preservation principle).
232
+ """
233
+ issues: list[str] = []
234
+
235
+ old_inline, old_standalone = extract_comment_texts(old_content, file_path)
236
+ new_inline, new_standalone = extract_comment_texts(new_content, file_path)
237
+
238
+ added_inline = new_inline - old_inline
239
+ if added_inline:
240
+ sample = next(iter(added_inline))
241
+ issues.append(f"Inline comment added: {sample[:60]} - refactor to self-documenting code")
242
+
243
+ added_standalone = new_standalone - old_standalone
244
+ if added_standalone:
245
+ sample = next(iter(added_standalone))
246
+ print(f"[CODE_RULES advisory] Standalone comment added: {sample[:60]} - prefer self-documenting code", file=sys.stderr)
247
+
248
+ all_old = old_inline | old_standalone
249
+ all_new = new_inline | new_standalone
250
+ removed_comments = all_old - all_new
251
+ if removed_comments:
252
+ old_line_count = len([line for line in old_content.split("\n") if line.strip()])
253
+ new_line_count = len([line for line in new_content.split("\n") if line.strip()])
254
+ code_was_removed = new_line_count < old_line_count - len(removed_comments)
255
+ if not code_was_removed:
256
+ sample = next(iter(removed_comments))
257
+ issues.append(f"Existing comment removed: {sample[:60]} - NEVER delete existing comments")
258
+
259
+ return issues
260
+
261
+
262
+ def check_imports_at_top(content: str) -> list[str]:
263
+ """Check for imports inside functions (Python only)."""
264
+ issues = []
265
+ lines = content.split("\n")
266
+ inside_function = False
267
+ function_indent = 0
268
+
269
+ for line_number, line in enumerate(lines, 1):
270
+ stripped = line.strip()
271
+
272
+ if not stripped:
273
+ continue
274
+
275
+ func_match = re.match(r"^(\s*)(async\s+)?def\s+\w+", line)
276
+ if func_match:
277
+ inside_function = True
278
+ function_indent = len(func_match.group(1)) if func_match.group(1) else 0
279
+ continue
280
+
281
+ if inside_function:
282
+ current_indent = len(line) - len(line.lstrip())
283
+ if current_indent <= function_indent and stripped and not stripped.startswith(("#", "@", ")")):
284
+ inside_function = False
285
+
286
+ if inside_function:
287
+ if stripped.startswith(("import ", "from ")) and "TYPE_CHECKING" not in content[:500]:
288
+ issues.append(f"Line {line_number}: Import inside function - move to top of file")
289
+
290
+ if len(issues) >= 3:
291
+ break
292
+
293
+ return issues
294
+
295
+
296
+ def check_logging_fstrings(content: str) -> list[str]:
297
+ """Check for f-strings in logging calls."""
298
+ issues = []
299
+ pattern = re.compile(r'\blog_(debug|info|warning|error|critical)\s*\(\s*f["\']')
300
+
301
+ for line_number, line in enumerate(content.split("\n"), 1):
302
+ if pattern.search(line):
303
+ issues.append(f"Line {line_number}: f-string in log call - use format args instead")
304
+
305
+ if len(issues) >= 3:
306
+ break
307
+
308
+ return issues
309
+
310
+
311
+ def check_file_line_count(content: str) -> list[str]:
312
+ """Check file line count."""
313
+ line_count = content.count("\n") + 1
314
+ if line_count > 400:
315
+ return [f"File has {line_count} lines (max 400) - split into smaller modules"]
316
+ return []
317
+
318
+
319
+ def check_windows_api_none(content: str) -> list[str]:
320
+ """Check for win32gui calls with None parameter."""
321
+ issues = []
322
+ pattern = re.compile(r"win32gui\.\w+\s*\([^)]*,\s*None\s*\)")
323
+
324
+ for line_number, line in enumerate(content.split("\n"), 1):
325
+ if pattern.search(line):
326
+ issues.append(f"Line {line_number}: win32gui call with None - use 0 for unused int params")
327
+
328
+ if len(issues) >= 3:
329
+ break
330
+
331
+ return issues
332
+
333
+
334
+ def check_magic_values(content: str, file_path: str) -> list[str]:
335
+ """Check for magic values in function bodies."""
336
+ if is_config_file(file_path) or is_test_file(file_path):
337
+ return []
338
+
339
+ issues = []
340
+ lines = content.split("\n")
341
+ inside_function = False
342
+
343
+ number_pattern = re.compile(r"(?<![.\w])(\d+\.?\d*)(?![.\w])")
344
+ allowed_numbers = {"0", "1", "-1", "0.0", "1.0", "2", "100"}
345
+
346
+ for line_number, line in enumerate(lines, 1):
347
+ stripped = line.strip()
348
+
349
+ if not stripped:
350
+ continue
351
+
352
+ if re.match(r"^(async\s+)?def\s+\w+", stripped):
353
+ inside_function = True
354
+ continue
355
+
356
+ if re.match(r"^class\s+\w+", stripped):
357
+ inside_function = False
358
+ continue
359
+
360
+ if inside_function:
361
+ if "=" in stripped and stripped.split("=")[0].strip().isupper():
362
+ continue
363
+
364
+ if stripped.startswith(("return", "yield", "raise")):
365
+ continue
366
+
367
+ numbers_found = number_pattern.findall(stripped)
368
+ for number in numbers_found:
369
+ if number not in allowed_numbers:
370
+ if "range(" in stripped or "enumerate(" in stripped:
371
+ continue
372
+ if "[" in stripped and "]" in stripped:
373
+ continue
374
+ issues.append(f"Line {line_number}: Magic value {number} - extract to named constant")
375
+ break
376
+
377
+ if len(issues) >= 3:
378
+ break
379
+
380
+ return issues
381
+
382
+
383
+ def check_e2e_test_naming(content: str, file_path: str) -> list[str]:
384
+ """Check for online/offline in test names (spec files only)."""
385
+ if not is_spec_file(file_path):
386
+ return []
387
+
388
+ issues = []
389
+ pattern = re.compile(r'(test|it|describe)\s*\(\s*["\'][^"\']*\b(online|offline)\b[^"\']*["\']', re.IGNORECASE)
390
+
391
+ for line_number, line in enumerate(content.split("\n"), 1):
392
+ if pattern.search(line):
393
+ issues.append(f"Line {line_number}: Test name contains online/offline - file scope defines this")
394
+
395
+ if len(issues) >= 3:
396
+ break
397
+
398
+ return issues
399
+
400
+
401
+ def is_migration_file(file_path: str) -> bool:
402
+ """Check if file is a Django migration (must be self-contained)."""
403
+ path_lower = file_path.lower().replace("\\", "/")
404
+ return any(pattern.replace("\\", "/") in path_lower for pattern in MIGRATION_PATH_PATTERNS)
405
+
406
+
407
+ def check_constants_outside_config(content: str, file_path: str) -> list[str]:
408
+ """Check for UPPER_SNAKE constants defined outside config files."""
409
+ if is_config_file(file_path):
410
+ return []
411
+
412
+ if is_test_file(file_path):
413
+ return []
414
+
415
+ if is_workflow_registry_file(file_path):
416
+ return []
417
+
418
+ if is_migration_file(file_path):
419
+ return []
420
+
421
+ issues = []
422
+ lines = content.split("\n")
423
+ inside_function = False
424
+ inside_class = False
425
+
426
+ constant_pattern = re.compile(r"^([A-Z][A-Z0-9_]{2,})\s*=\s*[^=]")
427
+
428
+ for line_number, line in enumerate(lines, 1):
429
+ stripped = line.strip()
430
+
431
+ if not stripped:
432
+ continue
433
+
434
+ if re.match(r"^(async\s+)?def\s+\w+", stripped):
435
+ inside_function = True
436
+ continue
437
+
438
+ if re.match(r"^class\s+\w+", stripped):
439
+ inside_class = True
440
+ inside_function = False
441
+ continue
442
+
443
+ indent = len(line) - len(line.lstrip())
444
+ if indent == 0 and stripped and not stripped.startswith(("#", "@", ")")):
445
+ inside_function = False
446
+ inside_class = False
447
+
448
+ if not inside_function and not inside_class:
449
+ match = constant_pattern.match(stripped)
450
+ if match:
451
+ constant_name = match.group(1)
452
+ if constant_name not in ("__all__",):
453
+ issues.append(f"Line {line_number}: Constant {constant_name} - move to config/")
454
+
455
+ if len(issues) >= 3:
456
+ break
457
+
458
+ return issues
459
+
460
+
461
+ def validate_content(content: str, file_path: str, old_content: str = "") -> list[str]:
462
+ """Run all applicable validators on content.
463
+
464
+ Args:
465
+ content: The new content being written.
466
+ file_path: Path to the file.
467
+ old_content: Previous content (old_string for Edit, existing file for Write).
468
+ Used to detect comment additions/removals instead of flagging all comments.
469
+ """
470
+ extension = get_file_extension(file_path)
471
+ all_issues = []
472
+
473
+ if extension in PYTHON_EXTENSIONS:
474
+ if not is_test_file(file_path):
475
+ all_issues.extend(check_comment_changes(old_content, content, file_path))
476
+ all_issues.extend(check_imports_at_top(content))
477
+ all_issues.extend(check_logging_fstrings(content))
478
+ all_issues.extend(check_windows_api_none(content))
479
+ all_issues.extend(check_magic_values(content, file_path))
480
+ all_issues.extend(check_constants_outside_config(content, file_path))
481
+
482
+ elif extension in JAVASCRIPT_EXTENSIONS:
483
+ if not is_test_file(file_path):
484
+ all_issues.extend(check_comment_changes(old_content, content, file_path))
485
+ all_issues.extend(check_e2e_test_naming(content, file_path))
486
+
487
+ if extension in ALL_CODE_EXTENSIONS:
488
+ all_issues.extend(check_file_line_count(content))
489
+
490
+ return all_issues
491
+
492
+
493
+ def main() -> None:
494
+ try:
495
+ input_data = json.load(sys.stdin)
496
+ except json.JSONDecodeError:
497
+ sys.exit(0)
498
+
499
+ tool_name = input_data.get("tool_name", "")
500
+ tool_input = input_data.get("tool_input", {})
501
+ file_path = tool_input.get("file_path", "")
502
+
503
+ if not file_path:
504
+ sys.exit(0)
505
+
506
+ if is_hook_infrastructure(file_path):
507
+ sys.exit(0)
508
+
509
+ extension = get_file_extension(file_path)
510
+ if extension not in ALL_CODE_EXTENSIONS:
511
+ sys.exit(0)
512
+
513
+ old_content = ""
514
+ if tool_name == "Edit":
515
+ content = tool_input.get("new_string", "")
516
+ old_content = tool_input.get("old_string", "")
517
+ else:
518
+ content = tool_input.get("content", "") or tool_input.get("new_string", "")
519
+ try:
520
+ with open(file_path, "r", encoding="utf-8") as existing_file:
521
+ old_content = existing_file.read()
522
+ except (FileNotFoundError, OSError, UnicodeDecodeError):
523
+ old_content = ""
524
+
525
+ if old_content:
526
+ sys.exit(0)
527
+
528
+ if not content:
529
+ sys.exit(0)
530
+
531
+ issues = validate_content(content, file_path, old_content)
532
+
533
+ if issues:
534
+ issue_list = "; ".join(issues[:10])
535
+ result = {
536
+ "hookSpecificOutput": {
537
+ "hookEventName": "PreToolUse",
538
+ "permissionDecision": "deny",
539
+ "permissionDecisionReason": f"BLOCKED: [CODE_RULES] {len(issues)} violation(s): {issue_list}",
540
+ }
541
+ }
542
+ print(json.dumps(result))
543
+ sys.stdout.flush()
544
+
545
+ sys.exit(0)
546
+
547
+
548
+ if __name__ == "__main__":
549
+ main()
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import os
4
+ import re
5
+ import sys
6
+
7
+ CLAUDE_DIRECTORY_PATH = os.path.normpath(os.path.expanduser("~/.claude"))
8
+
9
+ # Projects where git reset --hard is explicitly allowed by the user.
10
+ # Add your own project paths here, e.g.:
11
+ # os.path.normpath("C:/Users/you/your-project"),
12
+ ALLOW_GIT_RESET_HARD_PROJECTS: list[str] = []
13
+
14
+ DESTRUCTIVE_BASH_PATTERNS = [
15
+ (re.compile(r'\brm\s+-[a-z]*r[a-z]*f|\brm\s+-[a-z]*f[a-z]*r', re.IGNORECASE), "rm -rf (destructive recursive forced delete)"),
16
+ (re.compile(r'\brm\s+--recursive\b.*--force\b|\brm\s+--force\b.*--recursive\b', re.IGNORECASE), "rm --recursive --force (destructive recursive forced delete)"),
17
+ (re.compile(r'\brm\s+-r\s+([/~]|\.(?:\s|$)|\$HOME)', re.IGNORECASE), "rm -r on broad path (/, ~, $HOME, .)"),
18
+ (re.compile(r'\bmkfs\b', re.IGNORECASE), "mkfs (format filesystem)"),
19
+ (re.compile(r'\bdd\s+.*\bif=.*\bof=/dev/', re.IGNORECASE), "dd raw disk write"),
20
+ (re.compile(r'\bgit\s+reset\s+--hard\b', re.IGNORECASE), "git reset --hard (discards uncommitted work)"),
21
+ (re.compile(r'\bgit\s+push\s+--force(?!-with-lease)\b', re.IGNORECASE), "git push --force (rewrites remote history)"),
22
+ (re.compile(r'\bgit\s+push\s+-f\b', re.IGNORECASE), "git push -f (rewrites remote history)"),
23
+ (re.compile(r'\bgit\s+clean\s+(-fd|-df)\b', re.IGNORECASE), "git clean -fd (deletes untracked files and dirs)"),
24
+ (re.compile(r'\bgit\s+clean\s+-f\b', re.IGNORECASE), "git clean -f (deletes untracked files)"),
25
+ (re.compile(r'\bDROP\s+TABLE\b', re.IGNORECASE), "DROP TABLE (destroys database table)"),
26
+ (re.compile(r'\bDROP\s+DATABASE\b', re.IGNORECASE), "DROP DATABASE (destroys entire database)"),
27
+ (re.compile(r'\bTRUNCATE\s+TABLE\b', re.IGNORECASE), "TRUNCATE TABLE (removes all table rows)"),
28
+ (re.compile(r'\bgh\s+api\b.*/(comments|reviews)\b.*-X\s+POST', re.IGNORECASE), "gh api comment/review POST (visible to others)"),
29
+ (re.compile(r'\bgh\s+pr\s+comment\b', re.IGNORECASE), "gh pr comment (visible to others)"),
30
+ (re.compile(r'\bgh\s+pr\s+review\b', re.IGNORECASE), "gh pr review (visible to others)"),
31
+ (re.compile(r'\bgh\s+issue\s+comment\b', re.IGNORECASE), "gh issue comment (visible to others)"),
32
+ ]
33
+
34
+ def find_destructive_pattern(command: str) -> str | None:
35
+ for pattern_regex, pattern_description in DESTRUCTIVE_BASH_PATTERNS:
36
+ if pattern_regex.search(command):
37
+ return pattern_description
38
+ return None
39
+
40
+
41
+ def targets_only_claude_directory(command: str) -> bool:
42
+ """Check if rm command targets only paths under ~/.claude/."""
43
+ all_rm_target_paths = re.findall(
44
+ r'(?:rm\s+(?:-\w+\s+)*)("[^"]+"|\'[^\']+\'|\S+)',
45
+ command,
46
+ )
47
+ if not all_rm_target_paths:
48
+ return False
49
+
50
+ for each_raw_path in all_rm_target_paths:
51
+ each_stripped_path = each_raw_path.strip("\"'")
52
+ each_cleaned_path = re.split(r'[;&|]', each_stripped_path)[0]
53
+ if each_cleaned_path != each_stripped_path:
54
+ return False
55
+ each_resolved_path = os.path.normpath(os.path.expanduser(each_cleaned_path))
56
+ if not each_resolved_path.startswith(CLAUDE_DIRECTORY_PATH):
57
+ return False
58
+
59
+ return True
60
+
61
+
62
+ def main() -> None:
63
+ try:
64
+ hook_input = json.load(sys.stdin)
65
+ except json.JSONDecodeError:
66
+ sys.exit(0)
67
+
68
+ tool_name = hook_input.get("tool_name", "")
69
+ tool_input = hook_input.get("tool_input", {})
70
+
71
+ if tool_name != "Bash":
72
+ sys.exit(0)
73
+
74
+ command = tool_input.get("command", "")
75
+ matched_description = find_destructive_pattern(command)
76
+
77
+ if matched_description is not None and targets_only_claude_directory(command):
78
+ sys.exit(0)
79
+
80
+ # Allow git reset --hard in explicitly approved projects (case-insensitive for Windows drive letters)
81
+ if matched_description is not None and "git reset --hard" in matched_description:
82
+ cwd = os.path.normpath(os.getcwd()).lower()
83
+ command_lower = command.lower()
84
+ for allowed_project in ALLOW_GIT_RESET_HARD_PROJECTS:
85
+ allowed_lower = allowed_project.lower()
86
+ if cwd.startswith(allowed_lower):
87
+ sys.exit(0)
88
+ # Also check the cd target in the command itself
89
+ for path_match in re.findall(r'cd\s+"([^"]+)"', command):
90
+ if os.path.normpath(path_match).lower().startswith(allowed_lower):
91
+ sys.exit(0)
92
+
93
+ if matched_description is not None:
94
+ ask_response = {
95
+ "hookSpecificOutput": {
96
+ "hookEventName": "PreToolUse",
97
+ "permissionDecision": "ask",
98
+ "permissionDecisionReason": f"DESTRUCTIVE: {matched_description}. Requires explicit user approval."
99
+ }
100
+ }
101
+ print(json.dumps(ask_response))
102
+
103
+ sys.exit(0)
104
+
105
+
106
+ if __name__ == "__main__":
107
+ main()
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PreToolUse hook: blocks direct edits to Docker settings files.
4
+ Hooks must be added to the Windows settings.json instead.
5
+ """
6
+
7
+ import json
8
+ import sys
9
+
10
+ BLOCKED_PATHS = [
11
+ "settings-docker.json",
12
+ "settings-docker",
13
+ "docker/settings-docker.json",
14
+ ".claude/docker/settings-docker.json",
15
+ ]
16
+
17
+
18
+ def main() -> None:
19
+ try:
20
+ stdin_data = sys.stdin.read()
21
+ hook_input = json.loads(stdin_data)
22
+ tool_input = hook_input.get("tool_input", {})
23
+ file_path = tool_input.get("file_path", "")
24
+
25
+ for blocked in BLOCKED_PATHS:
26
+ if file_path.endswith(blocked):
27
+ message = "BLOCKED: Docker settings edit denied. Edit your user settings.json instead."
28
+ result = {
29
+ "hookSpecificOutput": {
30
+ "hookEventName": "PreToolUse",
31
+ "permissionDecision": "deny",
32
+ "permissionDecisionReason": message
33
+ }
34
+ }
35
+ print(json.dumps(result))
36
+ sys.exit(0)
37
+ except SystemExit:
38
+ raise
39
+ except Exception:
40
+ pass
41
+
42
+
43
+ if __name__ == "__main__":
44
+ main()