claude-dev-env 1.2.1 → 1.4.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.
@@ -175,7 +175,7 @@ Displays updated context
175
175
 
176
176
  ### With Hooks
177
177
 
178
- **SessionStart Hook** (~/.claude/hooks/hooks.json):
178
+ **SessionStart Hook** (~/.claude/settings.json):
179
179
  ```json
180
180
  {
181
181
  "name": "auto-load-project-context",
@@ -0,0 +1,95 @@
1
+ # GitHub PR Summary Writing Guide for AI
2
+
3
+ Use this guide when writing pull request descriptions. Follow these best practices to create clear, professional, and reviewable PR summaries.
4
+
5
+ ## Required Sections
6
+
7
+ ### What (Changes)
8
+
9
+ - Concise statement of what was changed
10
+ - What files or systems were modified
11
+ - What functionality was added, removed, or improved
12
+ - Keep to 2-3 sentences maximum
13
+
14
+ ### Why (Problem/Context)
15
+
16
+ - Explain the problem this PR solves
17
+ - Provide business or technical context
18
+ - Reference related issue numbers using `#123` or `Fixes #123`, `Closes #456`
19
+ - If no issue exists, briefly explain the motivation
20
+
21
+ ### How (Approach/Solution)
22
+
23
+ - Describe your implementation approach
24
+ - Explain any design decisions or trade-offs
25
+ - Include architectural changes if applicable
26
+ - Note any breaking changes prominently
27
+
28
+ ## Supporting Details
29
+
30
+ ### Testing and Quality
31
+
32
+ - What tests were added/modified
33
+ - How to manually verify the changes (if applicable)
34
+ - Any areas of concern or limitations
35
+ - Performance impact (if relevant)
36
+
37
+ ### Dependencies and Risk
38
+
39
+ - New dependencies introduced (if any)
40
+ - Backward compatibility status
41
+ - Potential side effects
42
+ - Migration steps (if needed)
43
+
44
+ ## Optional but Valuable
45
+
46
+ ### Related Issues/PRs
47
+
48
+ - Link to dependent PRs or issues
49
+ - Note any follow-up work needed
50
+
51
+ ### Screenshots/Examples (for UI changes)
52
+
53
+ - Before/after comparisons when visual changes are involved
54
+
55
+ ### Reviewer Guidance
56
+
57
+ - Specific areas to focus on
58
+ - Questions for reviewers
59
+ - Deployment considerations
60
+
61
+ ## Tone and Style Guidelines
62
+
63
+ - Be clear and concise -- reviewers scan quickly
64
+ - Use second person sparingly -- focus on what the code does, not what the reviewer should do
65
+ - Avoid jargon -- explain technical terms if non-obvious
66
+ - Use markdown formatting -- bullets, code blocks, headers for readability
67
+ - Be honest about limitations -- acknowledge trade-offs and known issues
68
+ - Assume reviewers are unfamiliar -- provide sufficient context
69
+
70
+ ## What to Avoid
71
+
72
+ - Vague statements like "fix bug" or "update code"
73
+ - AI-generated summaries without human verification
74
+ - Large walls of text -- break into sections
75
+ - Repeating information from commit messages
76
+ - References to temporary branch names or internal jargon without context
77
+
78
+ ## Example Structure
79
+
80
+ ```markdown
81
+ ## Description
82
+ Brief 1-2 sentence overview of the change.
83
+
84
+ ## Why
85
+ Problem/context and reference to related issue (#123).
86
+
87
+ ## How
88
+ Implementation approach and design decisions.
89
+
90
+ ## Testing
91
+ How this was tested and verified.
92
+
93
+ ## Risk Assessment
94
+ Any breaking changes, dependencies, or concerns.
95
+ ```
@@ -123,9 +123,21 @@ def parse_bash_command_from_stdin() -> str:
123
123
  return hook_event.get("tool_input", {}).get("command", "")
124
124
 
125
125
 
126
+ DRAFT_PR_INSTRUCTION = (
127
+ " Instead: (1) create a feature branch with `git checkout -b <descriptive-branch-name>`, "
128
+ "(2) commit your changes there, "
129
+ "(3) push with `git push -u origin <branch-name>`, "
130
+ "(4) create a draft PR with `gh pr create --draft`. "
131
+ "If you must commit to main, the user needs to approve explicitly."
132
+ )
133
+
134
+
126
135
  def build_denial_response(branch_name: str, repo_dir: str | None) -> dict:
127
136
  location = f" in {repo_dir}" if repo_dir else ""
128
- denial_reason = f"BLOCKED: Direct commit to '{branch_name}' is not allowed. Create a feature branch first: git checkout -b feature/your-branch-name. If you must commit to main, the user needs to approve explicitly."
137
+ denial_reason = (
138
+ f"BLOCKED: Direct commit to '{branch_name}'{location} is not allowed."
139
+ + DRAFT_PR_INSTRUCTION
140
+ )
129
141
 
