ima-claude 2.9.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 +463 -0
- package/dist/cli.js +1064 -0
- package/package.json +49 -0
- package/platforms/claude/adapter.ts +115 -0
- package/platforms/junie/adapter.ts +254 -0
- package/platforms/junie/agents-template.md +113 -0
- package/platforms/junie/hook-translations.md +84 -0
- package/platforms/shared/detector.ts +27 -0
- package/platforms/shared/installer.ts +202 -0
- package/platforms/shared/types.ts +78 -0
- package/plugins/ima-claude/.claude-plugin/plugin.json +25 -0
- package/plugins/ima-claude/agents/explorer.md +30 -0
- package/plugins/ima-claude/agents/implementer.md +30 -0
- package/plugins/ima-claude/agents/memory.md +42 -0
- package/plugins/ima-claude/agents/reviewer.md +53 -0
- package/plugins/ima-claude/agents/tester.md +33 -0
- package/plugins/ima-claude/agents/wp-developer.md +46 -0
- package/plugins/ima-claude/hooks/README.md +145 -0
- package/plugins/ima-claude/hooks/atlassian_prereqs.py +112 -0
- package/plugins/ima-claude/hooks/block_sed_edits.py +59 -0
- package/plugins/ima-claude/hooks/bootstrap.sh +90 -0
- package/plugins/ima-claude/hooks/bootstrap_utility_check.py +94 -0
- package/plugins/ima-claude/hooks/composer_autoload_check.py +70 -0
- package/plugins/ima-claude/hooks/docs_organization.py +104 -0
- package/plugins/ima-claude/hooks/enforce_rg_over_grep.py +56 -0
- package/plugins/ima-claude/hooks/fp_utility_check.py +90 -0
- package/plugins/ima-claude/hooks/hook_logger.py +69 -0
- package/plugins/ima-claude/hooks/hooks.json +239 -0
- package/plugins/ima-claude/hooks/jira_issue_fetch.py +79 -0
- package/plugins/ima-claude/hooks/jquery_in_wordpress.py +92 -0
- package/plugins/ima-claude/hooks/memory_bootstrap.py +79 -0
- package/plugins/ima-claude/hooks/memory_store_reminder.py +75 -0
- package/plugins/ima-claude/hooks/prompt_coach.py +125 -0
- package/plugins/ima-claude/hooks/prompt_coach_digest.md +48 -0
- package/plugins/ima-claude/hooks/prompt_coach_system.md +30 -0
- package/plugins/ima-claude/hooks/sequential_thinking_check.py +81 -0
- package/plugins/ima-claude/hooks/serena_over_grep.py +96 -0
- package/plugins/ima-claude/hooks/serena_over_read.py +66 -0
- package/plugins/ima-claude/hooks/serena_project_check.py +133 -0
- package/plugins/ima-claude/hooks/sql_injection_check.py +73 -0
- package/plugins/ima-claude/hooks/task_master_after_plan.py +31 -0
- package/plugins/ima-claude/hooks/task_master_before_impl.py +93 -0
- package/plugins/ima-claude/hooks/tavily_extract_advanced.py +48 -0
- package/plugins/ima-claude/hooks/vestige_before_external.py +86 -0
- package/plugins/ima-claude/hooks/webfetch_to_tavily.py +42 -0
- package/plugins/ima-claude/hooks/websearch_to_tavily.py +41 -0
- package/plugins/ima-claude/hooks/wp_security_check.py +150 -0
- package/plugins/ima-claude/personalities/README.md +45 -0
- package/plugins/ima-claude/personalities/enable-40k.md +69 -0
- package/plugins/ima-claude/personalities/enable-templars.md +69 -0
- package/plugins/ima-claude/skills/.research-summary.md +340 -0
- package/plugins/ima-claude/skills/architect/SKILL.md +304 -0
- package/plugins/ima-claude/skills/compound-bridge/SKILL.md +200 -0
- package/plugins/ima-claude/skills/discourse/SKILL.md +440 -0
- package/plugins/ima-claude/skills/discourse-admin/SKILL.md +192 -0
- package/plugins/ima-claude/skills/discourse-admin/references/api-endpoints.md +441 -0
- package/plugins/ima-claude/skills/discourse-admin/references/gotchas.md +107 -0
- package/plugins/ima-claude/skills/discourse-admin/references/staging-defaults.md +98 -0
- package/plugins/ima-claude/skills/discourse-admin/scripts/discourse-admin.py +319 -0
- package/plugins/ima-claude/skills/docs-organize/SKILL.md +254 -0
- package/plugins/ima-claude/skills/docs-organize/templates/active-README.md +50 -0
- package/plugins/ima-claude/skills/docs-organize/templates/archive-README.md +57 -0
- package/plugins/ima-claude/skills/docs-organize/templates/docs-README.md +43 -0
- package/plugins/ima-claude/skills/docs-organize/templates/phase-archive-README.md +83 -0
- package/plugins/ima-claude/skills/docs-organize/templates/section-README.md +48 -0
- package/plugins/ima-claude/skills/docs-organize/templates/transient-README.md +79 -0
- package/plugins/ima-claude/skills/docs-organize/templates/transient-gitignore +9 -0
- package/plugins/ima-claude/skills/ember-discourse/SKILL.md +496 -0
- package/plugins/ima-claude/skills/functional-programmer/SKILL.md +258 -0
- package/plugins/ima-claude/skills/ima-bootstrap/SKILL.md +278 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/bootstrap-patterns.md +356 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/ima-brand.md +273 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/theme-integration.md +212 -0
- package/plugins/ima-claude/skills/ima-brand/SKILL.md +108 -0
- package/plugins/ima-claude/skills/ima-brand/references/brand-identity.md +140 -0
- package/plugins/ima-claude/skills/ima-brand/references/digital-standards.md +180 -0
- package/plugins/ima-claude/skills/ima-brand/references/visual-system.md +173 -0
- package/plugins/ima-claude/skills/ima-forms-expert/SKILL.md +175 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/container-components.md +154 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/examples.md +328 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/field-components.md +298 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/form-factory.md +193 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/quick-reference.md +153 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/validation-engine.md +336 -0
- package/plugins/ima-claude/skills/jira-checkpoint/SKILL.md +178 -0
- package/plugins/ima-claude/skills/jquery/SKILL.md +413 -0
- package/plugins/ima-claude/skills/js-fp/SKILL.md +463 -0
- package/plugins/ima-claude/skills/js-fp/core-principles.md +487 -0
- package/plugins/ima-claude/skills/js-fp/examples/pure-functions.js +260 -0
- package/plugins/ima-claude/skills/js-fp/examples/tests/pure-functions.test.js +262 -0
- package/plugins/ima-claude/skills/js-fp/references/anti-patterns.md +120 -0
- package/plugins/ima-claude/skills/js-fp/references/performance-patterns.md +116 -0
- package/plugins/ima-claude/skills/js-fp/references/testing-patterns.md +134 -0
- package/plugins/ima-claude/skills/js-fp-api/SKILL.md +280 -0
- package/plugins/ima-claude/skills/js-fp-api/examples/crud-endpoint.js +258 -0
- package/plugins/ima-claude/skills/js-fp-api/references/middleware-patterns.md +134 -0
- package/plugins/ima-claude/skills/js-fp-api/references/security-sql.md +110 -0
- package/plugins/ima-claude/skills/js-fp-api/references/validation-patterns.md +165 -0
- package/plugins/ima-claude/skills/js-fp-react/SKILL.md +447 -0
- package/plugins/ima-claude/skills/js-fp-react/examples/ProductCard.tsx +65 -0
- package/plugins/ima-claude/skills/js-fp-react/references/hooks-advanced.md +136 -0
- package/plugins/ima-claude/skills/js-fp-react/references/performance-patterns.md +175 -0
- package/plugins/ima-claude/skills/js-fp-vue/SKILL.md +322 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/complete-examples.md +397 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/composables-advanced.md +282 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/reactivity-patterns.md +348 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/testing.md +314 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/SKILL.md +301 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/ajax-patterns.md +192 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/event-patterns.md +136 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/wp-integration.md +248 -0
- package/plugins/ima-claude/skills/livecanvas/SKILL.md +209 -0
- package/plugins/ima-claude/skills/livecanvas/references/livecanvas-features.md +311 -0
- package/plugins/ima-claude/skills/livecanvas/references/loops-and-logic.md +730 -0
- package/plugins/ima-claude/skills/livecanvas/references/picostrap.md +227 -0
- package/plugins/ima-claude/skills/mcp-atlassian/SKILL.md +339 -0
- package/plugins/ima-claude/skills/mcp-context7/SKILL.md +109 -0
- package/plugins/ima-claude/skills/mcp-memory/SKILL.md +182 -0
- package/plugins/ima-claude/skills/mcp-qdrant/SKILL.md +233 -0
- package/plugins/ima-claude/skills/mcp-sequential/SKILL.md +149 -0
- package/plugins/ima-claude/skills/mcp-serena/SKILL.md +174 -0
- package/plugins/ima-claude/skills/mcp-tavily/SKILL.md +118 -0
- package/plugins/ima-claude/skills/mcp-vestige/SKILL.md +259 -0
- package/plugins/ima-claude/skills/php-authnet/SKILL.md +275 -0
- package/plugins/ima-claude/skills/php-authnet/references/api-reference.md +624 -0
- package/plugins/ima-claude/skills/php-authnet/references/sandbox-testing.md +424 -0
- package/plugins/ima-claude/skills/php-fp/SKILL.md +333 -0
- package/plugins/ima-claude/skills/php-fp/examples/pure-functions.php +403 -0
- package/plugins/ima-claude/skills/php-fp/examples/tests/PureFunctionsTest.php +515 -0
- package/plugins/ima-claude/skills/php-fp/references/core-principles.md +277 -0
- package/plugins/ima-claude/skills/php-fp/references/testing-patterns.md +374 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/SKILL.md +216 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/fp-patterns.md +275 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/plugin-architecture.md +295 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/security-examples.md +203 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/testing-strategy.md +259 -0
- package/plugins/ima-claude/skills/phpunit-wp/SKILL.md +716 -0
- package/plugins/ima-claude/skills/playwright/SKILL.md +434 -0
- package/plugins/ima-claude/skills/playwright/references/accessibility-testing.md +153 -0
- package/plugins/ima-claude/skills/playwright/references/ci-cd.md +268 -0
- package/plugins/ima-claude/skills/playwright/references/network-mocking.md +270 -0
- package/plugins/ima-claude/skills/playwright/references/visual-regression.md +215 -0
- package/plugins/ima-claude/skills/py-fp/SKILL.md +663 -0
- package/plugins/ima-claude/skills/py-fp/examples/pure-functions.py +185 -0
- package/plugins/ima-claude/skills/py-fp/examples/tests/test_pure_functions.py +244 -0
- package/plugins/ima-claude/skills/py-fp/references/core-principles.md +381 -0
- package/plugins/ima-claude/skills/py-fp/references/testing-patterns.md +283 -0
- package/plugins/ima-claude/skills/quasar-fp/SKILL.md +327 -0
- package/plugins/ima-claude/skills/quasar-fp/metadata.json +85 -0
- package/plugins/ima-claude/skills/quasar-fp/references/component-patterns.md +257 -0
- package/plugins/ima-claude/skills/quasar-fp/references/theme-integration.md +233 -0
- package/plugins/ima-claude/skills/quasar-fp/references/utility-classes.md +237 -0
- package/plugins/ima-claude/skills/quickstart/SKILL.md +129 -0
- package/plugins/ima-claude/skills/rails/SKILL.md +359 -0
- package/plugins/ima-claude/skills/resume-session/SKILL.md +68 -0
- package/plugins/ima-claude/skills/rg/SKILL.md +205 -0
- package/plugins/ima-claude/skills/ruby-fp/SKILL.md +336 -0
- package/plugins/ima-claude/skills/save-session/SKILL.md +81 -0
- package/plugins/ima-claude/skills/scorecard/SKILL.md +96 -0
- package/plugins/ima-claude/skills/skill-analyzer/SKILL.md +127 -0
- package/plugins/ima-claude/skills/skill-analyzer/references/advanced-checklist.md +44 -0
- package/plugins/ima-claude/skills/skill-analyzer/references/core-checklist.md +60 -0
- package/plugins/ima-claude/skills/skill-analyzer/scripts/analyze_skill.py +418 -0
- package/plugins/ima-claude/skills/skill-creator/LICENSE.txt +202 -0
- package/plugins/ima-claude/skills/skill-creator/SKILL.md +343 -0
- package/plugins/ima-claude/skills/skill-creator/references/output-patterns.md +82 -0
- package/plugins/ima-claude/skills/skill-creator/references/workflows.md +28 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/init_skill.py +303 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/package_skill.py +110 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/plugins/ima-claude/skills/task-master/SKILL.md +51 -0
- package/plugins/ima-claude/skills/task-planner/SKILL.md +228 -0
- package/plugins/ima-claude/skills/task-runner/SKILL.md +192 -0
- package/plugins/ima-claude/skills/unit-testing/SKILL.md +198 -0
- package/plugins/ima-claude/skills/unit-testing/references/mock-patterns.md +181 -0
- package/plugins/ima-claude/skills/unit-testing/references/tdd-workflow.md +177 -0
- package/plugins/ima-claude/skills/unit-testing/references/test-strategy.md +126 -0
- package/plugins/ima-claude/skills/wp-local/SKILL.md +246 -0
- package/plugins/ima-claude/skills/wp-local/references/configuration.md +198 -0
- package/plugins/ima-claude/skills/wp-local/references/wp-cli-reference.md +406 -0
- package/plugins/ima-claude/skills/wp-local/scripts/wp-local.sh +61 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
UserPromptSubmit hook: Evaluate prompts with Haiku for team standards.
|
|
4
|
+
Provides feedback inline when valuable, stays silent otherwise.
|
|
5
|
+
|
|
6
|
+
Requirements:
|
|
7
|
+
ANTHROPIC_API_KEY env var (same key used for Claude Code)
|
|
8
|
+
pip install anthropic
|
|
9
|
+
|
|
10
|
+
Environment variables:
|
|
11
|
+
PROMPT_COACH_ENABLED=true - Enable evaluation
|
|
12
|
+
PROMPT_COACH_LOG=true - Log prompts + feedback to ~/.claude/prompt_coach.log
|
|
13
|
+
"""
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
# Only run if enabled
|
|
21
|
+
if os.environ.get("PROMPT_COACH_ENABLED", "").lower() != "true":
|
|
22
|
+
sys.exit(0)
|
|
23
|
+
|
|
24
|
+
# Common skip patterns (case-insensitive)
|
|
25
|
+
SKIP_PATTERNS = {
|
|
26
|
+
"yes", "no", "y", "n", "ok", "okay", "continue", "proceed",
|
|
27
|
+
"do it", "looks good", "go ahead", "sure", "thanks", "thank you",
|
|
28
|
+
"got it", "understood", "perfect", "great", "good", "nice",
|
|
29
|
+
"yep", "nope", "yup", "done", "next", "stop", "cancel", "abort",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def should_skip(prompt: str) -> bool:
|
|
34
|
+
"""Skip short prompts and common follow-ups."""
|
|
35
|
+
cleaned = prompt.strip().lower()
|
|
36
|
+
if len(cleaned) < 20:
|
|
37
|
+
return True
|
|
38
|
+
if cleaned in SKIP_PATTERNS:
|
|
39
|
+
return True
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def log_feedback(prompt: str, feedback: str) -> None:
|
|
44
|
+
"""Optionally log to file for review."""
|
|
45
|
+
if os.environ.get("PROMPT_COACH_LOG", "").lower() != "true":
|
|
46
|
+
return
|
|
47
|
+
log_path = Path.home() / ".claude" / "prompt_coach.log"
|
|
48
|
+
timestamp = datetime.now().isoformat()
|
|
49
|
+
try:
|
|
50
|
+
with open(log_path, "a") as f:
|
|
51
|
+
f.write(f"\n--- {timestamp} ---\n")
|
|
52
|
+
f.write(f"PROMPT: {prompt[:200]}{'...' if len(prompt) > 200 else ''}\n")
|
|
53
|
+
f.write(f"FEEDBACK: {feedback}\n")
|
|
54
|
+
except Exception:
|
|
55
|
+
pass # Don't fail on logging errors
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def main() -> None:
|
|
59
|
+
try:
|
|
60
|
+
data = json.load(sys.stdin)
|
|
61
|
+
except json.JSONDecodeError:
|
|
62
|
+
sys.exit(0)
|
|
63
|
+
|
|
64
|
+
# Handle both possible input formats
|
|
65
|
+
# UserPromptSubmit sends: {"prompt": "..."}
|
|
66
|
+
prompt = data.get("prompt", "")
|
|
67
|
+
|
|
68
|
+
if not prompt or should_skip(prompt):
|
|
69
|
+
sys.exit(0)
|
|
70
|
+
|
|
71
|
+
# Load system prompt and skills digest from adjacent files
|
|
72
|
+
hooks_dir = Path(__file__).parent
|
|
73
|
+
system_prompt_path = hooks_dir / "prompt_coach_system.md"
|
|
74
|
+
digest_path = hooks_dir / "prompt_coach_digest.md"
|
|
75
|
+
|
|
76
|
+
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
|
77
|
+
if not api_key:
|
|
78
|
+
sys.exit(0)
|
|
79
|
+
|
|
80
|
+
model = "claude-haiku-4-5-20251001"
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
system_prompt = system_prompt_path.read_text()
|
|
84
|
+
skills_digest = digest_path.read_text()
|
|
85
|
+
except FileNotFoundError as e:
|
|
86
|
+
log_feedback(prompt, f"[error: {e.filename} not found]")
|
|
87
|
+
sys.exit(0)
|
|
88
|
+
|
|
89
|
+
# Combine system prompt with skills digest
|
|
90
|
+
full_system = f"{system_prompt}\n\n---\n\n# SKILLS DIGEST\n\n{skills_digest}"
|
|
91
|
+
|
|
92
|
+
# Call Haiku API
|
|
93
|
+
try:
|
|
94
|
+
import anthropic
|
|
95
|
+
client = anthropic.Anthropic(api_key=api_key)
|
|
96
|
+
|
|
97
|
+
response = client.messages.create(
|
|
98
|
+
model=model,
|
|
99
|
+
max_tokens=300,
|
|
100
|
+
system=full_system,
|
|
101
|
+
messages=[{"role": "user", "content": f"USER PROMPT TO EVALUATE:\n\n{prompt}"}]
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
feedback = response.content[0].text.strip()
|
|
105
|
+
|
|
106
|
+
# Check if Haiku decided to stay silent
|
|
107
|
+
if feedback and feedback != "NO_FEEDBACK":
|
|
108
|
+
print("📋 Prompt Coach:", file=sys.stderr)
|
|
109
|
+
print(feedback, file=sys.stderr)
|
|
110
|
+
print("---", file=sys.stderr)
|
|
111
|
+
log_feedback(prompt, feedback)
|
|
112
|
+
else:
|
|
113
|
+
log_feedback(prompt, "[silent - no issues]")
|
|
114
|
+
|
|
115
|
+
except ImportError:
|
|
116
|
+
log_feedback(prompt, "[error: anthropic package not installed]")
|
|
117
|
+
except Exception as e:
|
|
118
|
+
# Fail silently - don't block on errors
|
|
119
|
+
log_feedback(prompt, f"[error: {e}]")
|
|
120
|
+
|
|
121
|
+
sys.exit(0)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
main()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# IMA Skills Digest for Prompt Coaching
|
|
2
|
+
|
|
3
|
+
## Skill Triggers (suggest when prompt matches)
|
|
4
|
+
|
|
5
|
+
| Prompt Contains | Suggest |
|
|
6
|
+
|----------------|---------|
|
|
7
|
+
| React, hooks, useCallback, component optimization | `js-fp-react` |
|
|
8
|
+
| Vue 3, composable, Composition API | `js-fp-vue` |
|
|
9
|
+
| Quasar, QBtn, QCard, q-pa-, utility classes | `quasar-fp` |
|
|
10
|
+
| Node.js API, REST, middleware, route handlers | `js-fp-api` |
|
|
11
|
+
| WordPress JavaScript, jQuery, AJAX, Gravity Forms | `js-fp-wordpress` |
|
|
12
|
+
| PHP function, pure functions, composition | `php-fp` |
|
|
13
|
+
| WordPress plugin, nonce, sanitization, capability | `php-fp-wordpress` |
|
|
14
|
+
| Architecture, new project, scaling, microservices | `architect` |
|
|
15
|
+
| Documentation structure, organize docs | `docs-organize` |
|
|
16
|
+
| WP-CLI, Local WP, wp plugin, wp db query | `wp-local` |
|
|
17
|
+
| Find in files, search code, grep | `rg` |
|
|
18
|
+
| Latest, current, 2025/2026, recent updates, research | `mcp-tavily` |
|
|
19
|
+
| Library docs, React API, how to use [library] | `mcp-context7` |
|
|
20
|
+
| Find references, rename symbol, refactor, what calls X | `mcp-serena` |
|
|
21
|
+
| Think through, step by step, debug, complex problem | `mcp-sequential` |
|
|
22
|
+
| Remember this, save preference, architectural decision | `mcp-memory` |
|
|
23
|
+
| IMA Forms, form validation, repeater field | `ima-forms-expert` |
|
|
24
|
+
| Analyze skill, audit skill, skill review | `skill-analyzer` |
|
|
25
|
+
| Create skill, build skill, skill template | `skill-creator` |
|
|
26
|
+
|
|
27
|
+
## Core Anti-Patterns (flag these)
|
|
28
|
+
|
|
29
|
+
**FP Utilities**: Creating custom pipe/compose/curry/partial - use native patterns
|
|
30
|
+
**Over-Engineering**: "make it generic", "add wrapper", "create utility" without evidence
|
|
31
|
+
**Premature Abstraction**: Extracting helpers before 3+ genuine reuses
|
|
32
|
+
**Security Gaps**: Raw SQL, missing nonces (WP), unsanitized input, hardcoded secrets
|
|
33
|
+
**Wrong Tool**: grep instead of rg, WebSearch instead of mcp-tavily, reading files instead of mcp-serena
|
|
34
|
+
|
|
35
|
+
## Team Philosophy
|
|
36
|
+
|
|
37
|
+
- Simple > Complex
|
|
38
|
+
- Evidence > Assumptions
|
|
39
|
+
- Specific > Generic (generalize with evidence)
|
|
40
|
+
- Add complexity only when proven needed
|
|
41
|
+
|
|
42
|
+
## When Skills Already Apply (stay silent)
|
|
43
|
+
|
|
44
|
+
- Prompt mentions a skill by name
|
|
45
|
+
- Prompt has clear, specific requirements
|
|
46
|
+
- Bug fix with reproduction steps
|
|
47
|
+
- Exploring/reading code without modification
|
|
48
|
+
- Simple follow-ups
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
You are a prompt coach for a development team using Claude Code with custom skills. Analyze prompts and provide brief, actionable feedback.
|
|
2
|
+
|
|
3
|
+
You will receive:
|
|
4
|
+
1. A SKILLS DIGEST (skill triggers + anti-patterns)
|
|
5
|
+
2. The USER PROMPT to evaluate
|
|
6
|
+
|
|
7
|
+
## Your Task
|
|
8
|
+
|
|
9
|
+
1. Check if prompt would benefit from a skill (use digest triggers)
|
|
10
|
+
2. Flag anti-patterns (custom FP utilities, over-engineering, security gaps)
|
|
11
|
+
3. Note vague requirements that need specifics
|
|
12
|
+
|
|
13
|
+
## Output Rules
|
|
14
|
+
|
|
15
|
+
**If feedback is valuable**: 2-3 bullet points max, one line each
|
|
16
|
+
```
|
|
17
|
+
• Consider: [skill-name] for [reason]
|
|
18
|
+
• [Anti-pattern]: [brief suggestion]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**If no feedback needed**: Respond with exactly: `NO_FEEDBACK`
|
|
22
|
+
|
|
23
|
+
## Stay Silent When
|
|
24
|
+
|
|
25
|
+
- Prompt already mentions a relevant skill
|
|
26
|
+
- Clear, specific requirements
|
|
27
|
+
- Simple follow-ups (yes, continue, do it)
|
|
28
|
+
- Questions about codebase (where is X?)
|
|
29
|
+
- Reading/exploring without modification intent
|
|
30
|
+
- No actionable improvement to suggest
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
UserPromptSubmit hook: Remind Claude to use Sequential Thinking for complex reasoning tasks.
|
|
4
|
+
|
|
5
|
+
Sequential Thinking prevents costly back-and-forth by structuring reasoning upfront.
|
|
6
|
+
Fires when the prompt contains debugging, analysis, architecture, or trade-off language.
|
|
7
|
+
|
|
8
|
+
Fires once per session — it's a setup reminder, not a per-action nudge.
|
|
9
|
+
Exit code 0 = soft warning via stderr.
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
STATE_FILE = os.path.expanduser("~/.claude/.sequential_reminded")
|
|
18
|
+
STALENESS_SECONDS = 3600
|
|
19
|
+
|
|
20
|
+
INVESTIGATION_SIGNALS = re.compile(
|
|
21
|
+
r"\b(debug|diagnos|investigat|figure out|why (is|does|isn't|doesn't|won't)|"
|
|
22
|
+
r"not working|broken|fail|error|exception|unexpected|strange|weird|"
|
|
23
|
+
r"trace|root cause|hunt down|track down)\b",
|
|
24
|
+
re.IGNORECASE,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
ANALYSIS_SIGNALS = re.compile(
|
|
28
|
+
r"\b(analyz|trade.?off|compare|evaluate|weigh|pros.?and.?cons|"
|
|
29
|
+
r"best (way|approach|option|pattern)|should (we|I)|"
|
|
30
|
+
r"architect|design|plan|how (should|would|do) (we|I))\b",
|
|
31
|
+
re.IGNORECASE,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
REMINDER = """Complex reasoning detected — Sequential Thinking structures the analysis before acting:
|
|
35
|
+
mcp__sequential-thinking__sequentialthinking
|
|
36
|
+
thought: "Step 1: ..." thoughtNumber: 1 totalThoughts: 5 nextThoughtNeeded: true
|
|
37
|
+
Useful for: debugging root causes, evaluating trade-offs, architecture decisions,
|
|
38
|
+
multi-step investigations where the approach may need to change mid-stream.
|
|
39
|
+
Prevents expensive trial-and-error by thinking it through first."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def is_reminded() -> bool:
|
|
43
|
+
if not os.path.exists(STATE_FILE):
|
|
44
|
+
return False
|
|
45
|
+
try:
|
|
46
|
+
mtime = os.path.getmtime(STATE_FILE)
|
|
47
|
+
return (time.time() - mtime) < STALENESS_SECONDS
|
|
48
|
+
except OSError:
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def mark_reminded() -> None:
|
|
53
|
+
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
|
|
54
|
+
with open(STATE_FILE, "w") as f:
|
|
55
|
+
f.write(str(time.time()))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
input_data = json.load(sys.stdin)
|
|
60
|
+
except json.JSONDecodeError:
|
|
61
|
+
sys.exit(0)
|
|
62
|
+
|
|
63
|
+
prompt = input_data.get("user_prompt", "").strip()
|
|
64
|
+
if not prompt or len(prompt.split()) < 10:
|
|
65
|
+
sys.exit(0)
|
|
66
|
+
|
|
67
|
+
if prompt.startswith("/"):
|
|
68
|
+
sys.exit(0)
|
|
69
|
+
|
|
70
|
+
if is_reminded():
|
|
71
|
+
sys.exit(0)
|
|
72
|
+
|
|
73
|
+
has_investigation = INVESTIGATION_SIGNALS.search(prompt)
|
|
74
|
+
has_analysis = ANALYSIS_SIGNALS.search(prompt)
|
|
75
|
+
|
|
76
|
+
if not (has_investigation or has_analysis):
|
|
77
|
+
sys.exit(0)
|
|
78
|
+
|
|
79
|
+
mark_reminded()
|
|
80
|
+
print(REMINDER, file=sys.stderr)
|
|
81
|
+
sys.exit(0)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PreToolUse hook: Nudge toward Serena symbol tools when Grep is used for code navigation.
|
|
4
|
+
|
|
5
|
+
When Claude uses Grep with patterns that look like symbol searches (class definitions,
|
|
6
|
+
function references, import tracing), suggest Serena's symbol tools instead — they're
|
|
7
|
+
more precise, understand scope, and save tokens.
|
|
8
|
+
|
|
9
|
+
Fires on every other symbol-like Grep (counter-based) to stay persistent without
|
|
10
|
+
being noise on every single search.
|
|
11
|
+
Exit code 0 = soft warning via stderr.
|
|
12
|
+
"""
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
STATE_FILE = os.path.expanduser("~/.claude/.serena_grep_count")
|
|
20
|
+
STALENESS_SECONDS = 3600
|
|
21
|
+
FIRE_EVERY_N = 2 # Remind on every Nth symbol-like grep
|
|
22
|
+
|
|
23
|
+
SYMBOL_PATTERNS = [
|
|
24
|
+
r"^(class|interface|trait|enum|abstract\s+class)\s+\w+",
|
|
25
|
+
r"^(function|def|fn)\s+\w+",
|
|
26
|
+
r"^(extends|implements)\s+\w+",
|
|
27
|
+
r"^(use|import|require|from)\s+",
|
|
28
|
+
r"->(\w+)\(", # method call
|
|
29
|
+
r"::\w+\(", # static method call
|
|
30
|
+
r"new\s+\w+", # constructor
|
|
31
|
+
r"\bfunction_exists\(",
|
|
32
|
+
r"\bclass_exists\(",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
COMPILED_PATTERNS = [re.compile(p) for p in SYMBOL_PATTERNS]
|
|
36
|
+
|
|
37
|
+
REMINDER = """Serena finds symbols without scanning file contents — much cheaper than Grep:
|
|
38
|
+
mcp__serena__jet_brains_find_symbol name_path_pattern: "{symbol}"
|
|
39
|
+
→ exact match, understands scope and inheritance
|
|
40
|
+
mcp__serena__jet_brains_find_referencing_symbols name_path: "{symbol}" relative_path: "{file}"
|
|
41
|
+
→ every caller across the codebase
|
|
42
|
+
mcp__serena__jet_brains_get_symbols_overview relative_path: "{file}"
|
|
43
|
+
→ all symbols in a file without reading it
|
|
44
|
+
Grep for text search. Serena for symbol search. 40-70% token savings."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_count() -> int:
|
|
48
|
+
if not os.path.exists(STATE_FILE):
|
|
49
|
+
return 0
|
|
50
|
+
try:
|
|
51
|
+
with open(STATE_FILE) as f:
|
|
52
|
+
data = json.load(f)
|
|
53
|
+
if time.time() - data.get("ts", 0) > STALENESS_SECONDS:
|
|
54
|
+
return 0
|
|
55
|
+
return data.get("count", 0)
|
|
56
|
+
except (json.JSONDecodeError, OSError):
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def increment_count(current: int) -> None:
|
|
61
|
+
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
|
|
62
|
+
with open(STATE_FILE, "w") as f:
|
|
63
|
+
json.dump({"count": current + 1, "ts": time.time()}, f)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
input_data = json.load(sys.stdin)
|
|
68
|
+
except json.JSONDecodeError:
|
|
69
|
+
sys.exit(0)
|
|
70
|
+
|
|
71
|
+
tool_name = input_data.get("tool_name", "")
|
|
72
|
+
if tool_name != "Grep":
|
|
73
|
+
sys.exit(0)
|
|
74
|
+
|
|
75
|
+
pattern = input_data.get("tool_input", {}).get("pattern", "")
|
|
76
|
+
if not pattern:
|
|
77
|
+
sys.exit(0)
|
|
78
|
+
|
|
79
|
+
is_symbol_search = any(cp.search(pattern) for cp in COMPILED_PATTERNS)
|
|
80
|
+
if not is_symbol_search:
|
|
81
|
+
sys.exit(0)
|
|
82
|
+
|
|
83
|
+
count = get_count()
|
|
84
|
+
increment_count(count)
|
|
85
|
+
|
|
86
|
+
# Fire on every Nth symbol grep (0-indexed: fire when count % N == 0)
|
|
87
|
+
if count % FIRE_EVERY_N != 0:
|
|
88
|
+
sys.exit(0)
|
|
89
|
+
|
|
90
|
+
symbol_match = re.search(r"\b([A-Z]\w+|\w+Service|\w+Controller|\w+Handler|\w+Repository)\b", pattern)
|
|
91
|
+
symbol = symbol_match.group(1) if symbol_match else "SymbolName"
|
|
92
|
+
|
|
93
|
+
file_hint = input_data.get("tool_input", {}).get("path", "path/to/file")
|
|
94
|
+
|
|
95
|
+
print(REMINDER.replace("{symbol}", symbol).replace("{file}", str(file_hint)), file=sys.stderr)
|
|
96
|
+
sys.exit(0)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PreToolUse hook: Suggest Serena symbol tools before reading code files.
|
|
4
|
+
|
|
5
|
+
Reading an entire file to understand its structure costs 2-10x more tokens than
|
|
6
|
+
using Serena's symbol overview. This hook fires on every code file Read and
|
|
7
|
+
reminds Claude to try the cheaper path first.
|
|
8
|
+
|
|
9
|
+
Fires on every code file read — not rate-limited, because the savings compound.
|
|
10
|
+
Skip list: config files, markdown, json, yaml, lock files, env files.
|
|
11
|
+
Exit code 0 = soft warning via stderr.
|
|
12
|
+
"""
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
CODE_EXTENSIONS = {
|
|
18
|
+
".php", ".ts", ".tsx", ".js", ".jsx", ".py",
|
|
19
|
+
".rb", ".go", ".java", ".cs", ".vue", ".svelte",
|
|
20
|
+
".rs", ".cpp", ".c", ".h",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
SKIP_EXTENSIONS = {
|
|
24
|
+
".md", ".json", ".yaml", ".yml", ".lock", ".env",
|
|
25
|
+
".txt", ".csv", ".xml", ".html", ".css", ".scss",
|
|
26
|
+
".toml", ".ini", ".cfg", ".conf", ".sh", ".bash",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
REMINDER = """Serena symbol tools cost 40-70% fewer tokens than reading files:
|
|
30
|
+
mcp__serena__jet_brains_get_symbols_overview relative_path: "{file}"
|
|
31
|
+
→ structure of the file (classes, methods, properties) without reading it
|
|
32
|
+
mcp__serena__jet_brains_find_symbol name_path_pattern: "ClassName"
|
|
33
|
+
→ find any symbol across the codebase instantly
|
|
34
|
+
mcp__serena__jet_brains_find_referencing_symbols name_path: "method" relative_path: "{file}"
|
|
35
|
+
→ find every caller without grep
|
|
36
|
+
Use Read only when you need the full implementation body of a specific symbol."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
input_data = json.load(sys.stdin)
|
|
41
|
+
except json.JSONDecodeError:
|
|
42
|
+
sys.exit(0)
|
|
43
|
+
|
|
44
|
+
tool_name = input_data.get("tool_name", "")
|
|
45
|
+
if tool_name != "Read":
|
|
46
|
+
sys.exit(0)
|
|
47
|
+
|
|
48
|
+
file_path = input_data.get("tool_input", {}).get("file_path", "")
|
|
49
|
+
if not file_path:
|
|
50
|
+
sys.exit(0)
|
|
51
|
+
|
|
52
|
+
_, ext = os.path.splitext(file_path.lower())
|
|
53
|
+
|
|
54
|
+
# Only fire for code files
|
|
55
|
+
if ext in SKIP_EXTENSIONS or ext not in CODE_EXTENSIONS:
|
|
56
|
+
sys.exit(0)
|
|
57
|
+
|
|
58
|
+
# Skip very small files (probably configs or stubs) — threshold 5KB
|
|
59
|
+
try:
|
|
60
|
+
if os.path.getsize(file_path) < 5000:
|
|
61
|
+
sys.exit(0)
|
|
62
|
+
except OSError:
|
|
63
|
+
pass # File doesn't exist yet or can't stat — proceed
|
|
64
|
+
|
|
65
|
+
print(REMINDER.replace("{file}", file_path), file=sys.stderr)
|
|
66
|
+
sys.exit(0)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PreToolUse hook: Ensure Serena is using the right project, especially in WP plugin subdirs.
|
|
4
|
+
|
|
5
|
+
Problem: Serena uses --project-from-cwd, so when Claude Code runs from a plugin
|
|
6
|
+
subdirectory (wp-content/plugins/my-plugin/), Serena can't find the .serena/ config
|
|
7
|
+
at the WordPress root. PHPStorm has the parent WP project open, not the plugin.
|
|
8
|
+
|
|
9
|
+
Fix: When any mcp__serena__jet_brains_* tool is called, check if we're inside a
|
|
10
|
+
wp-content/plugins/ or wp-content/themes/ path. If so, remind Claude that the
|
|
11
|
+
Serena project is the parent WordPress root and may need explicit activation.
|
|
12
|
+
|
|
13
|
+
Also tracks whether Serena has been used this session. If not, and Claude is doing
|
|
14
|
+
code navigation via Grep with symbol-like patterns, nudges toward Serena.
|
|
15
|
+
|
|
16
|
+
Exit code 0 = soft warning via stderr.
|
|
17
|
+
"""
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
|
|
23
|
+
STATE_FILE = os.path.expanduser("~/.claude/.serena_session_state")
|
|
24
|
+
STALENESS_SECONDS = 3600
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_state():
|
|
28
|
+
"""Read session state."""
|
|
29
|
+
if not os.path.exists(STATE_FILE):
|
|
30
|
+
return {"activated": False, "warned_wp": False, "timestamp": 0}
|
|
31
|
+
try:
|
|
32
|
+
with open(STATE_FILE, "r") as f:
|
|
33
|
+
state = json.load(f)
|
|
34
|
+
if time.time() - state.get("timestamp", 0) > STALENESS_SECONDS:
|
|
35
|
+
return {"activated": False, "warned_wp": False, "timestamp": 0}
|
|
36
|
+
return state
|
|
37
|
+
except (json.JSONDecodeError, OSError):
|
|
38
|
+
return {"activated": False, "warned_wp": False, "timestamp": 0}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def save_state(state):
|
|
42
|
+
"""Write session state."""
|
|
43
|
+
state["timestamp"] = time.time()
|
|
44
|
+
state_dir = os.path.dirname(STATE_FILE)
|
|
45
|
+
os.makedirs(state_dir, exist_ok=True)
|
|
46
|
+
with open(STATE_FILE, "w") as f:
|
|
47
|
+
json.dump(state, f)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def find_wp_root(cwd):
|
|
51
|
+
"""Walk up from cwd to find a WordPress root (contains wp-content/)."""
|
|
52
|
+
path = cwd
|
|
53
|
+
for _ in range(10): # max 10 levels up
|
|
54
|
+
if os.path.isdir(os.path.join(path, "wp-content")):
|
|
55
|
+
return path
|
|
56
|
+
parent = os.path.dirname(path)
|
|
57
|
+
if parent == path:
|
|
58
|
+
break
|
|
59
|
+
path = parent
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def find_serena_project(cwd):
|
|
64
|
+
"""Walk up from cwd to find nearest .serena/project.yml."""
|
|
65
|
+
path = cwd
|
|
66
|
+
for _ in range(10):
|
|
67
|
+
project_yml = os.path.join(path, ".serena", "project.yml")
|
|
68
|
+
if os.path.exists(project_yml):
|
|
69
|
+
return path
|
|
70
|
+
parent = os.path.dirname(path)
|
|
71
|
+
if parent == path:
|
|
72
|
+
break
|
|
73
|
+
path = parent
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_project_name(serena_root):
|
|
78
|
+
"""Extract project_name from .serena/project.yml."""
|
|
79
|
+
try:
|
|
80
|
+
project_yml = os.path.join(serena_root, ".serena", "project.yml")
|
|
81
|
+
with open(project_yml, "r") as f:
|
|
82
|
+
for line in f:
|
|
83
|
+
if line.strip().startswith("project_name:"):
|
|
84
|
+
name = line.split(":", 1)[1].strip().strip('"').strip("'")
|
|
85
|
+
if name:
|
|
86
|
+
return name
|
|
87
|
+
except OSError:
|
|
88
|
+
pass
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
input_data = json.load(sys.stdin)
|
|
94
|
+
except json.JSONDecodeError:
|
|
95
|
+
sys.exit(0)
|
|
96
|
+
|
|
97
|
+
tool_name = input_data.get("tool_name", "")
|
|
98
|
+
state = get_state()
|
|
99
|
+
cwd = os.getcwd()
|
|
100
|
+
|
|
101
|
+
# Track Serena usage
|
|
102
|
+
if tool_name.startswith("mcp__serena__"):
|
|
103
|
+
state["activated"] = True
|
|
104
|
+
save_state(state)
|
|
105
|
+
|
|
106
|
+
# Check 1: WP plugin subdirectory detection for Serena JetBrains tools
|
|
107
|
+
if tool_name.startswith("mcp__serena__jet_brains_") and not state.get("warned_wp"):
|
|
108
|
+
# Are we in a wp-content subdirectory?
|
|
109
|
+
in_wp_subdir = "wp-content/plugins/" in cwd or "wp-content/themes/" in cwd
|
|
110
|
+
|
|
111
|
+
if in_wp_subdir:
|
|
112
|
+
wp_root = find_wp_root(cwd)
|
|
113
|
+
serena_root = find_serena_project(cwd)
|
|
114
|
+
|
|
115
|
+
if serena_root and serena_root != cwd:
|
|
116
|
+
project_name = get_project_name(serena_root)
|
|
117
|
+
msg = (
|
|
118
|
+
f"Serena project is at the WordPress root, not this plugin directory.\n"
|
|
119
|
+
f" Project root: {serena_root}\n"
|
|
120
|
+
)
|
|
121
|
+
if project_name:
|
|
122
|
+
msg += f" Project name: {project_name}\n"
|
|
123
|
+
msg += (
|
|
124
|
+
f" Current dir: {cwd}\n"
|
|
125
|
+
f" If Serena tools fail, activate the project explicitly:\n"
|
|
126
|
+
f" mcp__serena__check_onboarding_performed\n"
|
|
127
|
+
f" Use relative_path from the WP root, not the plugin directory."
|
|
128
|
+
)
|
|
129
|
+
print(msg, file=sys.stderr)
|
|
130
|
+
state["warned_wp"] = True
|
|
131
|
+
save_state(state)
|
|
132
|
+
|
|
133
|
+
sys.exit(0)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PostToolUse hook: Warn about SQL string interpolation in JS/TS files.
|
|
4
|
+
|
|
5
|
+
Checks (soft warning only, exit 0):
|
|
6
|
+
H2 — Template literals or string concatenation building SQL queries with dynamic values.
|
|
7
|
+
|
|
8
|
+
Applies to: Edit, Write on .js, .ts, .mjs, .mts files.
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
JS_EXTENSIONS = (".js", ".ts", ".mjs", ".mts")
|
|
15
|
+
|
|
16
|
+
SQL_KEYWORDS = r"(?:SELECT|INSERT|UPDATE|DELETE|WHERE)"
|
|
17
|
+
|
|
18
|
+
# Template literal: `SELECT ... ${`
|
|
19
|
+
TEMPLATE_LITERAL_PATTERN = re.compile(
|
|
20
|
+
rf"`[^`]*\b{SQL_KEYWORDS}\b[^`]*\${{",
|
|
21
|
+
re.IGNORECASE | re.DOTALL,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# String concatenation: "SELECT..." + or 'SELECT...' +
|
|
25
|
+
STRING_CONCAT_PATTERN = re.compile(
|
|
26
|
+
rf"""(?:"|')[^"']*\b{SQL_KEYWORDS}\b[^"']*(?:"|')\s*\+""",
|
|
27
|
+
re.IGNORECASE,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
WARNING = (
|
|
31
|
+
"⚠️ H2: SQL string interpolation detected — use parameterized queries instead.\n"
|
|
32
|
+
" WRONG: `SELECT * FROM users WHERE id = ${userId}`\n"
|
|
33
|
+
" RIGHT: { sql: 'SELECT * FROM users WHERE id = ?', params: [userId] }"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def has_sql_interpolation(content: str) -> bool:
|
|
38
|
+
return bool(
|
|
39
|
+
TEMPLATE_LITERAL_PATTERN.search(content)
|
|
40
|
+
or STRING_CONCAT_PATTERN.search(content)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_content(tool_name: str, tool_input: dict) -> str:
|
|
45
|
+
if tool_name == "Write":
|
|
46
|
+
return tool_input.get("content", "")
|
|
47
|
+
# Edit: scan only the new_string — no need to read the full file for SQL injection
|
|
48
|
+
return tool_input.get("new_string", "")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
input_data = json.load(sys.stdin)
|
|
53
|
+
except json.JSONDecodeError:
|
|
54
|
+
sys.exit(0)
|
|
55
|
+
|
|
56
|
+
tool_name = input_data.get("tool_name", "")
|
|
57
|
+
tool_input = input_data.get("tool_input", {})
|
|
58
|
+
file_path = tool_input.get("file_path", "")
|
|
59
|
+
|
|
60
|
+
if tool_name not in ("Edit", "Write"):
|
|
61
|
+
sys.exit(0)
|
|
62
|
+
|
|
63
|
+
if not file_path.endswith(JS_EXTENSIONS):
|
|
64
|
+
sys.exit(0)
|
|
65
|
+
|
|
66
|
+
content = get_content(tool_name, tool_input)
|
|
67
|
+
if not content:
|
|
68
|
+
sys.exit(0)
|
|
69
|
+
|
|
70
|
+
if has_sql_interpolation(content):
|
|
71
|
+
print(WARNING, file=sys.stderr)
|
|
72
|
+
|
|
73
|
+
sys.exit(0)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PostToolUse hook: After exiting plan mode, direct Claude to delegate tasks to agents.
|
|
4
|
+
|
|
5
|
+
Matcher: ExitPlanMode
|
|
6
|
+
Fires once per plan exit. The plan is already done — this hook pushes Claude
|
|
7
|
+
to delegate each task to subagents via the Task tool instead of implementing directly.
|
|
8
|
+
Does NOT re-invoke task-master (that would restart planning).
|
|
9
|
+
Exit code 0 = soft warning via stderr.
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
REMINDER = """STOP. The plan is approved. Do NOT implement directly — DELEGATE.
|
|
15
|
+
|
|
16
|
+
Invoke /ima-claude:task-runner now to delegate each task to subagents.
|
|
17
|
+
You are the Orchestrator. You coordinate. Agents implement. Do NOT write code yourself.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
input_data = json.load(sys.stdin)
|
|
22
|
+
except json.JSONDecodeError:
|
|
23
|
+
sys.exit(0)
|
|
24
|
+
|
|
25
|
+
tool_name = input_data.get("tool_name", "")
|
|
26
|
+
|
|
27
|
+
if tool_name != "ExitPlanMode":
|
|
28
|
+
sys.exit(0)
|
|
29
|
+
|
|
30
|
+
print(REMINDER, file=sys.stderr)
|
|
31
|
+
sys.exit(0)
|