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,206 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
NTFY_TOPIC = os.environ.get("NTFY_TOPIC", "claude-notifications")
|
|
8
|
+
NTFY_BASE_URL = f"https://ntfy.sh/{NTFY_TOPIC}"
|
|
9
|
+
WINDOWS_CHIMES_PATH = os.path.join(os.environ.get("SYSTEMROOT", r"C:\Windows"), "Media", "Windows Battery Critical.wav")
|
|
10
|
+
LINUX_NOTIFICATION_SOUND = os.environ.get("NOTIFICATION_SOUND", "/usr/share/sounds/freedesktop/stereo/message.oga")
|
|
11
|
+
LINUX_NOTIFICATION_TIMEOUT_MS = "3000"
|
|
12
|
+
TOAST_DISPLAY_DURATION_MILLISECONDS = 6000
|
|
13
|
+
DEFAULT_LINUX_TOAST_TITLE = "Claude Code"
|
|
14
|
+
DEFAULT_LINUX_TOAST_MESSAGE = "Waiting for your input"
|
|
15
|
+
|
|
16
|
+
TOAST_SCRIPT_TEMPLATE = r'''
|
|
17
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
18
|
+
Add-Type -AssemblyName System.Drawing
|
|
19
|
+
Add-Type @"
|
|
20
|
+
using System;
|
|
21
|
+
using System.Runtime.InteropServices;
|
|
22
|
+
public class Win32 {{
|
|
23
|
+
[DllImport("user32.dll")]
|
|
24
|
+
public static extern bool SetProcessDPIAware();
|
|
25
|
+
[DllImport("user32.dll")]
|
|
26
|
+
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
|
|
27
|
+
[DllImport("user32.dll")]
|
|
28
|
+
public static extern int GetWindowLong(IntPtr hWnd, int nIndex);
|
|
29
|
+
[DllImport("user32.dll")]
|
|
30
|
+
public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
|
|
31
|
+
[DllImport("user32.dll")]
|
|
32
|
+
public static extern bool SetLayeredWindowAttributes(IntPtr hwnd, uint crKey, byte bAlpha, uint dwFlags);
|
|
33
|
+
public static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
|
|
34
|
+
public const uint SWP_NOACTIVATE = 0x0010;
|
|
35
|
+
public const uint SWP_SHOWWINDOW = 0x0040;
|
|
36
|
+
public const int GWL_EXSTYLE = -20;
|
|
37
|
+
public const int WS_EX_LAYERED = 0x80000;
|
|
38
|
+
public const int WS_EX_TRANSPARENT = 0x20;
|
|
39
|
+
public const uint LWA_ALPHA = 0x2;
|
|
40
|
+
}}
|
|
41
|
+
"@
|
|
42
|
+
|
|
43
|
+
[Win32]::SetProcessDPIAware() | Out-Null
|
|
44
|
+
|
|
45
|
+
$form = New-Object System.Windows.Forms.Form
|
|
46
|
+
$form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::None
|
|
47
|
+
$form.Size = New-Object System.Drawing.Size(520, 110)
|
|
48
|
+
$form.ShowInTaskbar = $false
|
|
49
|
+
$form.BackColor = [System.Drawing.Color]::FromArgb(66, 135, 245)
|
|
50
|
+
$form.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual
|
|
51
|
+
|
|
52
|
+
$screen = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea
|
|
53
|
+
$x = [int]($screen.Left + ($screen.Width - 520) / 2)
|
|
54
|
+
$y = [int]($screen.Bottom - 110 - 50)
|
|
55
|
+
$form.Location = New-Object System.Drawing.Point($x, $y)
|
|
56
|
+
|
|
57
|
+
$inner = New-Object System.Windows.Forms.Panel
|
|
58
|
+
$inner.Size = New-Object System.Drawing.Size(514, 104)
|
|
59
|
+
$inner.Location = New-Object System.Drawing.Point(3, 3)
|
|
60
|
+
$inner.BackColor = [System.Drawing.Color]::FromArgb(45, 45, 45)
|
|
61
|
+
$form.Controls.Add($inner)
|
|
62
|
+
|
|
63
|
+
$titleLabel = New-Object System.Windows.Forms.Label
|
|
64
|
+
$titleLabel.Text = "{title}"
|
|
65
|
+
$titleLabel.Font = New-Object System.Drawing.Font("Segoe UI", 12, [System.Drawing.FontStyle]::Bold)
|
|
66
|
+
$titleLabel.ForeColor = [System.Drawing.Color]::FromArgb(120, 180, 255)
|
|
67
|
+
$titleLabel.AutoSize = $false
|
|
68
|
+
$titleLabel.Size = New-Object System.Drawing.Size(514, 30)
|
|
69
|
+
$titleLabel.Location = New-Object System.Drawing.Point(0, 8)
|
|
70
|
+
$titleLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
|
|
71
|
+
$inner.Controls.Add($titleLabel)
|
|
72
|
+
|
|
73
|
+
$messageLabel = New-Object System.Windows.Forms.Label
|
|
74
|
+
$messageLabel.Text = "{message}"
|
|
75
|
+
$messageLabel.Font = New-Object System.Drawing.Font("Segoe UI", 11)
|
|
76
|
+
$messageLabel.ForeColor = [System.Drawing.Color]::White
|
|
77
|
+
$messageLabel.AutoSize = $false
|
|
78
|
+
$messageLabel.Size = New-Object System.Drawing.Size(500, 58)
|
|
79
|
+
$messageLabel.Location = New-Object System.Drawing.Point(7, 40)
|
|
80
|
+
$messageLabel.TextAlign = [System.Drawing.ContentAlignment]::TopCenter
|
|
81
|
+
$inner.Controls.Add($messageLabel)
|
|
82
|
+
|
|
83
|
+
$timer = New-Object System.Windows.Forms.Timer
|
|
84
|
+
$timer.Interval = {toast_duration}
|
|
85
|
+
$timer.Add_Tick({{ $form.Close() }})
|
|
86
|
+
$timer.Start()
|
|
87
|
+
|
|
88
|
+
$exStyle = [Win32]::GetWindowLong($form.Handle, [Win32]::GWL_EXSTYLE)
|
|
89
|
+
[Win32]::SetWindowLong($form.Handle, [Win32]::GWL_EXSTYLE, $exStyle -bor [Win32]::WS_EX_LAYERED -bor [Win32]::WS_EX_TRANSPARENT)
|
|
90
|
+
[Win32]::SetLayeredWindowAttributes($form.Handle, 0, 230, [Win32]::LWA_ALPHA)
|
|
91
|
+
[Win32]::SetWindowPos($form.Handle, [Win32]::HWND_TOPMOST, $x, $y, 520, 110, [Win32]::SWP_NOACTIVATE -bor [Win32]::SWP_SHOWWINDOW)
|
|
92
|
+
$form.Show()
|
|
93
|
+
[System.Windows.Forms.Application]::Run($form)
|
|
94
|
+
'''
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def is_wsl() -> bool:
|
|
98
|
+
if platform.system() != "Linux":
|
|
99
|
+
return False
|
|
100
|
+
try:
|
|
101
|
+
with open("/proc/version") as proc_version_file:
|
|
102
|
+
return "microsoft" in proc_version_file.read().lower()
|
|
103
|
+
except FileNotFoundError:
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def build_toast_script(title: str, message: str) -> str:
|
|
108
|
+
safe_title = title.replace('"', '`"').replace("'", "`'")
|
|
109
|
+
safe_message = message.replace('"', '`"').replace("'", "`'")
|
|
110
|
+
return TOAST_SCRIPT_TEMPLATE.format(
|
|
111
|
+
title=safe_title,
|
|
112
|
+
message=safe_message,
|
|
113
|
+
toast_duration=TOAST_DISPLAY_DURATION_MILLISECONDS,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def notify_wsl(title: str, message: str) -> None:
|
|
118
|
+
script = build_toast_script(title, message)
|
|
119
|
+
try:
|
|
120
|
+
subprocess.Popen(
|
|
121
|
+
["powershell.exe", "-ExecutionPolicy", "Bypass", "-Command", script],
|
|
122
|
+
stdout=subprocess.DEVNULL,
|
|
123
|
+
stderr=subprocess.DEVNULL,
|
|
124
|
+
start_new_session=True
|
|
125
|
+
)
|
|
126
|
+
except FileNotFoundError:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def notify_windows(title: str, message: str) -> None:
|
|
131
|
+
script = build_toast_script(title, message)
|
|
132
|
+
subprocess.Popen(
|
|
133
|
+
["powershell", "-ExecutionPolicy", "Bypass", "-Command", script],
|
|
134
|
+
stdout=subprocess.DEVNULL,
|
|
135
|
+
stderr=subprocess.DEVNULL,
|
|
136
|
+
creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def notify_ntfy(title: str, message: str, priority: str = "high") -> None:
|
|
141
|
+
try:
|
|
142
|
+
subprocess.Popen(
|
|
143
|
+
[
|
|
144
|
+
"curl", "-s",
|
|
145
|
+
"-H", f"Priority: {priority}",
|
|
146
|
+
"-H", "Tags: bell",
|
|
147
|
+
"-H", f"Title: {title}",
|
|
148
|
+
"-d", message,
|
|
149
|
+
NTFY_BASE_URL,
|
|
150
|
+
],
|
|
151
|
+
stdout=subprocess.DEVNULL,
|
|
152
|
+
stderr=subprocess.DEVNULL
|
|
153
|
+
)
|
|
154
|
+
except FileNotFoundError:
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def notify_linux() -> None:
|
|
159
|
+
subprocess.Popen(
|
|
160
|
+
[
|
|
161
|
+
"notify-send", "-t", LINUX_NOTIFICATION_TIMEOUT_MS,
|
|
162
|
+
"-u", "normal", "-i", "dialog-warning",
|
|
163
|
+
DEFAULT_LINUX_TOAST_TITLE, DEFAULT_LINUX_TOAST_MESSAGE,
|
|
164
|
+
],
|
|
165
|
+
stdout=subprocess.DEVNULL,
|
|
166
|
+
stderr=subprocess.DEVNULL
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def sound_windows() -> None:
|
|
171
|
+
subprocess.Popen(
|
|
172
|
+
["powershell", "-WindowStyle", "Hidden", "-Command", f"(New-Object Media.SoundPlayer '{WINDOWS_CHIMES_PATH}').PlaySync()"],
|
|
173
|
+
stdout=subprocess.DEVNULL,
|
|
174
|
+
stderr=subprocess.DEVNULL,
|
|
175
|
+
creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def sound_wsl() -> None:
|
|
180
|
+
try:
|
|
181
|
+
subprocess.Popen(
|
|
182
|
+
["powershell.exe", "-WindowStyle", "Hidden", "-Command", f"(New-Object Media.SoundPlayer '{WINDOWS_CHIMES_PATH}').PlaySync()"],
|
|
183
|
+
stdout=subprocess.DEVNULL,
|
|
184
|
+
stderr=subprocess.DEVNULL
|
|
185
|
+
)
|
|
186
|
+
except FileNotFoundError:
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def sound_linux() -> None:
|
|
191
|
+
if os.path.exists(LINUX_NOTIFICATION_SOUND):
|
|
192
|
+
for each_player in ["paplay", "aplay", "play"]:
|
|
193
|
+
try:
|
|
194
|
+
subprocess.Popen(
|
|
195
|
+
[each_player, LINUX_NOTIFICATION_SOUND],
|
|
196
|
+
stdout=subprocess.DEVNULL,
|
|
197
|
+
stderr=subprocess.DEVNULL
|
|
198
|
+
)
|
|
199
|
+
return
|
|
200
|
+
except FileNotFoundError:
|
|
201
|
+
continue
|
|
202
|
+
print("\a", end="", flush=True)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def get_project_name() -> str:
|
|
206
|
+
return os.path.basename(os.getcwd())
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Rewrite plugin config paths to match the current platform.
|
|
3
|
+
|
|
4
|
+
Solves the cross-platform path problem for shared ~/.claude directories.
|
|
5
|
+
On WSL/Docker, rewrites Windows paths to Linux paths.
|
|
6
|
+
On Windows, rewrites Linux paths back to Windows paths (heals WSL damage).
|
|
7
|
+
|
|
8
|
+
Uses json.load/json.dump to avoid all backslash escaping issues.
|
|
9
|
+
Called from Docker entrypoint.sh and can be called from session hooks.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from typing import Union
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
PLUGIN_CONFIG_FILENAMES = [
|
|
19
|
+
"known_marketplaces.json",
|
|
20
|
+
"installed_plugins.json",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
WINDOWS_PATH_PREFIX = os.path.join("C:\\Users", os.environ.get("USERNAME", "unknown"), ".claude")
|
|
24
|
+
LINUX_HOME_PATH_PREFIX = os.path.join(os.path.expanduser("~"), ".claude") if os.name != "nt" else ""
|
|
25
|
+
|
|
26
|
+
JsonValue = Union[str, int, float, bool, None, list, dict]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def detect_local_claude_directory() -> str:
|
|
30
|
+
return os.path.join(os.path.expanduser("~"), ".claude")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def rewrite_paths_in_value(
|
|
34
|
+
value: JsonValue,
|
|
35
|
+
from_prefix: str,
|
|
36
|
+
to_prefix: str,
|
|
37
|
+
) -> JsonValue:
|
|
38
|
+
if isinstance(value, str):
|
|
39
|
+
return value.replace(from_prefix, to_prefix)
|
|
40
|
+
if isinstance(value, dict):
|
|
41
|
+
return {
|
|
42
|
+
each_key: rewrite_paths_in_value(each_value, from_prefix, to_prefix)
|
|
43
|
+
for each_key, each_value in value.items()
|
|
44
|
+
}
|
|
45
|
+
if isinstance(value, list):
|
|
46
|
+
return [
|
|
47
|
+
rewrite_paths_in_value(each_element, from_prefix, to_prefix)
|
|
48
|
+
for each_element in value
|
|
49
|
+
]
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def rewrite_plugin_config_file(
|
|
54
|
+
file_path: str,
|
|
55
|
+
from_prefix: str,
|
|
56
|
+
to_prefix: str,
|
|
57
|
+
) -> bool:
|
|
58
|
+
try:
|
|
59
|
+
with open(file_path, "r", encoding="utf-8") as source_file:
|
|
60
|
+
parsed_config = json.load(source_file)
|
|
61
|
+
except FileNotFoundError:
|
|
62
|
+
return False
|
|
63
|
+
except json.JSONDecodeError as e:
|
|
64
|
+
print(
|
|
65
|
+
f"[rewrite-paths] ERROR: Invalid JSON in {file_path}: {e}",
|
|
66
|
+
file=sys.stderr,
|
|
67
|
+
)
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
rewritten_config = rewrite_paths_in_value(
|
|
71
|
+
parsed_config,
|
|
72
|
+
from_prefix,
|
|
73
|
+
to_prefix,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if rewritten_config == parsed_config:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
with open(file_path, "w", encoding="utf-8") as destination_file:
|
|
80
|
+
json.dump(rewritten_config, destination_file, indent=2)
|
|
81
|
+
destination_file.write("\n")
|
|
82
|
+
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def main() -> None:
|
|
87
|
+
is_windows = os.name == "nt"
|
|
88
|
+
local_claude_directory = detect_local_claude_directory()
|
|
89
|
+
|
|
90
|
+
if is_windows:
|
|
91
|
+
from_prefix = LINUX_HOME_PATH_PREFIX
|
|
92
|
+
to_prefix = WINDOWS_PATH_PREFIX
|
|
93
|
+
else:
|
|
94
|
+
from_prefix = WINDOWS_PATH_PREFIX
|
|
95
|
+
to_prefix = local_claude_directory
|
|
96
|
+
|
|
97
|
+
plugins_directory = os.path.join(local_claude_directory, "plugins")
|
|
98
|
+
|
|
99
|
+
rewritten_count = 0
|
|
100
|
+
for each_filename in PLUGIN_CONFIG_FILENAMES:
|
|
101
|
+
each_file_path = os.path.join(plugins_directory, each_filename)
|
|
102
|
+
was_rewritten = rewrite_plugin_config_file(
|
|
103
|
+
each_file_path,
|
|
104
|
+
from_prefix,
|
|
105
|
+
to_prefix,
|
|
106
|
+
)
|
|
107
|
+
if was_rewritten:
|
|
108
|
+
rewritten_count += 1
|
|
109
|
+
print(
|
|
110
|
+
f"[rewrite-paths] Rewrote paths in {each_filename}",
|
|
111
|
+
file=sys.stderr,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
main()
|
|
@@ -0,0 +1,30 @@
|
|
|
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()
|
|
@@ -0,0 +1,97 @@
|
|
|
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()
|
|
@@ -0,0 +1,39 @@
|
|
|
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()
|
|
@@ -0,0 +1,140 @@
|
|
|
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()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""SessionStart hook — removes empty plugin data directories to prevent EEXIST.
|
|
3
|
+
|
|
4
|
+
Claude Code's infrastructure calls mkdirSync without {recursive: true} when
|
|
5
|
+
setting up plugin data directories for hook execution. If the directory already
|
|
6
|
+
exists from a previous session, the mkdir throws EEXIST and the hook fails.
|
|
7
|
+
|
|
8
|
+
This workaround removes known empty plugin data directories at session start
|
|
9
|
+
so the infrastructure's mkdir succeeds when the Stop hook fires later.
|
|
10
|
+
|
|
11
|
+
Tracking: https://github.com/anthropics/claude-code/issues — unfixed code path
|
|
12
|
+
separate from the EEXIST fixes in v2.1.70-v2.1.72.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
PLUGINS_DATA_DIRECTORY = os.path.join(
|
|
19
|
+
os.path.expanduser("~"), ".claude", "plugins", "data"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
AFFECTED_PLUGIN_DIRECTORIES = [
|
|
23
|
+
"ralph-loop-claude-plugins-official",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> None:
|
|
28
|
+
for plugin_directory_name in AFFECTED_PLUGIN_DIRECTORIES:
|
|
29
|
+
target_path = os.path.join(PLUGINS_DATA_DIRECTORY, plugin_directory_name)
|
|
30
|
+
if not os.path.isdir(target_path):
|
|
31
|
+
continue
|
|
32
|
+
try:
|
|
33
|
+
os.rmdir(target_path)
|
|
34
|
+
except OSError:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
main()
|