130
142
  return {
131
143
  "hookSpecificOutput": {
@@ -1,9 +1,67 @@
1
1
  import json
2
+ import os
2
3
  import re
3
4
  import sys
4
5
 
6
+ PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
7
+ PR_GUIDE_PATH = os.path.join(PLUGIN_ROOT, "docs", "PR_DESCRIPTION_GUIDE.md")
5
8
 
6
- def main():
9
+ REQUIRED_PR_SECTION_HEADERS = [
10
+ "description",
11
+ "why",
12
+ "how",
13
+ ]
14
+
15
+ MINIMUM_PR_BODY_LENGTH = 50
16
+
17
+ VAGUE_LANGUAGE_PATTERN = re.compile(
18
+ r'\b(fix(?:ed)? (?:bug|issue|it)|update(?:d)? code|minor changes|various (?:fixes|updates|improvements))\b',
19
+ re.IGNORECASE,
20
+ )
21
+
22
+
23
+ def extract_body_from_command(command: str) -> str:
24
+ heredoc_match = re.search(r'--body\s+"\$\(cat <<', command)
25
+ if heredoc_match:
26
+ return command[heredoc_match.start():]
27
+
28
+ body_match = re.search(r'--body\s+"([^"]*)"', command) or re.search(r"--body\s+'([^']*)'", command)
29
+ if body_match:
30
+ return body_match.group(1)
31
+
32
+ short_flag_match = re.search(r'-b\s+"([^"]*)"', command) or re.search(r"-b\s+'([^']*)'", command)
33
+ if short_flag_match:
34
+ return short_flag_match.group(1)
35
+
36
+ return ""
37
+
38
+
39
+ def validate_pr_body(body: str) -> list[str]:
40
+ violations = []
41
+ body_lower = body.lower()
42
+
43
+ missing_required_sections = [
44
+ header for header in REQUIRED_PR_SECTION_HEADERS
45
+ if f"## {header}" not in body_lower and f"**{header}" not in body_lower
46
+ ]
47
+
48
+ if missing_required_sections:
49
+ formatted_sections = ", ".join(f"'{each_section.title()}'" for each_section in missing_required_sections)
50
+ violations.append(f"Missing required section(s): {formatted_sections}")
51
+
52
+ stripped_body = re.sub(r'#.*', '', body).strip()
53
+ stripped_body = re.sub(r'\*\*.*?\*\*', '', stripped_body).strip()
54
+ if len(stripped_body) < MINIMUM_PR_BODY_LENGTH:
55
+ violations.append("PR body too short -- provide meaningful context for reviewers")
56
+
57
+ vague_matches = VAGUE_LANGUAGE_PATTERN.findall(body)
58
+ if vague_matches:
59
+ violations.append(f"Vague language detected: {', '.join(vague_matches)} -- be specific about what changed and why")
60
+
61
+ return violations
62
+
63
+
64
+ def main() -> None:
7
65
  try:
8
66
  input_data = json.load(sys.stdin)
9
67
  except json.JSONDecodeError:
@@ -19,62 +77,29 @@ def main():
19
77
 
20
78
  is_pr_create = "gh pr create" in command and ("--body" in command or "-b " in command)
21
79
  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
80
 
24
- if not (is_pr_create or is_pr_edit or is_commit):
81
+ if not (is_pr_create or is_pr_edit):
25
82
  sys.exit(0)
26
83
 
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():]
84
+ body = extract_body_from_command(command)
43
85
 
44
86
  if not body:
45
87
  sys.exit(0)
46
88
 
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}")
89
+ violations = validate_pr_body(body)
70
90
 
71
91
  if violations:
72
92
  violation_list = "; ".join(violations)
93
+ pr_guide_reference = f" @{PR_GUIDE_PATH}" if os.path.exists(PR_GUIDE_PATH) else ""
94
+ denial_reason = (
95
+ f"BLOCKED: [PR_DESCRIPTION] {violation_list}. "
96
+ f"Follow the PR description guide:{pr_guide_reference}"
97
+ )
73
98
  result = {
74
99
  "hookSpecificOutput": {
75
100
  "hookEventName": "PreToolUse",
76
101
  "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\").",
102
+ "permissionDecisionReason": denial_reason,
78
103
  }
79
104
  }
80
105
  print(json.dumps(result))
@@ -11,12 +11,27 @@ This catches:
11
11
  Works in both WSL and Windows for any Python project with a git root.
12
12
  Project root is discovered via CLAUDE_PROJECT_ROOT env var or git rev-parse.
