cc-hook-registry 5.4.0 → 5.5.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/docs/index.html CHANGED
@@ -34,7 +34,7 @@ a{color:#58a6ff;text-decoration:none}
34
34
  <body>
35
35
  <div class="c">
36
36
  <h1>Claude Code Hook Registry</h1>
37
- <p class="sub">59 hooks from 7 projects. Search, browse, install.</p>
37
+ <p class="sub">54 hooks. Search, browse, install.</p>
38
38
 
39
39
  <input id="q" placeholder="Search hooks... (database, git, deploy, secret, docker)" oninput="render()">
40
40
 
@@ -51,6 +51,7 @@ a{color:#58a6ff;text-decoration:none}
51
51
  </div>
52
52
 
53
53
  <div class="footer">
54
+ <a href="playground.html">Hook Playground</a> ·
54
55
  <a href="https://github.com/yurukusa/cc-hook-registry">GitHub</a> ·
55
56
  <a href="https://www.npmjs.com/package/cc-hook-registry">npm</a> ·
56
57
  <a href="https://yurukusa.github.io/cc-safe-setup/">Safety Audit</a> ·
@@ -84,6 +85,34 @@ const H=[
84
85
  {id:'auto-approve-build',n:'Auto-Approve Build',c:'approve',d:'npm/cargo/go build, test, lint',i:'npx cc-safe-setup --install-example auto-approve-build',t:['build','test','npm']},
85
86
  {id:'auto-approve-python',n:'Auto-Approve Python',c:'approve',d:'pytest, mypy, ruff, black',i:'npx cc-safe-setup --install-example auto-approve-python',t:['python','pytest','lint']},
86
87
  {id:'auto-approve-docker',n:'Auto-Approve Docker',c:'approve',d:'docker build, compose, ps, logs',i:'npx cc-safe-setup --install-example auto-approve-docker',t:['docker','compose']},
88
+ {id:'protect-claudemd',n:'Protect CLAUDE.md',c:'safety',d:'Block edits to CLAUDE.md and settings files',i:'npx cc-safe-setup --install-example protect-claudemd',t:['claudemd','config']},
89
+ {id:'no-sudo-guard',n:'No Sudo',c:'safety',d:'Block all sudo commands',i:'npx cc-safe-setup --install-example no-sudo-guard',t:['sudo','root']},
90
+ {id:'no-install-global',n:'No Global Install',c:'safety',d:'Block npm -g and system pip',i:'npx cc-safe-setup --install-example no-install-global',t:['npm','pip','global']},
91
+ {id:'no-deploy-friday',n:'No Deploy Friday',c:'safety',d:'Block deploys on Fridays',i:'npx cc-safe-setup --install-example no-deploy-friday',t:['deploy','friday']},
92
+ {id:'work-hours-guard',n:'Work Hours Guard',c:'safety',d:'Restrict risky ops outside business hours',i:'npx cc-safe-setup --install-example work-hours-guard',t:['hours','time','night']},
93
+ {id:'auto-approve-go',n:'Auto-Approve Go',c:'approve',d:'go build/test/vet/fmt',i:'npx cc-safe-setup --install-example auto-approve-go',t:['go','golang']},
94
+ {id:'auto-approve-cargo',n:'Auto-Approve Cargo',c:'approve',d:'cargo build/test/clippy',i:'npx cc-safe-setup --install-example auto-approve-cargo',t:['rust','cargo']},
95
+ {id:'auto-approve-make',n:'Auto-Approve Make',c:'approve',d:'make build/test/lint',i:'npx cc-safe-setup --install-example auto-approve-make',t:['make','makefile']},
96
+ {id:'auto-approve-gradle',n:'Auto-Approve Gradle',c:'approve',d:'gradle/gradlew build/test',i:'npx cc-safe-setup --install-example auto-approve-gradle',t:['java','gradle']},
97
+ {id:'auto-approve-maven',n:'Auto-Approve Maven',c:'approve',d:'mvn compile/test/verify',i:'npx cc-safe-setup --install-example auto-approve-maven',t:['java','maven']},
98
+ {id:'verify-before-done',n:'Verify Before Done',c:'quality',d:'Warn when committing without running tests',i:'npx cc-safe-setup --install-example verify-before-done',t:['commit','test','verify']},
99
+ {id:'prompt-injection-guard',n:'Prompt Injection Guard',c:'safety',d:'Detect injection patterns in tool output',i:'npx cc-safe-setup --install-example prompt-injection-guard',t:['injection','security','xss']},
100
+ {id:'disk-space-guard',n:'Disk Space Guard',c:'utility',d:'Warn when disk space is running low',i:'npx cc-safe-setup --install-example disk-space-guard',t:['disk','space','storage']},
101
+ {id:'output-length-guard',n:'Output Length Guard',c:'utility',d:'Warn when tool output exceeds 50K chars',i:'npx cc-safe-setup --install-example output-length-guard',t:['output','context','large']},
102
+ {id:'uncommitted-work-guard',n:'Uncommitted Work Guard',c:'safety',d:'Block destructive git with uncommitted changes',i:'npx cc-safe-setup --install-example uncommitted-work-guard',t:['git','checkout','uncommitted']},
103
+ {id:'test-deletion-guard',n:'Test Deletion Guard',c:'quality',d:'Warn when removing test assertions',i:'npx cc-safe-setup --install-example test-deletion-guard',t:['test','delete','assertion']},
104
+ {id:'overwrite-guard',n:'Overwrite Guard',c:'safety',d:'Warn before silently overwriting files',i:'npx cc-safe-setup --install-example overwrite-guard',t:['write','overwrite','file']},
105
+ {id:'memory-write-guard',n:'Memory Write Guard',c:'safety',d:'Log writes to ~/.claude/ directory',i:'npx cc-safe-setup --install-example memory-write-guard',t:['memory','claude','config']},
106
+ {id:'fact-check-gate',n:'Fact Check Gate',c:'quality',d:'Warn when docs reference unread source files',i:'npx cc-safe-setup --install-example fact-check-gate',t:['docs','hallucination','verify']},
107
+ {id:'token-budget-guard',n:'Token Budget Guard',c:'utility',d:'Estimate and limit session token cost',i:'npx cc-safe-setup --install-example token-budget-guard',t:['cost','token','budget']},
108
+ {id:'conflict-marker-guard',n:'Conflict Marker Guard',c:'quality',d:'Block commits with merge conflict markers',i:'npx cc-safe-setup --install-example conflict-marker-guard',t:['merge','conflict','git']},
109
+ {id:'error-memory-guard',n:'Error Memory Guard',c:'safety',d:'Block retries of commands that already failed 3x',i:'npx cc-safe-setup --install-example error-memory-guard',t:['error','retry','loop']},
110
+ {id:'parallel-edit-guard',n:'Parallel Edit Guard',c:'safety',d:'Detect concurrent edits to same file',i:'npx cc-safe-setup --install-example parallel-edit-guard',t:['parallel','agent','conflict']},
111
+ {id:'large-read-guard',n:'Large Read Guard',c:'utility',d:'Warn before catting large files into context',i:'npx cc-safe-setup --install-example large-read-guard',t:['cat','large','context']},
112
+ {id:'compact-reminder',n:'Compact Reminder',c:'utility',d:'Suggest /compact after N tool calls',i:'npx cc-safe-setup --install-example compact-reminder',t:['compact','context','session']},
113
+ {id:'revert-helper',n:'Revert Helper',c:'utility',d:'Show undo command on session end',i:'npx cc-safe-setup --install-example revert-helper',t:['revert','undo','git']},
114
+ {id:'auto-stash-before-pull',n:'Auto Stash Before Pull',c:'safety',d:'Warn before pull/merge with dirty tree',i:'npx cc-safe-setup --install-example auto-stash-before-pull',t:['git','stash','pull']},
115
+ {id:'strict-allowlist',n:'Strict Allowlist',c:'safety',d:'Only allow explicitly permitted commands (allowlist mode)',i:'npx cc-safe-setup --install-example strict-allowlist',t:['allowlist','whitelist','strict']},
87
116
  {id:'safety-net',n:'Safety Net (TS)',c:'safety',d:'TypeScript hooks with configurable severity',i:'npx @anthropic-ai/claude-code-safety-net',t:['typescript','configurable'],s:1185},
88
117
  {id:'hooks-mastery',n:'Hooks Mastery (Python)',c:'utility',d:'Python hooks for all events + LLM',i:'git clone',t:['python','framework'],s:3386},
89
118
  ];
@@ -0,0 +1,640 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Hook Playground — Claude Code Hook Registry</title>
7
+ <meta name="description" content="Test which Claude Code hooks would fire on any command. Interactive safety checker.">
8
+ <style>
9
+ *{box-sizing:border-box;margin:0;padding:0}
10
+ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0d1117;color:#c9d1d9;min-height:100vh;padding:1.5rem}
11
+ .c{max-width:860px;margin:0 auto}
12
+ h1{font-size:1.4rem;color:#f0f6fc;margin-bottom:.2rem}
13
+ .sub{color:#8b949e;font-size:.85rem;margin-bottom:1.2rem}
14
+ a{color:#58a6ff;text-decoration:none}
15
+
16
+ .input-wrap{position:relative;margin-bottom:1rem}
17
+ #cmd{width:100%;padding:.7rem .7rem .7rem 2rem;background:#161b22;border:1px solid #30363d;border-radius:8px;color:#f0f6fc;font-family:'SF Mono',Monaco,monospace;font-size:.95rem}
18
+ #cmd:focus{outline:none;border-color:#58a6ff;box-shadow:0 0 0 3px #58a6ff22}
19
+ #cmd::placeholder{color:#484f58}
20
+ .prompt{position:absolute;left:.7rem;top:.75rem;color:#484f58;font-family:monospace;font-size:.95rem;pointer-events:none}
21
+
22
+ .score-bar{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:.8rem 1rem;margin-bottom:1rem;display:flex;align-items:center;gap:1rem}
23
+ .score-num{font-size:1.8rem;font-weight:700;min-width:3.5rem;text-align:center}
24
+ .score-label{font-size:.75rem;color:#8b949e}
25
+ .score-meter{flex:1;height:8px;background:#21262d;border-radius:4px;overflow:hidden}
26
+ .score-fill{height:100%;border-radius:4px;transition:width .3s,background .3s}
27
+ .verdict{font-size:.85rem;font-weight:600;min-width:90px;text-align:right}
28
+
29
+ .results{display:grid;gap:.5rem}
30
+ .result{background:#161b22;border:1px solid #30363d;border-radius:6px;padding:.65rem .85rem;display:flex;align-items:center;gap:.7rem;transition:border-color .2s}
31
+ .result.block{border-left:3px solid #f85149}
32
+ .result.warn{border-left:3px solid #d29922}
33
+ .result.pass{border-left:3px solid #3fb950}
34
+ .result.inactive{border-left:3px solid #30363d;opacity:.5}
35
+ .r-icon{font-size:1.1rem;min-width:1.5rem;text-align:center}
36
+ .r-name{font-weight:600;font-size:.85rem;color:#f0f6fc;min-width:140px}
37
+ .r-action{font-size:.7rem;font-weight:bold;padding:.15rem .4rem;border-radius:3px;min-width:55px;text-align:center}
38
+ .r-action.block{background:#da363322;color:#f85149}
39
+ .r-action.warn{background:#d2992222;color:#d29922}
40
+ .r-action.pass{background:#23863622;color:#3fb950}
41
+ .r-action.inactive{background:#21262d;color:#484f58}
42
+ .r-reason{font-size:.78rem;color:#8b949e;flex:1}
43
+ .r-install{font-family:monospace;font-size:.7rem;color:#58a6ff;cursor:pointer}
44
+ .r-install:hover{text-decoration:underline}
45
+
46
+ .examples{margin-top:1.5rem}
47
+ .examples h2{font-size:.9rem;color:#8b949e;margin-bottom:.5rem}
48
+ .ex-grid{display:flex;flex-wrap:wrap;gap:.3rem}
49
+ .ex{padding:.25rem .5rem;background:#161b22;border:1px solid #30363d;border-radius:4px;font-family:monospace;font-size:.75rem;color:#c9d1d9;cursor:pointer;transition:border-color .15s}
50
+ .ex:hover{border-color:#58a6ff;color:#58a6ff}
51
+
52
+ .nav{display:flex;gap:.5rem;margin-bottom:1rem;font-size:.8rem}
53
+ .footer{text-align:center;color:#484f58;font-size:.7rem;margin-top:2rem}
54
+
55
+ .empty{text-align:center;padding:2rem;color:#484f58;font-size:.85rem}
56
+ .tip{background:#161b22;border:1px solid #30363d;border-radius:6px;padding:.6rem .8rem;margin-bottom:1rem;font-size:.78rem;color:#8b949e}
57
+ .tip code{background:#21262d;padding:.1rem .3rem;border-radius:3px;color:#c9d1d9}
58
+ </style>
59
+ </head>
60
+ <body>
61
+ <div class="c">
62
+
63
+ <div class="nav">
64
+ <a href="index.html">Registry</a> · <b style="color:#f0f6fc">Playground</b>
65
+ </div>
66
+
67
+ <h1>Hook Playground</h1>
68
+ <p class="sub">Type any command — see which hooks would fire in real time</p>
69
+
70
+ <div class="input-wrap">
71
+ <span class="prompt">$</span>
72
+ <input id="cmd" placeholder="rm -rf /" autofocus oninput="analyze()">
73
+ </div>
74
+
75
+ <div class="score-bar" id="scorebar" style="display:none">
76
+ <div>
77
+ <div class="score-num" id="score">100</div>
78
+ <div class="score-label">Safety Score</div>
79
+ </div>
80
+ <div class="score-meter"><div class="score-fill" id="scorefill"></div></div>
81
+ <div class="verdict" id="verdict"></div>
82
+ </div>
83
+
84
+ <div class="results" id="results"></div>
85
+
86
+ <div class="examples">
87
+ <h2>Try these commands</h2>
88
+ <div class="ex-grid" id="examples"></div>
89
+ </div>
90
+
91
+ <div class="examples" style="margin-top:1.2rem">
92
+ <h2>Recommended by Stack</h2>
93
+ <div id="stacks" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:.5rem;margin-top:.5rem"></div>
94
+ </div>
95
+
96
+ <div class="tip" style="margin-top:1rem">
97
+ <strong style="color:#f0f6fc">How it works:</strong> This playground simulates the detection patterns from
98
+ <a href="https://www.npmjs.com/package/cc-safe-setup">cc-safe-setup</a>'s 71 hooks. Results show what would happen
99
+ if all hooks were installed. Install only the ones you need: <code>npx cc-safe-setup</code>
100
+ </div>
101
+
102
+ <div class="footer">
103
+ <a href="https://github.com/yurukusa/cc-hook-registry">GitHub</a> ·
104
+ <a href="https://www.npmjs.com/package/cc-hook-registry">npm</a> ·
105
+ <a href="https://yurukusa.github.io/cc-safe-setup/">Safety Audit</a>
106
+ </div>
107
+ </div>
108
+
109
+ <script>
110
+ const HOOKS = [
111
+ // === Safety Guards ===
112
+ {
113
+ id: 'destructive-guard', name: 'Destructive Guard', cat: 'safety',
114
+ install: 'npx cc-safe-setup',
115
+ test(cmd) {
116
+ if (/\brm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)?|--recursive\s+)/i.test(cmd)) {
117
+ if (/\s+\/\s*$|\s+\/[a-z]|\s+~\/?\s*$|\s+\.\s*$|\s+\$HOME/i.test(cmd))
118
+ return { action: 'block', reason: 'Destructive rm targeting critical path' };
119
+ if (/\s+\.\.\//i.test(cmd))
120
+ return { action: 'block', reason: 'rm -rf with parent directory escape' };
121
+ return { action: 'warn', reason: 'Recursive delete detected — check target carefully' };
122
+ }
123
+ if (/\bgit\s+reset\s+--hard/i.test(cmd))
124
+ return { action: 'block', reason: 'git reset --hard destroys uncommitted changes' };
125
+ if (/\bgit\s+clean\s+(-[a-zA-Z]*f|-[a-zA-Z]*d)/i.test(cmd))
126
+ return { action: 'block', reason: 'git clean removes untracked files permanently' };
127
+ if (/Remove-Item.*-Recurse.*-Force|rd\s+\/s\s+\/q|del\s+\/s\s+\/q/i.test(cmd))
128
+ return { action: 'block', reason: 'PowerShell/cmd destructive command' };
129
+ if (/\bsudo\s+mkfs\b/i.test(cmd))
130
+ return { action: 'block', reason: 'Filesystem format command' };
131
+ if (/--no-preserve-root/i.test(cmd))
132
+ return { action: 'block', reason: '--no-preserve-root bypasses safety' };
133
+ return null;
134
+ }
135
+ },
136
+ {
137
+ id: 'branch-guard', name: 'Branch Guard', cat: 'safety',
138
+ install: 'npx cc-safe-setup',
139
+ test(cmd) {
140
+ if (/\bgit\s+push\b.*\b(main|master)\b/i.test(cmd))
141
+ return { action: 'block', reason: 'Direct push to main/master branch' };
142
+ if (/\bgit\s+push\s+.*--force\b/i.test(cmd))
143
+ return { action: 'block', reason: 'Force push can overwrite remote history' };
144
+ if (/\bgit\s+push\s+.*-f\b/i.test(cmd))
145
+ return { action: 'block', reason: 'Force push (-f shorthand)' };
146
+ return null;
147
+ }
148
+ },
149
+ {
150
+ id: 'secret-guard', name: 'Secret Guard', cat: 'safety',
151
+ install: 'npx cc-safe-setup',
152
+ test(cmd) {
153
+ if (/\bgit\s+add\s+.*\.env\b/i.test(cmd))
154
+ return { action: 'block', reason: 'Adding .env file to git exposes secrets' };
155
+ if (/\bgit\s+add\s+.*\.(pem|key|p12|pfx|jks)\b/i.test(cmd))
156
+ return { action: 'block', reason: 'Adding credential/key file to git' };
157
+ if (/\bgit\s+add\s+[\.\-]A?\s*$/i.test(cmd) || /\bgit\s+add\s+(-A|--all|\.)(\s|$)/i.test(cmd))
158
+ return { action: 'warn', reason: 'git add all — may include secret files' };
159
+ if (/\bgit\s+add\s+.*credentials/i.test(cmd))
160
+ return { action: 'block', reason: 'Adding credentials file to git' };
161
+ if (/\bgit\s+add\s+.*id_rsa/i.test(cmd))
162
+ return { action: 'block', reason: 'Adding SSH private key to git' };
163
+ return null;
164
+ }
165
+ },
166
+ {
167
+ id: 'block-database-wipe', name: 'Database Wipe Guard', cat: 'safety',
168
+ install: 'npx cc-safe-setup --install-example block-database-wipe',
169
+ test(cmd) {
170
+ if (/\bDROP\s+(DATABASE|TABLE|SCHEMA)\b/i.test(cmd))
171
+ return { action: 'block', reason: 'SQL DROP destroys data permanently' };
172
+ if (/\bmigrate:fresh\b|\bmigrate:reset\b/i.test(cmd))
173
+ return { action: 'block', reason: 'Laravel migrate:fresh/reset wipes all tables' };
174
+ if (/\bprisma\s+(migrate\s+)?reset\b/i.test(cmd))
175
+ return { action: 'block', reason: 'Prisma reset drops and recreates database' };
176
+ if (/\brails\s+db:drop\b|\brake\s+db:drop\b/i.test(cmd))
177
+ return { action: 'block', reason: 'Rails db:drop destroys database' };
178
+ if (/\bdjango.*flush\b|\bmanage\.py\s+flush\b/i.test(cmd))
179
+ return { action: 'block', reason: 'Django flush deletes all data' };
180
+ if (/\bTRUNCATE\b/i.test(cmd))
181
+ return { action: 'block', reason: 'TRUNCATE removes all rows' };
182
+ return null;
183
+ }
184
+ },
185
+ {
186
+ id: 'deploy-guard', name: 'Deploy Guard', cat: 'safety',
187
+ install: 'npx cc-safe-setup --install-example deploy-guard',
188
+ test(cmd) {
189
+ const deployTools = /\b(vercel|netlify|firebase|fly|heroku|rsync|scp)\s+(deploy|push)\b/i;
190
+ if (deployTools.test(cmd))
191
+ return { action: 'warn', reason: 'Deploy command — ensure changes are committed first' };
192
+ if (/\bgit\s+push\s+.*heroku\b/i.test(cmd))
193
+ return { action: 'warn', reason: 'Heroku deploy via git push' };
194
+ return null;
195
+ }
196
+ },
197
+ {
198
+ id: 'scope-guard', name: 'Scope Guard', cat: 'safety',
199
+ install: 'npx cc-safe-setup --install-example scope-guard',
200
+ test(cmd) {
201
+ if (/\b(cat|rm|mv|cp|chmod|chown)\s+\/(?!tmp|dev\/null)/i.test(cmd))
202
+ return { action: 'warn', reason: 'Absolute path outside project — potential scope escape' };
203
+ if (/~\//i.test(cmd) && /\b(rm|mv|chmod)\b/i.test(cmd))
204
+ return { action: 'warn', reason: 'Operating on home directory files' };
205
+ if (/\.\.\//g.test(cmd) && (cmd.match(/\.\.\//g)||[]).length >= 2)
206
+ return { action: 'block', reason: 'Multiple parent directory traversals' };
207
+ return null;
208
+ }
209
+ },
210
+ {
211
+ id: 'protect-dotfiles', name: 'Dotfile Protector', cat: 'safety',
212
+ install: 'npx cc-safe-setup --install-example protect-dotfiles',
213
+ test(cmd) {
214
+ if (/~\/\.(bashrc|zshrc|profile|bash_profile)\b/i.test(cmd))
215
+ return { action: 'block', reason: 'Modifying shell config file' };
216
+ if (/~\/\.aws\//i.test(cmd))
217
+ return { action: 'block', reason: 'Accessing AWS credentials directory' };
218
+ if (/~\/\.ssh\//i.test(cmd))
219
+ return { action: 'block', reason: 'Accessing SSH key directory' };
220
+ if (/~\/\.gnupg\//i.test(cmd))
221
+ return { action: 'block', reason: 'Accessing GPG key directory' };
222
+ return null;
223
+ }
224
+ },
225
+ {
226
+ id: 'no-sudo-guard', name: 'No Sudo', cat: 'safety',
227
+ install: 'npx cc-safe-setup --install-example no-sudo-guard',
228
+ test(cmd) {
229
+ if (/^\s*sudo\b/i.test(cmd) || /&&\s*sudo\b/i.test(cmd) || /;\s*sudo\b/i.test(cmd))
230
+ return { action: 'block', reason: 'sudo escalates privileges — use with caution' };
231
+ return null;
232
+ }
233
+ },
234
+ {
235
+ id: 'no-install-global', name: 'No Global Install', cat: 'safety',
236
+ install: 'npx cc-safe-setup --install-example no-install-global',
237
+ test(cmd) {
238
+ if (/\bnpm\s+install\s+(-g|--global)\b/i.test(cmd))
239
+ return { action: 'block', reason: 'Global npm install modifies system' };
240
+ if (/\bpip\s+install\b/i.test(cmd) && !/\bvenv\b|\.venv\b|-m\s+pip/i.test(cmd))
241
+ return { action: 'warn', reason: 'pip install without venv — may be system-wide' };
242
+ return null;
243
+ }
244
+ },
245
+ {
246
+ id: 'symlink-guard', name: 'Symlink Guard', cat: 'safety',
247
+ install: 'npx cc-safe-setup --install-example symlink-guard',
248
+ test(cmd) {
249
+ if (/\brm\b.*-[a-zA-Z]*r/i.test(cmd) && /\bln\b|\bsymlink\b|\bjunction\b/i.test(cmd))
250
+ return { action: 'warn', reason: 'rm -r on symlinked target may traverse' };
251
+ return null;
252
+ }
253
+ },
254
+ {
255
+ id: 'env-source-guard', name: 'Env Source Guard', cat: 'safety',
256
+ install: 'npx cc-safe-setup --install-example env-source-guard',
257
+ test(cmd) {
258
+ if (/\b(source|\.)\s+.*\.env\b/i.test(cmd))
259
+ return { action: 'block', reason: 'Sourcing .env executes it as shell script' };
260
+ return null;
261
+ }
262
+ },
263
+ {
264
+ id: 'protect-claudemd', name: 'Protect CLAUDE.md', cat: 'safety',
265
+ install: 'npx cc-safe-setup --install-example protect-claudemd',
266
+ test(cmd) {
267
+ if (/CLAUDE\.md/i.test(cmd) && /\b(rm|mv|sed|echo\s+>)\b/i.test(cmd))
268
+ return { action: 'block', reason: 'Modifying CLAUDE.md changes AI behavior rules' };
269
+ if (/settings\.json/i.test(cmd) && /\.claude/i.test(cmd))
270
+ return { action: 'warn', reason: 'Modifying Claude Code settings' };
271
+ return null;
272
+ }
273
+ },
274
+ {
275
+ id: 'git-tag-guard', name: 'Git Tag Guard', cat: 'safety',
276
+ install: 'npx cc-safe-setup --install-example git-tag-guard',
277
+ test(cmd) {
278
+ if (/\bgit\s+tag\s+-d\b/i.test(cmd))
279
+ return { action: 'block', reason: 'Deleting git tags' };
280
+ if (/\bgit\s+push\s+.*--delete\s+.*tag/i.test(cmd))
281
+ return { action: 'block', reason: 'Deleting remote tags' };
282
+ return null;
283
+ }
284
+ },
285
+ {
286
+ id: 'npm-publish-guard', name: 'npm Publish Guard', cat: 'safety',
287
+ install: 'npx cc-safe-setup --install-example npm-publish-guard',
288
+ test(cmd) {
289
+ if (/\bnpm\s+publish\b/i.test(cmd))
290
+ return { action: 'warn', reason: 'npm publish is irreversible — verify version and contents' };
291
+ return null;
292
+ }
293
+ },
294
+ {
295
+ id: 'no-deploy-friday', name: 'No Deploy Friday', cat: 'safety',
296
+ install: 'npx cc-safe-setup --install-example no-deploy-friday',
297
+ test(cmd) {
298
+ const isFriday = new Date().getDay() === 5;
299
+ if (isFriday && /\b(deploy|publish|release)\b/i.test(cmd))
300
+ return { action: 'block', reason: 'No deploys on Friday — wait until Monday' };
301
+ return null;
302
+ }
303
+ },
304
+ {
305
+ id: 'dependency-audit', name: 'Dependency Audit', cat: 'safety',
306
+ install: 'npx cc-safe-setup --install-example dependency-audit',
307
+ test(cmd) {
308
+ if (/\bnpm\s+install\s+\w/i.test(cmd) && !/\b(--save-dev|-D)\b/i.test(cmd))
309
+ return { action: 'warn', reason: 'New production dependency — verify package trustworthiness' };
310
+ if (/\bpip\s+install\s+\w/i.test(cmd))
311
+ return { action: 'warn', reason: 'New Python dependency — check package reputation' };
312
+ return null;
313
+ }
314
+ },
315
+ // === Quality Hooks ===
316
+ {
317
+ id: 'diff-size-guard', name: 'Diff Size Guard', cat: 'quality',
318
+ install: 'npx cc-safe-setup --install-example diff-size-guard',
319
+ test(cmd) {
320
+ if (/\bgit\s+commit\b/i.test(cmd))
321
+ return { action: 'warn', reason: 'Check diff size before committing (10+ files = review)' };
322
+ return null;
323
+ }
324
+ },
325
+ {
326
+ id: 'commit-quality-gate', name: 'Commit Quality', cat: 'quality',
327
+ install: 'npx cc-safe-setup --install-example commit-quality-gate',
328
+ test(cmd) {
329
+ if (/\bgit\s+commit\s+-m\s+["']?(fix|update|change|wip|tmp)["']?\s*$/i.test(cmd))
330
+ return { action: 'warn', reason: 'Vague commit message — be more descriptive' };
331
+ return null;
332
+ }
333
+ },
334
+ {
335
+ id: 'syntax-check', name: 'Syntax Check', cat: 'quality',
336
+ install: 'npx cc-safe-setup',
337
+ test(cmd) { return null; /* PostToolUse — checks after edits */ }
338
+ },
339
+ // === Auto-Approve ===
340
+ {
341
+ id: 'auto-approve-build', name: 'Auto-Approve Build', cat: 'approve',
342
+ install: 'npx cc-safe-setup --install-example auto-approve-build',
343
+ test(cmd) {
344
+ if (/\bnpm\s+(test|run\s+(build|lint|format|check))\b/i.test(cmd))
345
+ return { action: 'pass', reason: 'Safe build/test command — auto-approved' };
346
+ if (/\bcargo\s+(build|test|check|clippy)\b/i.test(cmd))
347
+ return { action: 'pass', reason: 'Safe Cargo command — auto-approved' };
348
+ if (/\bgo\s+(build|test|vet|fmt)\b/i.test(cmd))
349
+ return { action: 'pass', reason: 'Safe Go command — auto-approved' };
350
+ return null;
351
+ }
352
+ },
353
+ {
354
+ id: 'auto-approve-python', name: 'Auto-Approve Python', cat: 'approve',
355
+ install: 'npx cc-safe-setup --install-example auto-approve-python',
356
+ test(cmd) {
357
+ if (/\b(pytest|mypy|ruff|black|isort|flake8|pylint)\b/i.test(cmd))
358
+ return { action: 'pass', reason: 'Safe Python tool — auto-approved' };
359
+ return null;
360
+ }
361
+ },
362
+ {
363
+ id: 'auto-approve-docker', name: 'Auto-Approve Docker', cat: 'approve',
364
+ install: 'npx cc-safe-setup --install-example auto-approve-docker',
365
+ test(cmd) {
366
+ if (/\bdocker\s+(build|compose|ps|logs|images)\b/i.test(cmd))
367
+ return { action: 'pass', reason: 'Safe Docker command — auto-approved' };
368
+ if (/\bdocker\s+(rm|rmi|system\s+prune)\b/i.test(cmd))
369
+ return { action: 'warn', reason: 'Docker cleanup command — verify targets' };
370
+ return null;
371
+ }
372
+ },
373
+ {
374
+ id: 'auto-approve-go', name: 'Auto-Approve Go', cat: 'approve',
375
+ install: 'npx cc-safe-setup --install-example auto-approve-go',
376
+ test(cmd) {
377
+ if (/\bgo\s+(build|test|vet|fmt|mod\s+tidy)\b/i.test(cmd))
378
+ return { action: 'pass', reason: 'Safe Go command — auto-approved' };
379
+ return null;
380
+ }
381
+ },
382
+ {
383
+ id: 'auto-approve-cargo', name: 'Auto-Approve Cargo', cat: 'approve',
384
+ install: 'npx cc-safe-setup --install-example auto-approve-cargo',
385
+ test(cmd) {
386
+ if (/\bcargo\s+(build|test|check|clippy|fmt)\b/i.test(cmd))
387
+ return { action: 'pass', reason: 'Safe Cargo command — auto-approved' };
388
+ return null;
389
+ }
390
+ },
391
+ {
392
+ id: 'auto-approve-make', name: 'Auto-Approve Make', cat: 'approve',
393
+ install: 'npx cc-safe-setup --install-example auto-approve-make',
394
+ test(cmd) {
395
+ if (/\bmake\s*(build|test|lint|check|all)?\s*$/i.test(cmd))
396
+ return { action: 'pass', reason: 'Safe make target — auto-approved' };
397
+ return null;
398
+ }
399
+ },
400
+ {
401
+ id: 'auto-approve-gradle', name: 'Auto-Approve Gradle', cat: 'approve',
402
+ install: 'npx cc-safe-setup --install-example auto-approve-gradle',
403
+ test(cmd) {
404
+ if (/\b(gradle|gradlew|\.\/gradlew)\s+(build|test|check)\b/i.test(cmd))
405
+ return { action: 'pass', reason: 'Safe Gradle command — auto-approved' };
406
+ return null;
407
+ }
408
+ },
409
+ {
410
+ id: 'auto-approve-maven', name: 'Auto-Approve Maven', cat: 'approve',
411
+ install: 'npx cc-safe-setup --install-example auto-approve-maven',
412
+ test(cmd) {
413
+ if (/\bmvn\s+(compile|test|verify|package)\b/i.test(cmd))
414
+ return { action: 'pass', reason: 'Safe Maven command — auto-approved' };
415
+ return null;
416
+ }
417
+ },
418
+ // === Utility ===
419
+ {
420
+ id: 'context-monitor', name: 'Context Monitor', cat: 'utility',
421
+ install: 'npx cc-safe-setup',
422
+ test(cmd) { return null; /* Monitors context %, not commands */ }
423
+ },
424
+ {
425
+ id: 'loop-detector', name: 'Loop Detector', cat: 'utility',
426
+ install: 'npx cc-safe-setup --install-example loop-detector',
427
+ test(cmd) { return null; /* Detects repetition patterns, not single commands */ }
428
+ },
429
+ {
430
+ id: 'cost-tracker', name: 'Cost Tracker', cat: 'utility',
431
+ install: 'npx cc-safe-setup --install-example cost-tracker',
432
+ test(cmd) { return null; /* Tracks cumulative cost, not single commands */ }
433
+ },
434
+ {
435
+ id: 'uncommitted-work-guard', name: 'Uncommitted Work Guard', cat: 'safety',
436
+ install: 'npx cc-safe-setup --install-example uncommitted-work-guard',
437
+ test(cmd) {
438
+ if (/\bgit\s+(checkout\s+--|restore\s+\.|reset\s+--hard|clean\s+-[a-zA-Z]*f|stash\s+drop)/i.test(cmd))
439
+ return { action: 'block', reason: 'Blocks destructive git when uncommitted changes exist' };
440
+ return null;
441
+ }
442
+ },
443
+ {
444
+ id: 'test-deletion-guard', name: 'Test Deletion Guard', cat: 'quality',
445
+ install: 'npx cc-safe-setup --install-example test-deletion-guard',
446
+ test(cmd) { return null; /* Checks Edit old_string vs new_string, not commands */ }
447
+ },
448
+ {
449
+ id: 'overwrite-guard', name: 'Overwrite Guard', cat: 'safety',
450
+ install: 'npx cc-safe-setup --install-example overwrite-guard',
451
+ test(cmd) { return null; /* Checks Write target file existence, not commands */ }
452
+ },
453
+ {
454
+ id: 'verify-before-done', name: 'Verify Before Done', cat: 'quality',
455
+ install: 'npx cc-safe-setup --install-example verify-before-done',
456
+ test(cmd) {
457
+ if (/\bgit\s+commit\b/i.test(cmd))
458
+ return { action: 'warn', reason: 'Check if tests passed before committing' };
459
+ return null;
460
+ }
461
+ },
462
+ {
463
+ id: 'disk-space-guard', name: 'Disk Space Guard', cat: 'utility',
464
+ install: 'npx cc-safe-setup --install-example disk-space-guard',
465
+ test(cmd) { return null; /* Checks df output, not command patterns */ }
466
+ },
467
+ {
468
+ id: 'error-memory-guard', name: 'Error Memory Guard', cat: 'safety',
469
+ install: 'npx cc-safe-setup --install-example error-memory-guard',
470
+ test(cmd) { return null; /* Tracks failure history, not single commands */ }
471
+ },
472
+ {
473
+ id: 'large-read-guard', name: 'Large Read Guard', cat: 'utility',
474
+ install: 'npx cc-safe-setup --install-example large-read-guard',
475
+ test(cmd) {
476
+ if (/^\s*cat\s+\S+\.(log|sql|csv|json|xml|min\.js|bundle)/i.test(cmd))
477
+ return { action: 'warn', reason: 'Large file — use head/tail/grep instead' };
478
+ return null;
479
+ }
480
+ },
481
+ {
482
+ id: 'strict-allowlist', name: 'Strict Allowlist', cat: 'safety',
483
+ install: 'npx cc-safe-setup --install-example strict-allowlist',
484
+ test(cmd) { return null; /* Checks against allowlist file */ }
485
+ },
486
+ {
487
+ id: 'conflict-marker-guard', name: 'Conflict Marker Guard', cat: 'quality',
488
+ install: 'npx cc-safe-setup --install-example conflict-marker-guard',
489
+ test(cmd) {
490
+ if (/\bgit\s+commit\b/i.test(cmd))
491
+ return { action: 'warn', reason: 'Checks staged files for <<<<<<< markers' };
492
+ return null;
493
+ }
494
+ },
495
+ ];
496
+
497
+ const EXAMPLES = [
498
+ 'rm -rf /',
499
+ 'rm -rf ~/',
500
+ 'rm -rf .',
501
+ 'rm -rf node_modules',
502
+ 'git push origin main',
503
+ 'git push --force',
504
+ 'git reset --hard HEAD~5',
505
+ 'git clean -fd',
506
+ 'git add .env',
507
+ 'git add .',
508
+ 'git add .env.production',
509
+ 'git commit -m "fix"',
510
+ 'sudo rm -rf /var/log',
511
+ 'npm install -g typescript',
512
+ 'pip install requests',
513
+ 'DROP DATABASE production',
514
+ 'npx prisma reset',
515
+ 'rails db:drop',
516
+ 'source .env',
517
+ 'vercel deploy',
518
+ 'npm publish',
519
+ 'docker system prune',
520
+ 'npm test',
521
+ 'cargo build',
522
+ 'pytest -v',
523
+ 'go test ./...',
524
+ 'make build',
525
+ 'Remove-Item -Recurse -Force *',
526
+ 'cat ~/.aws/credentials',
527
+ 'sed -i "" ~/.bashrc',
528
+ 'git tag -d v1.0.0',
529
+ ];
530
+
531
+ // Render examples
532
+ document.getElementById('examples').innerHTML = EXAMPLES.map(e =>
533
+ `<button class="ex" onclick="tryCmd('${e.replace(/'/g,"\\'")}')">${e}</button>`
534
+ ).join('');
535
+
536
+ function tryCmd(cmd) {
537
+ document.getElementById('cmd').value = cmd;
538
+ analyze();
539
+ }
540
+
541
+ function analyze() {
542
+ const cmd = document.getElementById('cmd').value.trim();
543
+ const resultsEl = document.getElementById('results');
544
+ const scoreBar = document.getElementById('scorebar');
545
+
546
+ if (!cmd) {
547
+ resultsEl.innerHTML = '<div class="empty">Type a command above to see which hooks would activate</div>';
548
+ scoreBar.style.display = 'none';
549
+ return;
550
+ }
551
+
552
+ scoreBar.style.display = 'flex';
553
+
554
+ const results = HOOKS.map(hook => {
555
+ const result = hook.test(cmd);
556
+ return {
557
+ ...hook,
558
+ result: result || { action: 'inactive', reason: 'No match — this hook would not fire' }
559
+ };
560
+ });
561
+
562
+ // Sort: blocks first, then warns, then pass, then inactive
563
+ const order = { block: 0, warn: 1, pass: 2, inactive: 3 };
564
+ results.sort((a, b) => order[a.result.action] - order[b.result.action]);
565
+
566
+ // Calculate score
567
+ const blocks = results.filter(r => r.result.action === 'block').length;
568
+ const warns = results.filter(r => r.result.action === 'warn').length;
569
+ const passes = results.filter(r => r.result.action === 'pass').length;
570
+ const score = Math.max(0, 100 - blocks * 30 - warns * 10 + passes * 5);
571
+
572
+ const scoreEl = document.getElementById('score');
573
+ const fillEl = document.getElementById('scorefill');
574
+ const verdictEl = document.getElementById('verdict');
575
+
576
+ scoreEl.textContent = score;
577
+ fillEl.style.width = score + '%';
578
+
579
+ if (score >= 80) {
580
+ scoreEl.style.color = '#3fb950';
581
+ fillEl.style.background = '#238636';
582
+ verdictEl.textContent = passes > 0 ? 'Auto-approved' : 'Safe';
583
+ verdictEl.style.color = '#3fb950';
584
+ } else if (score >= 40) {
585
+ scoreEl.style.color = '#d29922';
586
+ fillEl.style.background = '#9e6a03';
587
+ verdictEl.textContent = 'Caution';
588
+ verdictEl.style.color = '#d29922';
589
+ } else {
590
+ scoreEl.style.color = '#f85149';
591
+ fillEl.style.background = '#da3633';
592
+ verdictEl.textContent = 'Blocked';
593
+ verdictEl.style.color = '#f85149';
594
+ }
595
+
596
+ const icons = { block: '🛑', warn: '⚠️', pass: '✅', inactive: '·' };
597
+
598
+ // Only show active hooks + first 5 inactive
599
+ const active = results.filter(r => r.result.action !== 'inactive');
600
+ const inactive = results.filter(r => r.result.action === 'inactive').slice(0, 5);
601
+ const hidden = results.filter(r => r.result.action === 'inactive').length - inactive.length;
602
+
603
+ const shown = [...active, ...inactive];
604
+
605
+ resultsEl.innerHTML = shown.map(r => `
606
+ <div class="result ${r.result.action}">
607
+ <span class="r-icon">${icons[r.result.action]}</span>
608
+ <span class="r-name">${r.name}</span>
609
+ <span class="r-action ${r.result.action}">${r.result.action.toUpperCase()}</span>
610
+ <span class="r-reason">${r.result.reason}</span>
611
+ ${r.result.action !== 'inactive' ? `<span class="r-install" onclick="navigator.clipboard.writeText('${r.install}');this.textContent='Copied!';setTimeout(()=>this.textContent='Install',1500)">Install</span>` : ''}
612
+ </div>
613
+ `).join('') + (hidden > 0 ? `<div style="text-align:center;color:#484f58;font-size:.75rem;padding:.3rem">+${hidden} more hooks (no match)</div>` : '');
614
+ }
615
+
616
+ // Stack recommendations
617
+ const STACKS = [
618
+ { name: 'Node.js', icon: '📦', hooks: ['destructive-guard','branch-guard','secret-guard','auto-approve-build','npm-publish-guard','dependency-audit'], cmd: 'npx cc-safe-setup && npx cc-safe-setup --install-example auto-approve-build' },
619
+ { name: 'Python', icon: '🐍', hooks: ['destructive-guard','branch-guard','secret-guard','auto-approve-python','env-source-guard'], cmd: 'npx cc-safe-setup && npx cc-safe-setup --install-example auto-approve-python' },
620
+ { name: 'Go', icon: '🔵', hooks: ['destructive-guard','branch-guard','auto-approve-go','diff-size-guard'], cmd: 'npx cc-safe-setup && npx cc-safe-setup --install-example auto-approve-go' },
621
+ { name: 'Rust', icon: '🦀', hooks: ['destructive-guard','branch-guard','auto-approve-cargo','diff-size-guard'], cmd: 'npx cc-safe-setup && npx cc-safe-setup --install-example auto-approve-cargo' },
622
+ { name: 'Java', icon: '☕', hooks: ['destructive-guard','branch-guard','auto-approve-gradle','auto-approve-maven'], cmd: 'npx cc-safe-setup && npx cc-safe-setup --install-example auto-approve-gradle' },
623
+ { name: 'Docker', icon: '🐳', hooks: ['destructive-guard','branch-guard','auto-approve-docker','deploy-guard'], cmd: 'npx cc-safe-setup && npx cc-safe-setup --install-example auto-approve-docker' },
624
+ { name: 'Database', icon: '🗄️', hooks: ['destructive-guard','block-database-wipe','secret-guard','diff-size-guard'], cmd: 'npx cc-safe-setup && npx cc-safe-setup --install-example block-database-wipe' },
625
+ { name: 'DevOps', icon: '🚀', hooks: ['destructive-guard','branch-guard','deploy-guard','no-sudo-guard','no-deploy-friday'], cmd: 'npx cc-safe-setup && npx cc-safe-setup --install-example deploy-guard' },
626
+ ];
627
+
628
+ document.getElementById('stacks').innerHTML = STACKS.map(s => `
629
+ <div style="background:#161b22;border:1px solid #30363d;border-radius:6px;padding:.6rem;cursor:pointer" onclick="navigator.clipboard.writeText('${s.cmd}');this.querySelector('.st-copy').textContent='Copied!';setTimeout(()=>this.querySelector('.st-copy').textContent='Copy install',1500)">
630
+ <div style="font-size:1rem;margin-bottom:.3rem">${s.icon} <strong style="color:#f0f6fc;font-size:.85rem">${s.name}</strong></div>
631
+ <div style="color:#8b949e;font-size:.7rem">${s.hooks.length} hooks recommended</div>
632
+ <div class="st-copy" style="color:#58a6ff;font-size:.7rem;margin-top:.3rem">Copy install</div>
633
+ </div>
634
+ `).join('');
635
+
636
+ // Initial render
637
+ analyze();
638
+ </script>
639
+ </body>
640
+ </html>
package/index.mjs CHANGED
@@ -336,6 +336,26 @@ else if (command === 'recommend') {
336
336
  recommendations.push({ id: 'block-database-wipe', reason: 'Laravel detected — protect against migrate:fresh', priority: 1 });
337
337
  }
338
338
 
339
+ if (existsSync(join(cwd, 'go.mod'))) {
340
+ recommendations.push({ id: 'auto-approve-go', reason: 'Go project detected', priority: 2 });
341
+ }
342
+
343
+ if (existsSync(join(cwd, 'Cargo.toml'))) {
344
+ recommendations.push({ id: 'auto-approve-cargo', reason: 'Rust project detected', priority: 2 });
345
+ }
346
+
347
+ if (existsSync(join(cwd, 'pom.xml'))) {
348
+ recommendations.push({ id: 'auto-approve-maven', reason: 'Maven project detected', priority: 2 });
349
+ }
350
+
351
+ if (existsSync(join(cwd, 'build.gradle')) || existsSync(join(cwd, 'build.gradle.kts'))) {
352
+ recommendations.push({ id: 'auto-approve-gradle', reason: 'Gradle project detected', priority: 2 });
353
+ }
354
+
355
+ if (existsSync(join(cwd, 'Makefile'))) {
356
+ recommendations.push({ id: 'auto-approve-make', reason: 'Makefile detected', priority: 2 });
357
+ }
358
+
339
359
  // Always useful
340
360
  recommendations.push({ id: 'compound-command-approver', reason: 'Fixes permission matching for cd && commands', priority: 2 });
341
361
  recommendations.push({ id: 'loop-detector', reason: 'Prevents infinite command loops', priority: 3 });
@@ -425,9 +445,35 @@ else if (command === 'init') {
425
445
  console.log(' ' + c.blue + '⬡' + c.reset + ' Rails/Laravel detected');
426
446
  toInstall.push('block-database-wipe');
427
447
  }
448
+ if (existsSync(join(cwd, 'go.mod'))) {
449
+ console.log(' ' + c.blue + '⬡' + c.reset + ' Go detected');
450
+ toInstall.push('auto-approve-go');
451
+ }
452
+ if (existsSync(join(cwd, 'Cargo.toml'))) {
453
+ console.log(' ' + c.blue + '⬡' + c.reset + ' Rust detected');
454
+ toInstall.push('auto-approve-cargo');
455
+ }
456
+ if (existsSync(join(cwd, 'pom.xml'))) {
457
+ console.log(' ' + c.blue + '⬡' + c.reset + ' Maven detected');
458
+ toInstall.push('auto-approve-maven');
459
+ }
460
+ if (existsSync(join(cwd, 'build.gradle')) || existsSync(join(cwd, 'build.gradle.kts'))) {
461
+ console.log(' ' + c.blue + '⬡' + c.reset + ' Gradle detected');
462
+ toInstall.push('auto-approve-gradle');
463
+ }
464
+ if (existsSync(join(cwd, 'Makefile'))) {
465
+ console.log(' ' + c.blue + '⬡' + c.reset + ' Makefile detected');
466
+ toInstall.push('auto-approve-make');
467
+ }
468
+ if (existsSync(join(cwd, 'docker-compose.yml')) || existsSync(join(cwd, 'docker-compose.yaml')) || existsSync(join(cwd, 'compose.yml'))) {
469
+ if (!toInstall.includes('auto-approve-docker')) {
470
+ console.log(' ' + c.blue + '⬡' + c.reset + ' Docker Compose detected');
471
+ toInstall.push('auto-approve-docker');
472
+ }
473
+ }
428
474
 
429
475
  // Always useful
430
- toInstall.push('compound-command-approver', 'loop-detector', 'session-handoff');
476
+ toInstall.push('compound-command-approver', 'loop-detector', 'session-handoff', 'cost-tracker');
431
477
 
432
478
  // Deduplicate
433
479
  const unique = [...new Set(toInstall)];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-hook-registry",
3
- "version": "5.4.0",
3
+ "version": "5.5.0",
4
4
  "description": "Search, browse, and install Claude Code hooks from the community. GitHub-based registry, no server needed.",
5
5
  "main": "index.mjs",
6
6
  "bin": {