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.
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/agents/agent-writer.md +157 -0
- package/agents/clasp-deployment-orchestrator.md +609 -0
- package/agents/clean-coder.md +295 -0
- package/agents/code-quality-agent.md +40 -0
- package/agents/code-standards-agent.md +93 -0
- package/agents/config-centralizer.md +686 -0
- package/agents/config-extraction-agent.md +225 -0
- package/agents/doc-orchestrator.md +47 -0
- package/agents/docs-agent.md +112 -0
- package/agents/docx-agent.md +211 -0
- package/agents/git-commit-crafter.md +100 -0
- package/agents/magic-value-eliminator-agent.md +72 -0
- package/agents/mandatory-agent-workflow-agent.md +88 -0
- package/agents/parallel-workflow-coordinator.md +779 -0
- package/agents/pdf-agent.md +302 -0
- package/agents/plan-executor.md +226 -0
- package/agents/pr-description-writer.md +87 -0
- package/agents/project-context-loader.md +238 -0
- package/agents/project-docs-analyzer.md +54 -0
- package/agents/project-structure-organizer-agent.md +72 -0
- package/agents/readability-review-agent.md +76 -0
- package/agents/refactoring-specialist.md +69 -0
- package/agents/right-sized-engineer.md +129 -0
- package/agents/session-continuity-manager.md +53 -0
- package/agents/skill-to-agent-converter.md +371 -0
- package/agents/skill-writer-agent.md +470 -0
- package/agents/stub-detector-agent.md +140 -0
- package/agents/tdd-test-writer.md +62 -0
- package/agents/test-data-builder.md +68 -0
- package/agents/tooling-builder.md +78 -0
- package/agents/user-docs-writer.md +67 -0
- package/agents/validation-expert.md +71 -0
- package/agents/workflow-visual-documenter.md +82 -0
- package/agents/xlsx-agent.md +169 -0
- package/bin/install.mjs +256 -0
- package/commands/commit.md +28 -0
- package/commands/docupdate.md +322 -0
- package/commands/implement.md +102 -0
- package/commands/initialize.md +91 -0
- package/commands/plan.md +63 -0
- package/commands/pr-comments.md +47 -0
- package/commands/readability-review.md +20 -0
- package/commands/review-plan.md +7 -0
- package/commands/right-size.md +15 -0
- package/commands/stubcheck.md +89 -0
- package/commands/sum.md +30 -0
- package/docs/CODE_RULES.md +186 -0
- package/docs/DJANGO_PATTERNS.md +80 -0
- package/docs/REACT_PATTERNS.md +185 -0
- package/docs/TEST_QUALITY.md +104 -0
- package/hooks/advisory/migration-safety-advisor.py +49 -0
- package/hooks/advisory/refactor-guard.py +205 -0
- package/hooks/blocking/block-main-commit.py +168 -0
- package/hooks/blocking/code-rules-enforcer.py +549 -0
- package/hooks/blocking/destructive-command-blocker.py +107 -0
- package/hooks/blocking/docker-settings-guard.py +44 -0
- package/hooks/blocking/hedging-language-blocker.py +130 -0
- package/hooks/blocking/parallel-task-blocker.py +69 -0
- package/hooks/blocking/pr-description-enforcer.py +87 -0
- package/hooks/blocking/pyautogui-scroll-blocker.py +74 -0
- package/hooks/blocking/sensitive-file-protector.py +70 -0
- package/hooks/blocking/tdd-enforcer.py +62 -0
- package/hooks/blocking/test-preflight-check.py +343 -0
- package/hooks/blocking/write-existing-file-blocker.py +63 -0
- package/hooks/git-hooks/post-commit.py +103 -0
- package/hooks/github-action/test_workflow.py +33 -0
- package/hooks/hooks.json +246 -0
- package/hooks/lifecycle/config-change-guard.py +84 -0
- package/hooks/lifecycle/session-end-cleanup.py +59 -0
- package/hooks/notification/attention-needed-notify.py +63 -0
- package/hooks/notification/claude-notification-handler.py +59 -0
- package/hooks/notification/notification_utils.py +206 -0
- package/hooks/rewrite-plugin-paths.py +116 -0
- package/hooks/session/bulk-edit-reminder.py +30 -0
- package/hooks/session/code-rules-reminder.py +97 -0
- package/hooks/session/compact-context-reinject.py +39 -0
- package/hooks/session/hook-structure-context.py +140 -0
- package/hooks/session/plugin-data-dir-cleanup.py +39 -0
- package/hooks/validation/code-style-validator.py +145 -0
- package/hooks/validation/e2e-test-validator.py +142 -0
- package/hooks/validation/hook-format-validator.py +66 -0
- package/hooks/validation/mypy_validator.py +180 -0
- package/hooks/validators/README.md +125 -0
- package/hooks/validators/VALIDATION_REPORT.md +287 -0
- package/hooks/validators/__init__.py +19 -0
- package/hooks/validators/abbreviation_checks.py +82 -0
- package/hooks/validators/code_quality_checks.py +133 -0
- package/hooks/validators/comment_checks.py +188 -0
- package/hooks/validators/file_structure_checks.py +182 -0
- package/hooks/validators/git_checks.py +107 -0
- package/hooks/validators/health_check.py +214 -0
- package/hooks/validators/magic_value_checks.py +81 -0
- package/hooks/validators/mypy_integration.py +52 -0
- package/hooks/validators/output_formatter.py +266 -0
- package/hooks/validators/pr_reference_checks.py +72 -0
- package/hooks/validators/python_antipattern_checks.py +110 -0
- package/hooks/validators/python_style_checks.py +364 -0
- package/hooks/validators/react_checks.py +90 -0
- package/hooks/validators/ruff_integration.py +80 -0
- package/hooks/validators/run_all_validators.py +772 -0
- package/hooks/validators/security_checks.py +135 -0
- package/hooks/validators/test_abbreviation_checks.py +76 -0
- package/hooks/validators/test_bad.tsx +7 -0
- package/hooks/validators/test_code_quality_checks.py +129 -0
- package/hooks/validators/test_file_structure_checks.py +307 -0
- package/hooks/validators/test_files/01_basic_component.tsx +10 -0
- package/hooks/validators/test_files/02_component_without_react.tsx +10 -0
- package/hooks/validators/test_files/03_pure_component.tsx +10 -0
- package/hooks/validators/test_files/04_pure_component_import.tsx +10 -0
- package/hooks/validators/test_files/05_typescript_generics.tsx +14 -0
- package/hooks/validators/test_files/06_typescript_two_generics.tsx +18 -0
- package/hooks/validators/test_files/07_multiline_declaration.tsx +11 -0
- package/hooks/validators/test_files/08_error_boundary_valid.tsx +14 -0
- package/hooks/validators/test_files/09_error_boundary_with_other_class.tsx +20 -0
- package/hooks/validators/test_files/10_inheritance_chain.tsx +16 -0
- package/hooks/validators/test_files/11_ts_file.ts +10 -0
- package/hooks/validators/test_files/12_non_react_class.tsx +14 -0
- package/hooks/validators/test_files/13_functional_component.tsx +8 -0
- package/hooks/validators/test_files/14_indented_class.tsx +13 -0
- package/hooks/validators/test_files/15_getDerivedStateFromError.tsx +14 -0
- package/hooks/validators/test_files/16_mixed_components.tsx +20 -0
- package/hooks/validators/test_files/EXECUTIVE_SUMMARY.md +175 -0
- package/hooks/validators/test_files/TEST_RESULTS_TABLE.txt +60 -0
- package/hooks/validators/test_files/VALIDATION_REPORT.md +201 -0
- package/hooks/validators/test_files/async_views.py +23 -0
- package/hooks/validators/test_files/async_with_imports.py +14 -0
- package/hooks/validators/test_files/bad_inline_imports.py +37 -0
- package/hooks/validators/test_files/management/commands/cmd_01_no_debug_check.py +10 -0
- package/hooks/validators/test_files/management/commands/cmd_02_proper_debug_check.py +14 -0
- package/hooks/validators/test_files/management/commands/cmd_03_debug_check_with_return.py +14 -0
- package/hooks/validators/test_files/management/commands/cmd_04_imported_DEBUG.py +14 -0
- package/hooks/validators/test_files/management/commands/cmd_05_debug_check_in_helper.py +16 -0
- package/hooks/validators/test_files/management/commands/cmd_06_debug_check_late.py +22 -0
- package/hooks/validators/test_files/management/commands/cmd_07_positive_debug_check.py +15 -0
- package/hooks/validators/test_files/management/commands/cmd_08_debug_with_and.py +14 -0
- package/hooks/validators/test_files/not_management_command.py +10 -0
- package/hooks/validators/test_files/skip_decorators/test_01_simple_skip.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_02_pytest_skipif.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_03_unittest_skipIf.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_04_skip_with_parens.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_05_xfail.py +7 -0
- package/hooks/validators/test_files/skip_decorators/test_06_custom_skip.py +11 -0
- package/hooks/validators/test_files/skip_decorators/test_07_capital_Skip.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_08_skipUnless.py +7 -0
- package/hooks/validators/test_files/skip_decorators/test_09_pytest_mark_skip_simple.py +7 -0
- package/hooks/validators/test_files/test_async_functions.py +45 -0
- package/hooks/validators/test_files/test_purecomponent/PureComponentExample.tsx +7 -0
- package/hooks/validators/test_files/test_purecomponent/ReactPureComponentExample.tsx +7 -0
- package/hooks/validators/test_git_checks.py +295 -0
- package/hooks/validators/test_good.tsx +5 -0
- package/hooks/validators/test_health_check.py +57 -0
- package/hooks/validators/test_magic_value_checks.py +63 -0
- package/hooks/validators/test_mypy_integration.py +27 -0
- package/hooks/validators/test_output_formatter.py +150 -0
- package/hooks/validators/test_pr_reference_checks.py +41 -0
- package/hooks/validators/test_python_antipattern_checks.py +113 -0
- package/hooks/validators/test_python_style_checks.py +439 -0
- package/hooks/validators/test_react_checks.py +213 -0
- package/hooks/validators/test_results.txt +25 -0
- package/hooks/validators/test_ruff_integration.py +27 -0
- package/hooks/validators/test_run_all_validators.py +228 -0
- package/hooks/validators/test_run_all_validators_integration.py +48 -0
- package/hooks/validators/test_safety_checks.py +243 -0
- package/hooks/validators/test_security_checks.py +105 -0
- package/hooks/validators/test_test_safety_checks.py +321 -0
- package/hooks/validators/test_todo_checks.py +39 -0
- package/hooks/validators/test_type_safety_checks.py +85 -0
- package/hooks/validators/test_useless_test_checks.py +55 -0
- package/hooks/validators/test_validator_base.py +26 -0
- package/hooks/validators/test_verify_paths.py +34 -0
- package/hooks/validators/todo_checks.py +59 -0
- package/hooks/validators/type_safety_checks.py +101 -0
- package/hooks/validators/useless_test_checks.py +92 -0
- package/hooks/validators/validator_base.py +19 -0
- package/hooks/validators/verify_paths.py +57 -0
- package/hooks/workflow/auto-formatter.py +114 -0
- package/hooks/workflow/investigation-tracker-reset.py +46 -0
- package/package.json +30 -0
- package/rules/agent-spawn-protocol.md +47 -0
- package/rules/cleanup-temp-files.md +27 -0
- package/rules/code-reviews.md +11 -0
- package/rules/code-standards.md +43 -0
- package/rules/conservative-action.md +20 -0
- package/rules/context7.md +12 -0
- package/rules/explore-thoroughly.md +27 -0
- package/rules/git-workflow.md +42 -0
- package/rules/parallel-tools.md +23 -0
- package/rules/research-mode.md +23 -0
- package/rules/right-sized-engineering.md +28 -0
- package/rules/tdd.md +7 -0
- package/rules/testing.md +12 -0
- package/skills/agent-prompt/SKILL.md +102 -0
- package/skills/anthropic-plan/SKILL.md +107 -0
- package/skills/everything-search/SKILL.md +144 -0
- package/skills/ingest/SKILL.md +40 -0
- package/skills/npm-creator/SKILL.md +183 -0
- package/skills/pr-review-responder/EXAMPLES.md +590 -0
- package/skills/pr-review-responder/PRINCIPLES.md +539 -0
- package/skills/pr-review-responder/README.md +209 -0
- package/skills/pr-review-responder/SKILL.md +202 -0
- package/skills/pr-review-responder/TESTING.md +407 -0
- package/skills/pr-review-responder/scripts/respond_to_reviews.py +376 -0
- package/skills/pr-review-responder/update_skill.py +297 -0
- package/skills/prompt-generator/REFERENCE.md +150 -0
- package/skills/prompt-generator/SKILL.md +154 -0
- package/skills/readability-review/SKILL.md +127 -0
- package/skills/recall/SKILL.md +27 -0
- package/skills/remember/SKILL.md +63 -0
- package/skills/rule-audit/SKILL.md +307 -0
- package/skills/rule-creator/SKILL.md +150 -0
- package/skills/skill-writer/REFERENCE.md +246 -0
- package/skills/skill-writer/SKILL.md +270 -0
- package/skills/tdd-team/SKILL.md +128 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Stop hook that blocks Claude responses containing hedging language.
|
|
4
|
+
|
|
5
|
+
Words like "likely", "probably", "presumably" signal unverified claims.
|
|
6
|
+
When detected, Claude is forced to re-check and respond with verified facts.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
15
|
+
|
|
16
|
+
RESEARCH_MODE_SKILL_SEARCH_PATHS = [
|
|
17
|
+
os.path.join(PLUGIN_ROOT, "skills", "research-mode", "SKILL.md"),
|
|
18
|
+
os.path.join(os.path.expanduser("~"), ".claude", "skills", "research-mode", "SKILL.md"),
|
|
19
|
+
os.path.join(os.path.expanduser("~"), ".claude", "plugins", "marketplaces", "claude-deep-research", "skills", "research-mode", "SKILL.md"),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
HEDGING_WORDS = [
|
|
23
|
+
r"\blikely\b",
|
|
24
|
+
r"\bunlikely\b",
|
|
25
|
+
r"\bprobably\b",
|
|
26
|
+
r"\bprobable\b",
|
|
27
|
+
r"\bpresumably\b",
|
|
28
|
+
r"\bperhaps\b",
|
|
29
|
+
r"\bpossibly\b",
|
|
30
|
+
r"\bseemingly\b",
|
|
31
|
+
r"\bapparently\b",
|
|
32
|
+
r"\barguably\b",
|
|
33
|
+
r"\bsupposedly\b",
|
|
34
|
+
r"\bostensibly\b",
|
|
35
|
+
r"\bconceivably\b",
|
|
36
|
+
r"\bplausibly\b",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
HEDGING_PHRASES = [
|
|
40
|
+
r"\bmight be\b",
|
|
41
|
+
r"\bcould be\b",
|
|
42
|
+
r"\bseems to be\b",
|
|
43
|
+
r"\bappears to be\b",
|
|
44
|
+
r"\bin all likelihood\b",
|
|
45
|
+
r"\bmore likely than not\b",
|
|
46
|
+
r"\bit.s possible that\b",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
ALL_HEDGING_PATTERNS = [
|
|
50
|
+
re.compile(pattern, re.IGNORECASE) for pattern in HEDGING_WORDS + HEDGING_PHRASES
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
CODE_BLOCK_PATTERN = re.compile(r"```[\s\S]*?```", re.MULTILINE)
|
|
54
|
+
INLINE_CODE_PATTERN = re.compile(r"`[^`]+`")
|
|
55
|
+
QUOTED_BLOCK_PATTERN = re.compile(r"^>.*$", re.MULTILINE)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def strip_code_and_quotes(text: str) -> str:
|
|
59
|
+
"""Remove code blocks, inline code, and blockquotes to avoid false positives."""
|
|
60
|
+
text = CODE_BLOCK_PATTERN.sub("", text)
|
|
61
|
+
text = INLINE_CODE_PATTERN.sub("", text)
|
|
62
|
+
text = QUOTED_BLOCK_PATTERN.sub("", text)
|
|
63
|
+
return text
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def find_hedging_words(text: str) -> list[str]:
|
|
67
|
+
"""Return all hedging words/phrases found in the text."""
|
|
68
|
+
prose_text = strip_code_and_quotes(text)
|
|
69
|
+
matched_terms = []
|
|
70
|
+
|
|
71
|
+
for pattern in ALL_HEDGING_PATTERNS:
|
|
72
|
+
all_matches = pattern.findall(prose_text)
|
|
73
|
+
for each_match in all_matches:
|
|
74
|
+
normalized_term = each_match.strip().lower()
|
|
75
|
+
if normalized_term not in matched_terms:
|
|
76
|
+
matched_terms.append(normalized_term)
|
|
77
|
+
|
|
78
|
+
return matched_terms
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main() -> None:
|
|
82
|
+
try:
|
|
83
|
+
hook_input = json.load(sys.stdin)
|
|
84
|
+
except json.JSONDecodeError:
|
|
85
|
+
sys.exit(0)
|
|
86
|
+
|
|
87
|
+
if hook_input.get("stop_hook_active", False):
|
|
88
|
+
sys.exit(0)
|
|
89
|
+
|
|
90
|
+
assistant_message = hook_input.get("last_assistant_message", "")
|
|
91
|
+
|
|
92
|
+
if not assistant_message:
|
|
93
|
+
sys.exit(0)
|
|
94
|
+
|
|
95
|
+
found_hedging_terms = find_hedging_words(assistant_message)
|
|
96
|
+
|
|
97
|
+
if not found_hedging_terms:
|
|
98
|
+
sys.exit(0)
|
|
99
|
+
|
|
100
|
+
formatted_term_list = ", ".join(f'"{term}"' for term in found_hedging_terms)
|
|
101
|
+
|
|
102
|
+
research_mode_content = "(Could not load research-mode skill file)"
|
|
103
|
+
for each_skill_path in RESEARCH_MODE_SKILL_SEARCH_PATHS:
|
|
104
|
+
try:
|
|
105
|
+
with open(each_skill_path, encoding="utf-8") as skill_file:
|
|
106
|
+
research_mode_content = skill_file.read()
|
|
107
|
+
break
|
|
108
|
+
except OSError:
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
block_response = {
|
|
112
|
+
"decision": "block",
|
|
113
|
+
"reason": (
|
|
114
|
+
f"ANTI-HALLUCINATION GUARDRAIL: Your response contains hedging language: "
|
|
115
|
+
f"{formatted_term_list}. "
|
|
116
|
+
f"These words signal unverified claims. You MUST rewrite your response "
|
|
117
|
+
f"with these constraints active:\n\n"
|
|
118
|
+
f"{research_mode_content}\n\n"
|
|
119
|
+
f"Do NOT simply remove the hedging word and keep the unverified claim. "
|
|
120
|
+
f"Either VERIFY it with a source or replace it with 'I don't know'.\n\n"
|
|
121
|
+
f"You MUST re-output the complete, revised response with the corrections applied."
|
|
122
|
+
),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
print(json.dumps(block_response))
|
|
126
|
+
sys.exit(0)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
if __name__ == "__main__":
|
|
130
|
+
main()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PreToolUse:Task hook — suggests team orchestration for parallel Task spawning.
|
|
3
|
+
|
|
4
|
+
Detects parallel Task/Agent calls without team_name and injects a suggestion
|
|
5
|
+
into the conversation context. Does NOT block — the tool call proceeds.
|
|
6
|
+
|
|
7
|
+
Uses atomic file creation (O_CREAT | O_EXCL) to detect concurrent calls.
|
|
8
|
+
Lock auto-expires after 3 seconds to avoid false positives on sequential calls.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
LOCK_FILE = os.path.join(tempfile.gettempdir(), "claude-parallel-task-guard.lock")
|
|
18
|
+
LOCK_MAX_AGE_SECONDS = 3
|
|
19
|
+
|
|
20
|
+
SUGGESTION_MESSAGE = (
|
|
21
|
+
"SUGGESTION: Multiple parallel agents detected without team orchestration. "
|
|
22
|
+
"Consider using TeamCreate + team_name for better coordination, "
|
|
23
|
+
"progress tracking, and file ownership management. "
|
|
24
|
+
"This is optional — parallel agents will proceed without it."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main() -> None:
|
|
29
|
+
try:
|
|
30
|
+
input_data = json.load(sys.stdin)
|
|
31
|
+
except json.JSONDecodeError:
|
|
32
|
+
sys.exit(0)
|
|
33
|
+
|
|
34
|
+
tool_name = input_data.get("tool_name", "")
|
|
35
|
+
tool_input = input_data.get("tool_input", {})
|
|
36
|
+
|
|
37
|
+
if tool_name not in ("Task", "Agent"):
|
|
38
|
+
sys.exit(0)
|
|
39
|
+
|
|
40
|
+
# Team-orchestrated tasks — no suggestion needed
|
|
41
|
+
if tool_input.get("team_name"):
|
|
42
|
+
sys.exit(0)
|
|
43
|
+
|
|
44
|
+
# Clean stale locks (previous turn's lock that wasn't cleaned up)
|
|
45
|
+
try:
|
|
46
|
+
if os.path.exists(LOCK_FILE):
|
|
47
|
+
lock_age = time.time() - os.path.getmtime(LOCK_FILE)
|
|
48
|
+
if lock_age > LOCK_MAX_AGE_SECONDS:
|
|
49
|
+
os.unlink(LOCK_FILE)
|
|
50
|
+
except OSError:
|
|
51
|
+
pass # Race with another process cleaning — fine
|
|
52
|
+
|
|
53
|
+
# Atomic create: only one concurrent caller wins
|
|
54
|
+
try:
|
|
55
|
+
fd = os.open(LOCK_FILE, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
56
|
+
os.write(fd, str(time.time()).encode())
|
|
57
|
+
os.close(fd)
|
|
58
|
+
# First Task in this turn — no suggestion
|
|
59
|
+
sys.exit(0)
|
|
60
|
+
except FileExistsError:
|
|
61
|
+
pass # Another Task already holds the lock
|
|
62
|
+
|
|
63
|
+
# Second+ parallel Task without team → suggest (not block)
|
|
64
|
+
print(SUGGESTION_MESSAGE, file=sys.stderr)
|
|
65
|
+
sys.exit(0)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
main()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
try:
|
|
8
|
+
input_data = json.load(sys.stdin)
|
|
9
|
+
except json.JSONDecodeError:
|
|
10
|
+
sys.exit(0)
|
|
11
|
+
|
|
12
|
+
tool_name = input_data.get("tool_name", "")
|
|
13
|
+
if tool_name != "Bash":
|
|
14
|
+
sys.exit(0)
|
|
15
|
+
|
|
16
|
+
command = input_data.get("tool_input", {}).get("command", "")
|
|
17
|
+
if not command:
|
|
18
|
+
sys.exit(0)
|
|
19
|
+
|
|
20
|
+
is_pr_create = "gh pr create" in command and ("--body" in command or "-b " in command)
|
|
21
|
+
is_pr_edit = "gh pr edit" in command and "--body" in command
|
|
22
|
+
is_commit = re.search(r'git commit\b', command) and ("-m " in command or "-m\"" in command or "-m'" in command)
|
|
23
|
+
|
|
24
|
+
if not (is_pr_create or is_pr_edit or is_commit):
|
|
25
|
+
sys.exit(0)
|
|
26
|
+
|
|
27
|
+
body = ""
|
|
28
|
+
if is_pr_create or is_pr_edit:
|
|
29
|
+
body_match = re.search(r'--body\s+"([^"]*)"', command) or re.search(r"--body\s+'([^']*)'", command)
|
|
30
|
+
if body_match:
|
|
31
|
+
body = body_match.group(1)
|
|
32
|
+
heredoc_match = re.search(r'--body\s+"\$\(cat <<', command)
|
|
33
|
+
if heredoc_match:
|
|
34
|
+
body = command[heredoc_match.start():]
|
|
35
|
+
|
|
36
|
+
if is_commit:
|
|
37
|
+
msg_match = re.search(r'-m\s+"([^"]*)"', command) or re.search(r"-m\s+'([^']*)'", command)
|
|
38
|
+
if msg_match:
|
|
39
|
+
body = msg_match.group(1)
|
|
40
|
+
heredoc_match = re.search(r'-m\s+"\$\(cat <<', command)
|
|
41
|
+
if heredoc_match:
|
|
42
|
+
body = command[heredoc_match.start():]
|
|
43
|
+
|
|
44
|
+
if not body:
|
|
45
|
+
sys.exit(0)
|
|
46
|
+
|
|
47
|
+
violations = []
|
|
48
|
+
|
|
49
|
+
if is_pr_create or is_pr_edit:
|
|
50
|
+
if "## Summary" not in body and "## summary" not in body.lower():
|
|
51
|
+
violations.append("Missing '## Summary' section")
|
|
52
|
+
|
|
53
|
+
has_file_bold = bool(re.search(r'\*\*\w+\.\w+\*\*', body))
|
|
54
|
+
has_bullet_section = bool(re.search(r'###.*(?:test|config|fix)', body, re.IGNORECASE))
|
|
55
|
+
|
|
56
|
+
if not has_file_bold and not has_bullet_section:
|
|
57
|
+
violations.append("Production changes must be grouped by file with **filename** bold headers explaining WHY")
|
|
58
|
+
|
|
59
|
+
jargon_patterns = [
|
|
60
|
+
(r'\bDexie\b', "Dexie (say 'database' or 'local database')"),
|
|
61
|
+
(r'\bReact Query\b', "React Query (say 'cache' or 'data cache')"),
|
|
62
|
+
(r'\bsyncStatus\b', "syncStatus (describe the behavior, not the field)"),
|
|
63
|
+
(r'\blocalUpdatedAt\b', "localUpdatedAt (describe the behavior, not the field)"),
|
|
64
|
+
(r'\bpullStartedAt\b', "pullStartedAt (describe the behavior, not the field)"),
|
|
65
|
+
(r'\buseMutation\b', "useMutation (describe what it does for the user)"),
|
|
66
|
+
]
|
|
67
|
+
for pattern, name in jargon_patterns:
|
|
68
|
+
if re.search(pattern, body):
|
|
69
|
+
violations.append(f"Jargon detected: {name}")
|
|
70
|
+
|
|
71
|
+
if violations:
|
|
72
|
+
violation_list = "; ".join(violations)
|
|
73
|
+
result = {
|
|
74
|
+
"hookSpecificOutput": {
|
|
75
|
+
"hookEventName": "PreToolUse",
|
|
76
|
+
"permissionDecision": "deny",
|
|
77
|
+
"permissionDecisionReason": f"BLOCKED: [PR_DESCRIPTION_STYLE] {violation_list}. Use the pr-description-writer custom agent: Agent(subagent_type=\"pr-description-writer\", team_name=\"your-team\", prompt=\"Write PR description for the current branch\").",
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
print(json.dumps(result))
|
|
81
|
+
sys.stdout.flush()
|
|
82
|
+
|
|
83
|
+
sys.exit(0)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
main()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Block pyautogui mousewheel scroll usage - guide Claude to use pynput instead.
|
|
4
|
+
|
|
5
|
+
pyautogui's scroll implementation uses incorrect delta values with SendInput API.
|
|
6
|
+
pynput uses the same SendInput API but sends correct delta values.
|
|
7
|
+
"""
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
PYAUTOGUI_SCROLL_PATTERNS = [
|
|
13
|
+
r'pyautogui\.scroll\s*\(',
|
|
14
|
+
r'pyautogui\.hscroll\s*\(',
|
|
15
|
+
r'pyautogui\.vscroll\s*\(',
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
COMPILED_PATTERNS = [re.compile(pattern) for pattern in PYAUTOGUI_SCROLL_PATTERNS]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def check_for_pyautogui_scroll(content: str) -> list[str]:
|
|
22
|
+
"""Check for pyautogui scroll function usage."""
|
|
23
|
+
violations = []
|
|
24
|
+
lines = content.split('\n')
|
|
25
|
+
|
|
26
|
+
for line_num, line in enumerate(lines, 1):
|
|
27
|
+
for pattern in COMPILED_PATTERNS:
|
|
28
|
+
if pattern.search(line):
|
|
29
|
+
violations.append(f"Line {line_num}: {line.strip()}")
|
|
30
|
+
break
|
|
31
|
+
|
|
32
|
+
return violations
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def main() -> None:
|
|
36
|
+
try:
|
|
37
|
+
input_data = json.load(sys.stdin)
|
|
38
|
+
except json.JSONDecodeError:
|
|
39
|
+
sys.exit(0)
|
|
40
|
+
|
|
41
|
+
tool_input = input_data.get("tool_input", {})
|
|
42
|
+
file_path = tool_input.get("file_path", "")
|
|
43
|
+
|
|
44
|
+
if not file_path:
|
|
45
|
+
sys.exit(0)
|
|
46
|
+
|
|
47
|
+
# Only check Python files
|
|
48
|
+
if not file_path.endswith('.py'):
|
|
49
|
+
sys.exit(0)
|
|
50
|
+
|
|
51
|
+
content = tool_input.get("content", "") or tool_input.get("new_string", "")
|
|
52
|
+
|
|
53
|
+
if not content:
|
|
54
|
+
sys.exit(0)
|
|
55
|
+
|
|
56
|
+
violations = check_for_pyautogui_scroll(content)
|
|
57
|
+
|
|
58
|
+
if violations:
|
|
59
|
+
violation_list = "\n".join(f" • {v}" for v in violations[:5])
|
|
60
|
+
result = {
|
|
61
|
+
"hookSpecificOutput": {
|
|
62
|
+
"hookEventName": "PreToolUse",
|
|
63
|
+
"permissionDecision": "deny",
|
|
64
|
+
"permissionDecisionReason": f"BLOCKED: pyautogui scroll() is broken on Windows (incorrect delta values). Use pynput instead: from pynput.mouse import Controller; mouse = Controller(); mouse.scroll(0, -3) for scrolling down 3 clicks."
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
print(json.dumps(result))
|
|
68
|
+
sys.stdout.flush()
|
|
69
|
+
|
|
70
|
+
sys.exit(0)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
main()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import fnmatch
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
SENSITIVE_PATTERNS = [
|
|
8
|
+
".env",
|
|
9
|
+
".env.*",
|
|
10
|
+
"*.env",
|
|
11
|
+
"*.pem",
|
|
12
|
+
"*.key",
|
|
13
|
+
"*.p12",
|
|
14
|
+
"*.pfx",
|
|
15
|
+
"credentials.json",
|
|
16
|
+
"secrets.json",
|
|
17
|
+
"id_rsa",
|
|
18
|
+
"id_ed25519",
|
|
19
|
+
"package-lock.json",
|
|
20
|
+
"yarn.lock",
|
|
21
|
+
"Pipfile.lock",
|
|
22
|
+
"poetry.lock",
|
|
23
|
+
"pnpm-lock.yaml",
|
|
24
|
+
"composer.lock",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
WRITE_EDIT_TOOLS = {"Write", "Edit"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_sensitive_file(file_path: str) -> str | None:
|
|
31
|
+
filename = os.path.basename(file_path)
|
|
32
|
+
for each_pattern in SENSITIVE_PATTERNS:
|
|
33
|
+
if fnmatch.fnmatch(filename, each_pattern):
|
|
34
|
+
return each_pattern
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main() -> None:
|
|
39
|
+
try:
|
|
40
|
+
hook_input = json.load(sys.stdin)
|
|
41
|
+
except json.JSONDecodeError:
|
|
42
|
+
sys.exit(0)
|
|
43
|
+
|
|
44
|
+
tool_name = hook_input.get("tool_name", "")
|
|
45
|
+
tool_input = hook_input.get("tool_input", {})
|
|
46
|
+
|
|
47
|
+
if tool_name not in WRITE_EDIT_TOOLS:
|
|
48
|
+
sys.exit(0)
|
|
49
|
+
|
|
50
|
+
file_path = tool_input.get("file_path", "")
|
|
51
|
+
if not file_path:
|
|
52
|
+
sys.exit(0)
|
|
53
|
+
|
|
54
|
+
matched_pattern = is_sensitive_file(file_path)
|
|
55
|
+
|
|
56
|
+
if matched_pattern is not None:
|
|
57
|
+
deny_response = {
|
|
58
|
+
"hookSpecificOutput": {
|
|
59
|
+
"hookEventName": "PreToolUse",
|
|
60
|
+
"permissionDecision": "deny",
|
|
61
|
+
"permissionDecisionReason": f"BLOCKED: Sensitive file '{os.path.basename(file_path)}' (pattern: '{matched_pattern}'). Edit manually outside Claude Code."
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
print(json.dumps(deny_response))
|
|
65
|
+
|
|
66
|
+
sys.exit(0)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
main()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
TDD enforcement hook.
|
|
4
|
+
|
|
5
|
+
Prompts confirmation when writing/editing production code files.
|
|
6
|
+
Skips: Test files, config files, documentation.
|
|
7
|
+
"""
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
PRODUCTION_EXTENSIONS = {'.py', '.ts', '.tsx', '.js', '.jsx'}
|
|
13
|
+
SKIP_PATTERNS = {
|
|
14
|
+
'test_', '_test.', '.test.', 'tests/', '__tests__/',
|
|
15
|
+
'conftest', 'fixture', 'mock', 'stub'
|
|
16
|
+
}
|
|
17
|
+
SKIP_EXTENSIONS = {'.md', '.json', '.yaml', '.yml', '.toml', '.ini', '.cfg', '.txt'}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> None:
|
|
21
|
+
try:
|
|
22
|
+
input_data = json.load(sys.stdin)
|
|
23
|
+
except json.JSONDecodeError:
|
|
24
|
+
sys.exit(0)
|
|
25
|
+
|
|
26
|
+
tool_input = input_data.get("tool_input", {})
|
|
27
|
+
file_path = tool_input.get("file_path", "")
|
|
28
|
+
|
|
29
|
+
if not file_path:
|
|
30
|
+
sys.exit(0)
|
|
31
|
+
|
|
32
|
+
path = Path(file_path)
|
|
33
|
+
ext = path.suffix.lower()
|
|
34
|
+
|
|
35
|
+
# Skip config/docs
|
|
36
|
+
if ext in SKIP_EXTENSIONS:
|
|
37
|
+
sys.exit(0)
|
|
38
|
+
|
|
39
|
+
# Skip non-production code files
|
|
40
|
+
if ext not in PRODUCTION_EXTENSIONS:
|
|
41
|
+
sys.exit(0)
|
|
42
|
+
|
|
43
|
+
# Skip test files
|
|
44
|
+
name_lower = path.name.lower()
|
|
45
|
+
path_str = str(path).lower()
|
|
46
|
+
if any(pattern in name_lower or pattern in path_str for pattern in SKIP_PATTERNS):
|
|
47
|
+
sys.exit(0)
|
|
48
|
+
|
|
49
|
+
# Block production code - require confirmation
|
|
50
|
+
result = {
|
|
51
|
+
"hookSpecificOutput": {
|
|
52
|
+
"hookEventName": "PreToolUse",
|
|
53
|
+
"permissionDecision": "allow",
|
|
54
|
+
"additionalContext": "[TDD] Writing production code. Confirm you have a failing test first."
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
print(json.dumps(result))
|
|
58
|
+
sys.exit(0)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
main()
|