13
13
  """
14
+ import importlib
14
15
  import json
15
16
  import os
16
17
  import platform
17
18
  import subprocess
18
19
  import sys
19
20
  from pathlib import Path
21
+ from types import ModuleType
22
+
23
+ NOTIFICATION_UTILS_DIRECTORY = os.path.join(
24
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "notification"
25
+ )
26
+ sys.path.insert(0, NOTIFICATION_UTILS_DIRECTORY)
27
+
28
+
29
+ def load_notification_utils() -> ModuleType | None:
30
+ try:
31
+ return importlib.import_module("notification_utils")
32
+ except ImportError:
33
+ return None
34
+
20
35
 
21
36
  IS_WINDOWS = platform.system() == "Windows"
22
37
 
@@ -104,6 +119,25 @@ def format_error_summary(all_error_lines: list[str]) -> str:
104
119
  return error_summary
105
120
 
106
121
 
122
+ def send_block_notification(error_summary: str) -> None:
123
+ notification_module = load_notification_utils()
124
+ if notification_module is None:
125
+ return
126
+
127
+ notification_title = "Mypy Type Errors"
128
+ notification_body = f"Write blocked: {error_summary[:200]}"
129
+
130
+ try:
131
+ if notification_module.is_wsl():
132
+ notification_module.notify_wsl(notification_title, notification_body)
133
+ elif platform.system() == "Linux":
134
+ notification_module.notify_linux()
135
+ elif platform.system() == "Windows":
136
+ notification_module.notify_windows(notification_title, notification_body)
137
+ except (AttributeError, OSError):
138
+ pass
139
+
140
+
107
141
  def build_block_response(error_summary: str) -> dict[str, str | dict[str, str]]:
108
142
  return {
109
143
  "decision": "block",
@@ -171,6 +205,7 @@ def main() -> None:
171
205
  sys.exit(0)
172
206
 
173
207
  error_summary = format_error_summary(all_error_lines)
208
+ send_block_notification(error_summary)
174
209
  block_response = build_block_response(error_summary)
175
210
  print(json.dumps(block_response))
176
211
  sys.exit(0)
@@ -1,10 +1,45 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
+ import importlib
3
4
  import json
4
5
  import os
6
+ import platform
5
7
  import subprocess
6
8
  import sys
7
9
  from pathlib import Path
10
+ from types import ModuleType
11
+
12
+ NOTIFICATION_UTILS_DIRECTORY = os.path.join(
13
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "notification"
14
+ )
15
+ sys.path.insert(0, NOTIFICATION_UTILS_DIRECTORY)
16
+
17
+
18
+ def load_notification_utils() -> ModuleType | None:
19
+ try:
20
+ return importlib.import_module("notification_utils")
21
+ except ImportError:
22
+ return None
23
+
24
+
25
+ def send_format_notification(file_path: str, formatter_name: str) -> None:
26
+ notification_module = load_notification_utils()
27
+ if notification_module is None:
28
+ return
29
+
30
+ notification_title = "Auto-Formatter"
31
+ notification_body = f"{formatter_name} formatted: {Path(file_path).name}"
32
+
33
+ try:
34
+ if notification_module.is_wsl():
35
+ notification_module.notify_wsl(notification_title, notification_body)
36
+ elif platform.system() == "Linux":
37
+ notification_module.notify_linux()
38
+ elif platform.system() == "Windows":
39
+ notification_module.notify_windows(notification_title, notification_body)
40
+ except (AttributeError, OSError):
41
+ pass
42
+
8
43
 
9
44
  PYTHON_EXTENSIONS = {".py"}
10
45
  JS_EXTENSIONS = {".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs"}
@@ -89,6 +124,8 @@ def main() -> None:
89
124
  try:
90
125
  format_run = subprocess.run(each_formatter_command, capture_output=True, text=True, timeout=PYTHON_FORMAT_TIMEOUT_SECONDS)
91
126
  if format_run.returncode == 0:
127
+ formatter_name = each_formatter_command[0] if each_formatter_command[0] != sys.executable else each_formatter_command[2]
128
+ send_format_notification(file_path, formatter_name)
92
129
  break
93
130
  except FileNotFoundError:
94
131
  continue
@@ -98,12 +135,14 @@ def main() -> None:
98
135
  if not has_prettier_config(file_path):
99
136
  sys.exit(0)
100
137
  try:
101
- subprocess.run(
138
+ prettier_run = subprocess.run(
102
139
  ["npx", "--yes", "prettier", "--write", file_path],
103
140
  capture_output=True,
104
141
  text=True,
105
142
  timeout=JS_FORMAT_TIMEOUT_SECONDS,
106
143
  )
144
+ if prettier_run.returncode == 0:
145
+ send_format_notification(file_path, "prettier")
107
146
  except (FileNotFoundError, subprocess.TimeoutExpired):
108
147
  pass
109
148
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.2.1",
3
+ "version": "1.4.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,7 +29,7 @@ Scan the current repo root for these directories:
29
29
  | `commands/` | Slash commands (.md) | `~/.claude/commands/` |
30
30
  | `agents/` | Agent definitions (.md) | `~/.claude/agents/` |
31
31
  | `skills/` | Skill packages (subdirs) | `~/.claude/skills/` |
32
- | `hooks/` | Hook scripts + hooks.json | Merge into `~/.claude/settings.json` |
32
+ | `hooks/` | Hook scripts (+ optional hooks.json manifest) | Merge into `~/.claude/settings.json` |
33
33
 
34
34
  Use Glob to find which directories exist and count files in each. Report findings to the user.
35
35
 
@@ -97,6 +97,10 @@ If hooks/hooks.json exists:
97
97
  - Append new groups
98
98
  - Write settings.json with `JSON.stringify(data, null, 4)`
99
99
 
100
+ Important runtime note:
101
+ - `~/.claude/settings.json` is the runtime source of truth for hooks.
102
+ - `hooks/hooks.json` is a packaging/install manifest input for merge workflows, not a runtime file Claude reads directly.
103
+
100
104
  Print summary:
101
105
  ```
102
106
  Installed <package-name>:
@@ -176,8 +180,8 @@ When ready to publish: `npm publish`
176
180
  ## Remember
177
181
 
178
182
  - Zero external dependencies — only Node.js built-ins
179
- - hooks.json in the repo stays canonical (python3 + ${CLAUDE_PLUGIN_ROOT})
180
- - Rewriting happens only in the destination settings.json
183
+ - If present, hooks.json in the repo stays canonical (python3 + ${CLAUDE_PLUGIN_ROOT})
184
+ - Rewriting happens only in the destination settings.json (runtime source of truth)
181
185
  - path.join() everywhere — never concatenate paths with `/` or `\`
182
186
  - Idempotent: running twice produces the same result
183
187
  - Log every file action so the user sees what changed
@@ -1,44 +0,0 @@
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()
@@ -1,69 +0,0 @@
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()
@@ -1,74 +0,0 @@
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()
@@ -1,30 +0,0 @@
1
- #!/usr/bin/env python3
2
- import json
3
- import sys
4
-
5
- BULK_UPDATE_KEYWORDS = ["update all", "replace all", "change all", "fix all", "rename all"]
6
-
7
- BULK_UPDATE_REMINDER = "BULK UPDATE DETECTED: Use a Python script with --preview/--apply instead of line-by-line edits."
8
-
9
-
10
- def main() -> None:
11
- try:
12
- input_data = json.load(sys.stdin)
13
- except json.JSONDecodeError:
14
- sys.exit(0)
15
-
16
- prompt = input_data.get("prompt", "")
17
-
18
- if not prompt:
19
- sys.exit(0)
20
-
21
- message_lower = prompt.lower()
22
-
23
- if any(keyword in message_lower for keyword in BULK_UPDATE_KEYWORDS):
24
- print(BULK_UPDATE_REMINDER)
25
-
26
- sys.exit(0)
27
-
28
-
29
- if __name__ == "__main__":
30
- main()
@@ -1,97 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- UserPromptSubmit hook that detects code-related requests and injects CODE_RULES.md reminder.
4
- Triggers on keywords indicating code writing, planning, or implementation.
5
- """
6
-
7
- import json
8
- import re
9
- import sys
10
-
11
- CODE_KEYWORDS = [
12
- # Creation verbs
13
- r'\b(write|create|implement|add|build|make|generate|develop|setup|scaffold|bootstrap|initialize|init|compose|construct|define|declare|register|wire|connect|integrate|introduce)\b',
14
- # Code nouns
15
- r'\b(code|function|class|method|script|module|component|hook|test|spec|api|endpoint|route|handler|service|util|helper|factory|interface|type|enum|constant|variable|parameter|argument|logic|algorithm|feature|library|package|dependency|plugin|extension|widget|element|node|token|parser|serializer|validator|formatter|linter|compiler|transpiler|bundler|loader|resolver|provider|consumer|producer|subscriber|publisher|emitter|dispatcher|reducer|selector|adapter|wrapper|decorator|mixin|trait|protocol|abstract|generic|iterator|generator|coroutine|fiber|thread|process|worker|job|task|queue|stack|buffer|stream|pipe|socket|channel|signal|slot|observer|mediator|strategy|command|visitor|singleton|repository|gateway|mapper|transformer|converter|encoder|decoder|interceptor|guard|filter|middleware|pipeline|chain|proxy|facade|bridge|flyweight|memento|prototype)\b',
16
- # Modification verbs
17
- r'\b(fix|update|refactor|modify|change|edit|rewrite|improve|enhance|optimize|debug|patch|correct|adjust|tweak|rework|revise|extend|expand|rename|move|extract|inline|split|merge|combine|consolidate|simplify|clean|cleanup|reorganize|restructure|decouple|encapsulate|abstract|generalize|specialize|upgrade|downgrade|migrate|convert|transform|adapt|port|backport)\b',
18
- # Deletion/removal verbs
19
- r'\b(delete|remove|drop|deprecate|disable|deactivate|unregister|detach|disconnect|unbind|unsubscribe|uninstall|prune|trim|strip|purge|clear|reset|destroy|dispose|release|free|deallocate)\b',
20
- # Planning verbs
21
- r'\b(plan|design|structure|architect|outline|draft|sketch|propose|suggest|approach|strategy|solution|how would|how should|how do|how can|how to|what if|where should|when should|why does|why is|could we|should we|can we|let.s|need to|want to|going to)\b',
22
- # Review/analysis verbs
23
- r'\b(review|check|analyze|audit|inspect|examine|validate|verify|assess|evaluate|trace|profile|benchmark|measure|monitor|diagnose|troubleshoot|investigate|identify|detect|discover|locate|find the bug|root cause)\b',
24
- # Testing verbs
25
- r'\b(test|run tests|unit test|integration test|e2e test|end.to.end|assert|expect|mock|stub|spy|fake|fixture|setup|teardown|arrange|act|coverage|regression|smoke test|snapshot|parameterize)\b',
26
- # File types
27
- r'\.(py|js|ts|tsx|jsx|css|scss|less|html|json|yaml|yml|toml|ini|cfg|sql|sh|bash|zsh|vue|svelte|go|rs|java|kt|swift|rb|php|c|cpp|h|hpp|cs|fs|ex|exs|erl|hs|ml|clj|scala|groovy|dart|lua|r|jl|nim|zig|wasm|graphql|proto|tf|hcl)\b',
28
- # Programming concepts
29
- r'\b(loop|condition|if statement|switch|try|catch|exception|error handling|async|await|promise|callback|event|listener|state|props|render|return|import|export|inherit|extend|override|decorator|middleware|migration|schema|model|view|controller|template|query|mutation|subscription|context|scope|closure|binding|reference|pointer|memory|allocation|garbage collection|concurrency|parallelism|synchronization|deadlock|race condition|mutex|semaphore|lock|atomic|transaction|rollback|commit|index|constraint|foreign key|primary key|join|aggregate|subquery|cursor|trigger|stored procedure|materialized view)\b',
30
- # DevOps/infrastructure
31
- r'\b(deploy|release|publish|ship|rollout|rollback|ci|cd|pipeline|docker|container|kubernetes|k8s|terraform|ansible|nginx|apache|server|cluster|replica|shard|partition|load balancer|proxy|cdn|ssl|tls|certificate|dns|domain|cors|csp|firewall|vpc|subnet|security group|iam|role|policy|secret|vault|env var|environment variable|configuration|config file|dotenv)\b',
32
- # Database/data
33
- r'\b(database|db|sql|nosql|mongo|postgres|mysql|sqlite|redis|elasticsearch|dynamodb|cassandra|orm|queryset|recordset|dataset|dataframe|csv|parquet|avro|protobuf|graphql|rest|grpc|websocket|sse|webhook|polling|pagination|cursor|offset|limit|batch|bulk|upsert|crud)\b',
34
- # Sample/example requests
35
- r'\b(example|sample|snippet|demo|prototype|proof of concept|poc|skeleton|boilerplate|starter|template|scaffold|seed|initial|baseline|reference implementation|minimal|basic|simple|quick|small)\b',
36
- # Common tool/framework references
37
- r'\b(django|flask|fastapi|express|next|react|vue|angular|svelte|tailwind|bootstrap|jest|pytest|mocha|cypress|playwright|selenium|webpack|vite|rollup|esbuild|npm|yarn|pnpm|pip|poetry|cargo|gradle|maven|cmake|bazel|make|dockerfile|compose|github|gitlab|bitbucket|jira|confluence)\b',
38
- ]
39
-
40
- CONDENSED_RULES = """
41
- <code-rules-reminder>
42
- ## MANDATORY CODE RULES - APPLY TO ALL CODE (samples, plans, implementations)
43
-
44
- 1. **NO COMMENTS** - Self-documenting names only
45
- - BAD: `d = 0.5 # delay` -> GOOD: `delay_between_retries_seconds = 0.5`
46
-
47
- 2. **NO MAGIC VALUES** - Everything named and in config
48
- - BAD: `if score > 0.8:` -> GOOD: `if score > MINIMUM_CONFIDENCE_THRESHOLD:`
49
-
50
- 3. **NO ABBREVIATIONS** - Full words always
51
- - BAD: `ctx`, `cfg`, `msg`, `btn` -> GOOD: `context`, `configuration`, `message`, `button`
52
-
53
- 4. **COMPLETE TYPE HINTS** - All parameters and returns typed, no `Any`
54
-
55
- 5. **CENTRALIZED CONFIG** - Constants in config/, imported everywhere
56
-
57
- 6. **SEARCH BEFORE CREATE** - Use everything-search skill before defining constants
58
-
59
- 7. **ALL IMPORTS SHOWN** - Every code block includes its imports
60
-
61
- 8. **SELF-CONTAINED COMPONENTS** - Components own their modals/toasts/state
62
-
63
- CHECKLIST before writing ANY code:
64
- [ ] No comments (names explain everything)
65
- [ ] No magic values (all named constants)
66
- [ ] No abbreviations (full words)
67
- [ ] Complete types (all params + returns)
68
- [ ] Imports shown
69
-
70
- SCOPE: These rules apply to code you WRITE or MODIFY. Do NOT fix violations in untouched code unless explicitly instructed.
71
- </code-rules-reminder>
72
- """
73
-
74
-
75
- def main() -> None:
76
- try:
77
- hook_input = json.load(sys.stdin)
78
- except json.JSONDecodeError:
79
- sys.exit(0)
80
-
81
- prompt = hook_input.get("prompt", "")
82
-
83
- if not prompt:
84
- sys.exit(0)
85
-
86
- message_lower = prompt.lower()
87
-
88
- for pattern in CODE_KEYWORDS:
89
- if re.search(pattern, message_lower, re.IGNORECASE):
90
- print(CONDENSED_RULES)
91
- sys.exit(0)
92
-
93
- sys.exit(0)
94
-
95
-
96
- if __name__ == "__main__":
97
- main()
@@ -1,39 +0,0 @@
1
- #!/usr/bin/env python3
2
- import json
3
- import os
4
- import sys
5
-
6
- PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
7
- CODE_RULES_PATH = os.path.join(PLUGIN_ROOT, "docs", "CODE_RULES.md")
8
-
9
-
10
- def load_code_rules() -> str:
11
- try:
12
- with open(CODE_RULES_PATH, encoding="utf-8") as code_rules_file:
13
- return code_rules_file.read()
14
- except (FileNotFoundError, OSError):
15
- return ""
16
-
17
-
18
- def main() -> None:
19
- try:
20
- json.load(sys.stdin)
21
- except json.JSONDecodeError:
22
- sys.exit(0)
23
-
24
- code_rules_content = load_code_rules()
25
- if not code_rules_content:
26
- sys.exit(0)
27
-
28
- reinject_payload = {
29
- "hookSpecificOutput": {
30
- "hookEventName": "SessionStart",
31
- "additionalContext": code_rules_content,
32
- }
33
- }
34
- print(json.dumps(reinject_payload))
35
- sys.exit(0)
36
-
37
-
38
- if __name__ == "__main__":
39
- main()
@@ -1,140 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- UserPromptSubmit hook that detects "hook" mentions and injects
4
- context about our current hook structure and how to add new hooks.
5
- """
6
-
7
- import json
8
- import sys
9
-
10
-
11
- TRIGGER_PHRASES = [
12
- "hook",
13
- ]
14
-
15
- EXCLUDE_PHRASES = [
16
- "react hook",
17
- "usehook",
18
- "usestate",
19
- "useeffect",
20
- "custom hook",
21
- "git hook",
22
- "pre-commit hook",
23
- "webhook",
24
- ]
25
-
26
- CONTEXT = """
27
- <hook-structure-context>
28
- ## Claude Code Hook System
29
-
30
- ### Architecture
31
- - **Runner pattern**: `run-hook-wrapper.js` (Node.js) -> `run-hook.py` (Python) -> individual hook
32
- - **Hook directory**: `hooks/`
33
-
34
- ### Hook Organization (subfolder structure)
35
- ```
36
- hooks/
37
- |-- rewrite-plugin-paths.py
38
- |-- session/
39
- | |-- compact-context-reinject.py
40
- | |-- plugin-data-dir-cleanup.py
41
- | +-- hook-structure-context.py
42
- |-- notification/
43
- | |-- attention-needed-notify.py
44
- | |-- claude-notification-handler.py
45
- | +-- notification_utils.py
46
- |-- advisory/
47
- | |-- refactor-guard.py
48
- | +-- migration-safety-advisor.py
49
- |-- validation/
50
- | |-- code-style-validator.py
51
- | |-- hook-format-validator.py
52
- | |-- mypy_validator.py
53
- | +-- e2e-test-validator.py
54
- |-- lifecycle/
55
- | |-- config-change-guard.py
56
- | +-- session-end-cleanup.py
57
- |-- blocking/
58
- | |-- pyautogui-scroll-blocker.py
59
- | |-- sensitive-file-protector.py
60
- | |-- write-existing-file-blocker.py
61
- | +-- destructive-command-blocker.py
62
- |-- git-hooks/
63
- | +-- post-commit.py
64
- |-- github-action/
65
- | +-- test_workflow.py
66
- +-- validators/
67
- |-- (validation check modules)
68
- +-- test_files/
69
- ```
70
-
71
- ### Event Types
72
- | Event | When | Input (stdin JSON) | Can Block? |
73
- |-------|------|-------------------|------------|
74
- | SessionStart | Session begins | `{}` | No |
75
- | UserPromptSubmit | User sends message | `{"prompt": "..."}` | No (advisory) |
76
- | PreToolUse | Before tool execution | `{"tool_name": "...", "tool_input": {...}}` | YES (exit 2) |
77
- | PostToolUse | After tool execution | `{"tool_name": "...", "tool_input": {...}, "tool_output": "..."}` | No |
78
- | SubagentStop | Agent completes | `{"agent_type": "...", ...}` | No |
79
- | Stop | Session ends | `{}` | No |
80
-
81
- ### How to Add a New Hook
82
-
83
- 1. **Create the hook file** in the appropriate subfolder:
84
- ```python
85
- #!/usr/bin/env python3
86
- import json
87
- import sys
88
-
89
- hook_input = json.load(sys.stdin)
90
- print("Context to inject")
91
- sys.exit(0)
92
- ```
93
-
94
- 2. **Register in settings.json** under the correct event type:
95
- ```json
96
- {
97
- "type": "command",
98
- "command": "node -e \\"process.argv.splice(1,0,'_');require(require('os').homedir()+'/.claude/hooks/run-hook-wrapper.js')\\" \\"session/your-hook.py\\"",
99
- "timeout": 15000
100
- }
101
- ```
102
-
103
- 3. **Key rules**:
104
- - Always use the `run-hook-wrapper.js` pattern (cross-platform)
105
- - Set explicit timeouts (10000-30000ms)
106
- - PreToolUse hooks use `matcher` to scope which tools they fire on
107
- - UserPromptSubmit hooks match on `prompt` content
108
- - Print output = context injected into Claude's conversation
109
- - Advisory hooks exit 0; blocking hooks exit 2 with `hookSpecificOutput.permissionDecision`
110
- </hook-structure-context>
111
- """
112
-
113
-
114
- def main() -> None:
115
- try:
116
- hook_input = json.load(sys.stdin)
117
- except json.JSONDecodeError:
118
- sys.exit(0)
119
-
120
- prompt = hook_input.get("prompt", "")
121
-
122
- if not prompt:
123
- sys.exit(0)
124
-
125
- message_lower = prompt.lower()
126
-
127
- for exclude in EXCLUDE_PHRASES:
128
- if exclude in message_lower:
129
- sys.exit(0)
130
-
131
- for phrase in TRIGGER_PHRASES:
132
- if phrase in message_lower:
133
- print(CONTEXT)
134
- sys.exit(0)
135
-
136
- sys.exit(0)
137
-
138
-
139
- if __name__ == "__main__":
140
- main()
@@ -1,145 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Code style validator - checks for common style issues.
4
-
5
- - 4-space indentation (not tabs, not 2 spaces)
6
- - Single newlines between functions (not double)
7
- - Single newlines between class methods
8
- """
9
- import json
10
- import re
11
- import sys
12
-
13
-
14
- def check_indentation(content: str) -> list[str]:
15
- """Check for non-4-space indentation."""
16
- issues = []
17
- lines = content.split('\n')
18
-
19
- for line_num, line in enumerate(lines, 1):
20
- if not line or not line[0].isspace():
21
- continue
22
-
23
- # Check for tabs
24
- if '\t' in line:
25
- issues.append(f"Line {line_num}: Tab indentation - use 4 spaces")
26
- continue
27
-
28
- # Get leading spaces
29
- stripped = line.lstrip(' ')
30
- indent = len(line) - len(stripped)
31
-
32
- # Check if indent is multiple of 4
33
- if indent > 0 and indent % 4 != 0:
34
- issues.append(f"Line {line_num}: {indent}-space indent - use 4 spaces")
35
-
36
- return issues[:5] # Limit to first 5
37
-
38
-
39
- def check_function_spacing(content: str) -> list[str]:
40
- """Check for excessive blank lines between code blocks.
41
-
42
- Detects 2+ consecutive blank lines anywhere in the file, plus validates
43
- correct spacing before function/method/class definitions.
44
- """
45
- issues = []
46
- lines = content.split('\n')
47
-
48
- func_pattern = re.compile(r'^(\s*)(async\s+)?def\s+\w+')
49
- class_pattern = re.compile(r'^class\s+\w+')
50
-
51
- consecutive_blank_count = 0
52
- blank_run_start_line = 0
53
- prev_was_code = False
54
-
55
- for line_num, line in enumerate(lines, 1):
56
- stripped = line.strip()
57
-
58
- if not stripped:
59
- if consecutive_blank_count == 0:
60
- blank_run_start_line = line_num
61
- consecutive_blank_count += 1
62
- continue
63
-
64
- if consecutive_blank_count >= 3:
65
- issues.append(f"Line {blank_run_start_line}: {consecutive_blank_count} consecutive blank lines - max 2 allowed")
66
-
67
- func_match = func_pattern.match(line)
68
- class_match = class_pattern.match(line)
69
-
70
- if func_match and prev_was_code:
71
- indent = len(func_match.group(1)) if func_match.group(1) else 0
72
-
73
- if indent == 0:
74
- if consecutive_blank_count != 2:
75
- issues.append(f"Line {line_num}: Top-level function needs 2 blank lines above (has {consecutive_blank_count})")
76
- else:
77
- if consecutive_blank_count != 1:
78
- issues.append(f"Line {line_num}: Method needs 1 blank line above (has {consecutive_blank_count})")
79
-
80
- elif class_match and prev_was_code:
81
- if consecutive_blank_count != 2:
82
- issues.append(f"Line {line_num}: Class needs 2 blank lines above (has {consecutive_blank_count})")
83
-
84
- consecutive_blank_count = 0
85
- prev_was_code = not stripped.startswith('#') and not stripped.startswith('@')
86
-
87
- return issues[:5]
88
-
89
-
90
- def main() -> None:
91
- try:
92
- input_data = json.load(sys.stdin)
93
- except json.JSONDecodeError:
94
- sys.exit(0)
95
-
96
- tool_input = input_data.get("tool_input", {})
97
- file_path = tool_input.get("file_path", "")
98
-
99
- if not file_path:
100
- sys.exit(0)
101
-
102
- # Only check Python files
103
- if not file_path.endswith('.py'):
104
- sys.exit(0)
105
-
106
- # Skip test files (more lenient)
107
- if 'test' in file_path.lower() or 'conftest' in file_path.lower():
108
- sys.exit(0)
109
-
110
- tool_name = input_data.get("tool_name", "")
111
- content = tool_input.get("content", "") or tool_input.get("new_string", "")
112
-
113
- if not content:
114
- sys.exit(0)
115
-
116
- if tool_name == "Write":
117
- try:
118
- with open(file_path, "r", encoding="utf-8") as existing_file:
119
- existing_content = existing_file.read()
120
- if existing_content:
121
- sys.exit(0)
122
- except (FileNotFoundError, OSError, UnicodeDecodeError):
123
- pass
124
-
125
- issues = []
126
- issues.extend(check_indentation(content))
127
- issues.extend(check_function_spacing(content))
128
-
129
- if issues:
130
- issue_list = "; ".join(issues)
131
- result = {
132
- "hookSpecificOutput": {
133
- "hookEventName": "PreToolUse",
134
- "permissionDecision": "ask",
135
- "permissionDecisionReason": f"[Code Style] {len(issues)} issue(s): {issue_list}. Fix or proceed?"
136
- }
137
- }
138
- print(json.dumps(result))
139
- sys.stdout.flush()
140
-
141
- sys.exit(0)
142
-
143
-
144
- if __name__ == "__main__":
145
- main()
@@ -1,142 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Validate E2E test consistency between online/offline specs.
4
-
5
- Two checks:
6
- 1. Naming: offline tests must mirror online test names with " offline" suffix.
7
- 2. Coverage: when a new online e2e test file is written, a corresponding
8
- offline equivalent must exist. Blocks if missing.
9
-
10
- Triggered as PostToolUse hook when editing spec files.
11
- """
12
-
13
- import json
14
- import os
15
- import re
16
- import sys
17
- from pathlib import Path
18
-
19
-
20
- E2E_TEST_DIRECTORY = "frontend/tests/e2e"
21
-
22
-
23
- def extract_test_names(file_path: Path) -> set[str]:
24
- """Extract test names from spec file."""
25
- content = file_path.read_text()
26
- pattern = r"test\(['\"]([^'\"]+)['\"]"
27
- return set(re.findall(pattern, content))
28
-
29
-
30
- def validate_e2e_naming(project_root: Path) -> list[str]:
31
- """Return list of naming violations.
32
-
33
- Only validates tests that follow the naming convention (end with " offline").
34
- Legacy tests without the suffix are ignored - they may intentionally differ.
35
- """
36
- online = project_root / E2E_TEST_DIRECTORY / "online.spec.ts"
37
- offline = project_root / E2E_TEST_DIRECTORY / "offline.spec.ts"
38
-
39
- if not online.exists() or not offline.exists():
40
- return []
41
-
42
- online_tests = extract_test_names(online)
43
- offline_tests = extract_test_names(offline)
44
-
45
- violations = []
46
-
47
- for test in offline_tests:
48
- if not test.endswith(" offline"):
49
- continue
50
-
51
- online_name = test.removesuffix(" offline")
52
- if online_name not in online_tests:
53
- violations.append(f"No online pair for: '{test}'")
54
-
55
- return violations
56
-
57
-
58
- def validate_offline_coverage(file_path: str, project_root: Path) -> list[str]:
59
- """Check that online e2e test files have a corresponding offline file.
60
-
61
- When a new online spec file is written, the offline equivalent must exist.
62
- Returns blocking violations if offline file is missing.
63
- """
64
- e2e_directory = project_root / E2E_TEST_DIRECTORY
65
- file_name = Path(file_path).name
66
-
67
- if "offline" in file_name:
68
- return []
69
-
70
- if not file_name.endswith(".spec.ts"):
71
- return []
72
-
73
- offline_name = file_name.replace(".spec.ts", ".offline.spec.ts")
74
- if file_name == "online.spec.ts":
75
- offline_name = "offline.spec.ts"
76
-
77
- offline_path = e2e_directory / offline_name
78
- if not offline_path.exists():
79
- return [f"Missing offline equivalent: {offline_name} required for {file_name}"]
80
-
81
- return []
82
-
83
-
84
- def main() -> None:
85
- """Hook entry point - reads tool input from stdin."""
86
- try:
87
- input_data = json.load(sys.stdin)
88
- except json.JSONDecodeError:
89
- sys.exit(0)
90
-
91
- tool_input = input_data.get("tool_input", {})
92
- file_path = tool_input.get("file_path", "")
93
-
94
- if not file_path:
95
- sys.exit(0)
96
-
97
- if ".spec.ts" not in file_path:
98
- sys.exit(0)
99
-
100
- path_object = Path(file_path)
101
- project_root = path_object.parent
102
- while project_root != project_root.parent:
103
- if (project_root / E2E_TEST_DIRECTORY).exists():
104
- break
105
- project_root = project_root.parent
106
- else:
107
- sys.exit(0)
108
-
109
- if not (project_root / E2E_TEST_DIRECTORY).exists():
110
- sys.exit(0)
111
-
112
- naming_violations = validate_e2e_naming(project_root)
113
- coverage_violations = validate_offline_coverage(file_path, project_root)
114
-
115
- if coverage_violations:
116
- violation_list = "; ".join(coverage_violations)
117
- result = {
118
- "hookSpecificOutput": {
119
- "hookEventName": "PostToolUse",
120
- "additionalContext": f"[E2E COVERAGE] Offline test required: {violation_list}"
121
- }
122
- }
123
- print(json.dumps(result))
124
- sys.stdout.flush()
125
- sys.exit(0)
126
-
127
- if naming_violations:
128
- violation_list = "; ".join(naming_violations)
129
- result = {
130
- "hookSpecificOutput": {
131
- "hookEventName": "PostToolUse",
132
- "additionalContext": f"[E2E NAMING] {violation_list}. Offline tests must mirror online names with ' offline' suffix."
133
- }
134
- }
135
- print(json.dumps(result))
136
- sys.stdout.flush()
137
-
138
- sys.exit(0)
139
-
140
-
141
- if __name__ == "__main__":
142
- main()