cc-safe-setup 4.0.3 → 5.1.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/README.md +1 -0
- package/docs/app.html +271 -0
- package/examples/env-source-guard.sh +51 -0
- package/index.mjs +211 -34
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -249,6 +249,7 @@ Or browse all available examples in [`examples/`](examples/):
|
|
|
249
249
|
- [Hooks Cookbook](https://github.com/yurukusa/claude-code-hooks/blob/main/COOKBOOK.md) — 25 recipes from real GitHub Issues ([interactive version](https://yurukusa.github.io/claude-code-hooks/))
|
|
250
250
|
- [Japanese guide (Qiita)](https://qiita.com/yurukusa/items/a9714b33f5d974e8f1e8) — この記事の日本語解説
|
|
251
251
|
- [Hook Test Runner](https://github.com/yurukusa/cc-hook-test) — `npx cc-hook-test <hook.sh>` to auto-test any hook
|
|
252
|
+
- [Hook Registry](https://github.com/yurukusa/cc-hook-registry) — `npx cc-hook-registry search database` to find community hooks
|
|
252
253
|
- [Hooks Cheat Sheet](https://yurukusa.github.io/cc-safe-setup/cheatsheet.html) — printable A4 quick reference
|
|
253
254
|
- [Ecosystem Comparison](https://yurukusa.github.io/cc-safe-setup/ecosystem.html) — all Claude Code hook projects compared
|
|
254
255
|
- [The incident that inspired this tool](https://github.com/anthropics/claude-code/issues/36339) — NTFS junction rm -rf
|
package/docs/app.html
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
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>cc-safe-setup — Claude Code Safety Suite</title>
|
|
7
|
+
<meta name="description" content="Audit, build, browse, and learn Claude Code hooks. All in one page, 100% client-side.">
|
|
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}
|
|
11
|
+
.nav{display:flex;background:#161b22;border-bottom:1px solid #30363d;overflow-x:auto}
|
|
12
|
+
.nav a{padding:.75rem 1.2rem;color:#8b949e;text-decoration:none;font-size:.85rem;white-space:nowrap;border-bottom:2px solid transparent;cursor:pointer}
|
|
13
|
+
.nav a.active{color:#f0f6fc;border-bottom-color:#f78166}
|
|
14
|
+
.nav a:hover{color:#c9d1d9}
|
|
15
|
+
.page{display:none;max-width:800px;margin:0 auto;padding:1.5rem}
|
|
16
|
+
.page.active{display:block}
|
|
17
|
+
h1{font-size:1.3rem;color:#f0f6fc;margin-bottom:.5rem}
|
|
18
|
+
h2{font-size:1.1rem;color:#f0f6fc;margin:1.5rem 0 .5rem}
|
|
19
|
+
h3{font-size:.95rem;color:#c9d1d9;margin:1rem 0 .3rem}
|
|
20
|
+
p,.sub{color:#8b949e;font-size:.85rem;margin-bottom:.75rem}
|
|
21
|
+
a{color:#58a6ff;text-decoration:none}
|
|
22
|
+
textarea,input,select{width:100%;padding:.5rem;background:#161b22;border:1px solid #30363d;border-radius:4px;color:#c9d1d9;font-family:monospace;font-size:.8rem;margin:.25rem 0}
|
|
23
|
+
textarea{height:150px;resize:vertical}
|
|
24
|
+
button,.btn{background:#238636;color:#fff;border:none;padding:.5rem 1rem;border-radius:4px;cursor:pointer;font-size:.85rem;margin:.25rem .25rem .25rem 0}
|
|
25
|
+
button:hover,.btn:hover{background:#2ea043}
|
|
26
|
+
.btn-sm{padding:.25rem .5rem;font-size:.75rem}
|
|
27
|
+
.btn-secondary{background:#30363d}
|
|
28
|
+
pre{background:#161b22;border:1px solid #30363d;border-radius:4px;padding:.6rem;font-size:.75rem;overflow-x:auto;position:relative;margin:.3rem 0}
|
|
29
|
+
.copy{position:absolute;top:.3rem;right:.3rem;background:#30363d;color:#c9d1d9;border:none;padding:.15rem .4rem;border-radius:3px;font-size:.65rem;cursor:pointer}
|
|
30
|
+
.score{font-size:1.8rem;font-weight:bold;margin:.5rem 0}
|
|
31
|
+
.score.good{color:#3fb950} .score.mid{color:#d29922} .score.bad{color:#f85149}
|
|
32
|
+
.risk{background:#161b22;border:1px solid #30363d;border-radius:4px;padding:.75rem;margin:.3rem 0;font-size:.8rem}
|
|
33
|
+
.good-item{color:#3fb950;margin:.2rem 0;font-size:.8rem}
|
|
34
|
+
table{width:100%;border-collapse:collapse;font-size:.8rem;margin:.5rem 0}
|
|
35
|
+
th,td{text-align:left;padding:.35rem .5rem;border-bottom:1px solid #21262d}
|
|
36
|
+
th{font-weight:600;color:#f0f6fc}
|
|
37
|
+
.recipe{background:#161b22;border:1px solid #30363d;border-radius:4px;margin:.3rem 0;overflow:hidden}
|
|
38
|
+
.recipe summary{padding:.5rem .75rem;cursor:pointer;font-weight:600;color:#f0f6fc;font-size:.85rem}
|
|
39
|
+
.recipe-body{padding:0 .75rem .5rem}
|
|
40
|
+
.filter{padding:.2rem .5rem;border-radius:3px;border:1px solid #30363d;background:transparent;color:#8b949e;cursor:pointer;font-size:.7rem;margin:.15rem}
|
|
41
|
+
.filter.active{background:#238636;border-color:#238636;color:#fff}
|
|
42
|
+
.badge{display:inline-block;padding:.1rem .3rem;border-radius:3px;font-size:.65rem;font-weight:bold}
|
|
43
|
+
.badge-bash{background:#1f6feb22;color:#58a6ff;border:1px solid #1f6feb44}
|
|
44
|
+
.badge-js{background:#f0e68c22;color:#f0e68c;border:1px solid #f0e68c44}
|
|
45
|
+
.badge-py{background:#3fb95022;color:#3fb950;border:1px solid #3fb95044}
|
|
46
|
+
.badge-ts{background:#388bfd22;color:#388bfd;border:1px solid #388bfd44}
|
|
47
|
+
.check{color:#3fb950} .cross{color:#484f58}
|
|
48
|
+
.footer{text-align:center;color:#484f58;font-size:.7rem;padding:2rem 1rem}
|
|
49
|
+
@media print{.nav{display:none} .page{display:block!important}}
|
|
50
|
+
</style>
|
|
51
|
+
</head>
|
|
52
|
+
<body>
|
|
53
|
+
|
|
54
|
+
<nav class="nav">
|
|
55
|
+
<a onclick="go('audit')" id="nav-audit" class="active">Audit</a>
|
|
56
|
+
<a onclick="go('builder')" id="nav-builder">Hook Builder</a>
|
|
57
|
+
<a onclick="go('cookbook')" id="nav-cookbook">Cookbook</a>
|
|
58
|
+
<a onclick="go('ecosystem')" id="nav-ecosystem">Ecosystem</a>
|
|
59
|
+
<a onclick="go('cheatsheet')" id="nav-cheatsheet">Cheat Sheet</a>
|
|
60
|
+
</nav>
|
|
61
|
+
|
|
62
|
+
<!-- PAGE: AUDIT -->
|
|
63
|
+
<div class="page active" id="page-audit">
|
|
64
|
+
<h1>Safety Audit</h1>
|
|
65
|
+
<p>Paste your <code>~/.claude/settings.json</code>. Nothing leaves your browser.</p>
|
|
66
|
+
<textarea id="settings" placeholder='{"permissions":{"allow":["Bash(git:*)"]},"hooks":{"PreToolUse":[...]}}'></textarea>
|
|
67
|
+
<button onclick="runAudit()">Run Audit</button>
|
|
68
|
+
<button class="btn-secondary" onclick="generateFresh()">Generate Fresh Setup</button>
|
|
69
|
+
<div id="audit-results"></div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<!-- PAGE: HOOK BUILDER -->
|
|
73
|
+
<div class="page" id="page-builder">
|
|
74
|
+
<h1>Hook Builder</h1>
|
|
75
|
+
<p>Build a custom hook without writing code.</p>
|
|
76
|
+
<div style="display:flex;gap:.75rem;flex-wrap:wrap">
|
|
77
|
+
<div style="flex:1;min-width:180px">
|
|
78
|
+
<label style="font-size:.75rem;color:#8b949e">Action</label>
|
|
79
|
+
<select id="hb-action"><option value="block">Block</option><option value="warn">Warn</option><option value="approve">Auto-approve</option></select>
|
|
80
|
+
</div>
|
|
81
|
+
<div style="flex:2;min-width:250px">
|
|
82
|
+
<label style="font-size:.75rem;color:#8b949e">Pattern (regex)</label>
|
|
83
|
+
<input id="hb-pattern" placeholder="e.g. rm\s+-rf, git push --force">
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<label style="font-size:.75rem;color:#8b949e">Message</label>
|
|
87
|
+
<input id="hb-message" placeholder="e.g. Run tests before pushing">
|
|
88
|
+
<button onclick="buildHook()">Generate</button>
|
|
89
|
+
<div id="hb-result"></div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<!-- PAGE: COOKBOOK -->
|
|
93
|
+
<div class="page" id="page-cookbook">
|
|
94
|
+
<h1>Hooks Cookbook</h1>
|
|
95
|
+
<p>Copy-paste recipes from real GitHub Issues.</p>
|
|
96
|
+
<input id="cb-search" placeholder="Search... (database, git, deploy)" oninput="filterRecipes()">
|
|
97
|
+
<div style="margin:.3rem 0" id="cb-filters"></div>
|
|
98
|
+
<div id="cb-count" style="color:#8b949e;font-size:.8rem"></div>
|
|
99
|
+
<div id="cb-list"></div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<!-- PAGE: ECOSYSTEM -->
|
|
103
|
+
<div class="page" id="page-ecosystem">
|
|
104
|
+
<h1>Ecosystem Comparison</h1>
|
|
105
|
+
<p>All major Claude Code hook projects compared.</p>
|
|
106
|
+
<table>
|
|
107
|
+
<tr><th>Project</th><th>Lang</th><th>Hooks</th><th>Install</th></tr>
|
|
108
|
+
<tr><td><a href="https://github.com/kenryu42/claude-code-safety-net">safety-net</a></td><td><span class="badge badge-ts">TS</span></td><td>5</td><td>npx</td></tr>
|
|
109
|
+
<tr><td><a href="https://github.com/yurukusa/cc-safe-setup">cc-safe-setup</a></td><td><span class="badge badge-bash">Bash</span></td><td>8+39</td><td>npx</td></tr>
|
|
110
|
+
<tr><td><a href="https://github.com/karanb192/claude-code-hooks">karanb192</a></td><td><span class="badge badge-js">JS</span></td><td>5+</td><td>copy</td></tr>
|
|
111
|
+
<tr><td><a href="https://github.com/disler/claude-code-hooks-mastery">mastery</a></td><td><span class="badge badge-py">Python</span></td><td>12</td><td>copy</td></tr>
|
|
112
|
+
<tr><td><a href="https://github.com/lasso-security/claude-hooks">lasso</a></td><td><span class="badge badge-py">Python</span></td><td>1</td><td>install.sh</td></tr>
|
|
113
|
+
</table>
|
|
114
|
+
<h2>Feature Matrix</h2>
|
|
115
|
+
<table>
|
|
116
|
+
<tr><th>Feature</th><th>safety-net</th><th>cc-safe-setup</th><th>karanb192</th><th>mastery</th></tr>
|
|
117
|
+
<tr><td>rm -rf blocker</td><td class="check">✓</td><td class="check">✓</td><td class="check">✓</td><td class="check">✓</td></tr>
|
|
118
|
+
<tr><td>Branch guard</td><td class="check">✓</td><td class="check">✓</td><td class="cross">-</td><td class="cross">-</td></tr>
|
|
119
|
+
<tr><td>Secret guard</td><td class="cross">-</td><td class="check">✓</td><td class="check">✓</td><td class="cross">-</td></tr>
|
|
120
|
+
<tr><td>Syntax check</td><td class="cross">-</td><td class="check">✓</td><td class="cross">-</td><td class="cross">-</td></tr>
|
|
121
|
+
<tr><td>Context monitor</td><td class="cross">-</td><td class="check">✓</td><td class="cross">-</td><td class="cross">-</td></tr>
|
|
122
|
+
<tr><td>Hook generator</td><td class="cross">-</td><td class="check">✓</td><td class="cross">-</td><td class="cross">-</td></tr>
|
|
123
|
+
<tr><td>Dashboard</td><td class="cross">-</td><td class="check">✓</td><td class="cross">-</td><td class="cross">-</td></tr>
|
|
124
|
+
<tr><td>GitHub Action</td><td class="cross">-</td><td class="check">✓</td><td class="cross">-</td><td class="cross">-</td></tr>
|
|
125
|
+
</table>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<!-- PAGE: CHEAT SHEET -->
|
|
129
|
+
<div class="page" id="page-cheatsheet">
|
|
130
|
+
<h1>Hooks Cheat Sheet</h1>
|
|
131
|
+
<p>Print this page (Ctrl+P) for a quick reference.</p>
|
|
132
|
+
|
|
133
|
+
<h3>Lifecycle</h3>
|
|
134
|
+
<pre>Prompt → PreToolUse → Tool → PostToolUse → Stop</pre>
|
|
135
|
+
|
|
136
|
+
<h3>Exit Codes</h3>
|
|
137
|
+
<table><tr><th>Code</th><th>Meaning</th></tr>
|
|
138
|
+
<tr><td><code>0</code></td><td>Allow</td></tr>
|
|
139
|
+
<tr><td><code>2</code></td><td><strong>Block</strong></td></tr></table>
|
|
140
|
+
|
|
141
|
+
<h3>Minimal Block Hook</h3>
|
|
142
|
+
<pre>#!/bin/bash
|
|
143
|
+
CMD=$(cat | jq -r '.tool_input.command // empty')
|
|
144
|
+
[ -z "$CMD" ] && exit 0
|
|
145
|
+
echo "$CMD" | grep -qE 'PATTERN' && echo "BLOCKED" >&2 && exit 2
|
|
146
|
+
exit 0</pre>
|
|
147
|
+
|
|
148
|
+
<h3>Auto-Approve Hook</h3>
|
|
149
|
+
<pre>#!/bin/bash
|
|
150
|
+
CMD=$(cat | jq -r '.tool_input.command // empty')
|
|
151
|
+
[ -z "$CMD" ] && exit 0
|
|
152
|
+
echo "$CMD" | grep -qE '^git\s+(status|log|diff)' && \
|
|
153
|
+
jq -n '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
|
|
154
|
+
exit 0</pre>
|
|
155
|
+
|
|
156
|
+
<h3>Quick Commands</h3>
|
|
157
|
+
<table>
|
|
158
|
+
<tr><td><code>npx cc-safe-setup</code></td><td>Install 8 hooks</td></tr>
|
|
159
|
+
<tr><td><code>--create "desc"</code></td><td>Generate hook</td></tr>
|
|
160
|
+
<tr><td><code>--audit</code></td><td>Score 0-100</td></tr>
|
|
161
|
+
<tr><td><code>--dashboard</code></td><td>Live status</td></tr>
|
|
162
|
+
<tr><td><code>--doctor</code></td><td>Diagnose</td></tr>
|
|
163
|
+
<tr><td><code>--benchmark</code></td><td>Speed test</td></tr>
|
|
164
|
+
</table>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<div class="footer">
|
|
168
|
+
cc-safe-setup · 100% client-side · <a href="https://github.com/yurukusa/cc-safe-setup">GitHub</a> · <a href="https://www.npmjs.com/package/cc-safe-setup">npm</a>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<script>
|
|
172
|
+
// Navigation
|
|
173
|
+
function go(page) {
|
|
174
|
+
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
175
|
+
document.querySelectorAll('.nav a').forEach(a => a.classList.remove('active'));
|
|
176
|
+
document.getElementById('page-' + page).classList.add('active');
|
|
177
|
+
document.getElementById('nav-' + page).classList.add('active');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// AUDIT
|
|
181
|
+
function runAudit() {
|
|
182
|
+
const raw = document.getElementById('settings').value.trim();
|
|
183
|
+
const el = document.getElementById('audit-results');
|
|
184
|
+
if (!raw) { generateFresh(); return; }
|
|
185
|
+
let s; try { s = JSON.parse(raw); } catch { el.innerHTML='<p style="color:#f85149">Invalid JSON</p>'; return; }
|
|
186
|
+
const {risks,good,score} = analyze(s);
|
|
187
|
+
renderAudit(risks, good, score, el);
|
|
188
|
+
}
|
|
189
|
+
function generateFresh() {
|
|
190
|
+
const {risks,good,score} = analyze({});
|
|
191
|
+
renderAudit(risks, good, score, document.getElementById('audit-results'));
|
|
192
|
+
}
|
|
193
|
+
function analyze(s) {
|
|
194
|
+
const risks=[], good=[];
|
|
195
|
+
const pre=s.hooks?.PreToolUse||[], post=s.hooks?.PostToolUse||[];
|
|
196
|
+
const all=JSON.stringify(pre).toLowerCase();
|
|
197
|
+
if(!pre.length) risks.push({s:'CRITICAL',i:'No PreToolUse hooks',f:'npx cc-safe-setup'});
|
|
198
|
+
else { good.push('PreToolUse ('+pre.length+')');
|
|
199
|
+
if(!all.match(/destructive|guard|rm.*rf/)) risks.push({s:'HIGH',i:'No destructive guard',f:'npx cc-safe-setup'});
|
|
200
|
+
else good.push('Destructive protection');
|
|
201
|
+
if(!all.match(/branch|push|main/)) risks.push({s:'HIGH',i:'No branch guard',f:'npx cc-safe-setup'});
|
|
202
|
+
else good.push('Branch protection');
|
|
203
|
+
if(!all.match(/secret|env|credential/)) risks.push({s:'HIGH',i:'No secret guard',f:'npx cc-safe-setup'});
|
|
204
|
+
else good.push('Secret protection');
|
|
205
|
+
}
|
|
206
|
+
if(!post.length) risks.push({s:'MEDIUM',i:'No PostToolUse hooks',f:'npx cc-safe-setup'});
|
|
207
|
+
else good.push('PostToolUse ('+post.length+')');
|
|
208
|
+
const score=Math.max(0,100-risks.reduce((n,r)=>n+(r.s==='CRITICAL'?30:r.s==='HIGH'?20:10),0));
|
|
209
|
+
return {risks,good,score};
|
|
210
|
+
}
|
|
211
|
+
function renderAudit(risks,good,score,el) {
|
|
212
|
+
const cls=score>=80?'good':score>=50?'mid':'bad';
|
|
213
|
+
let h='<div class="score '+cls+'">'+score+'/100</div>';
|
|
214
|
+
if(good.length) { h+='<h3 style="color:#3fb950">Working</h3>'; good.forEach(g=>h+='<div class="good-item">✓ '+g+'</div>'); }
|
|
215
|
+
if(risks.length) { h+='<h3>Risks ('+risks.length+')</h3>'; risks.forEach(r=>h+='<div class="risk"><strong>['+r.s+']</strong> '+r.i+'<br><code>'+r.f+'</code></div>'); }
|
|
216
|
+
if(!risks.length) h+='<p style="color:#3fb950">No risks detected.</p>';
|
|
217
|
+
el.innerHTML=h;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// HOOK BUILDER
|
|
221
|
+
function buildHook() {
|
|
222
|
+
const action=document.getElementById('hb-action').value;
|
|
223
|
+
const pattern=document.getElementById('hb-pattern').value.trim();
|
|
224
|
+
const message=document.getElementById('hb-message').value.trim()||'Blocked';
|
|
225
|
+
const el=document.getElementById('hb-result');
|
|
226
|
+
if(!pattern){el.innerHTML='<p style="color:#f85149">Enter a pattern</p>';return;}
|
|
227
|
+
let s='#!/bin/bash\nCMD=$(cat | jq -r \'.tool_input.command // empty\' 2>/dev/null)\n[ -z "$CMD" ] && exit 0\n';
|
|
228
|
+
if(action==='block') s+='echo "$CMD" | grep -qE \''+pattern+'\' && echo "BLOCKED: '+message+'" >&2 && exit 2\n';
|
|
229
|
+
else if(action==='warn') s+='echo "$CMD" | grep -qE \''+pattern+'\' && echo "WARNING: '+message+'" >&2\n';
|
|
230
|
+
else s+='echo "$CMD" | grep -qE \''+pattern+'\' && jq -n \'{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}\'\n';
|
|
231
|
+
s+='exit 0';
|
|
232
|
+
el.innerHTML='<h3>Script</h3><pre>'+esc(s)+'</pre><p style="font-size:.75rem;color:#8b949e">Save as ~/.claude/hooks/custom.sh, chmod +x, add to settings.json</p>';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// COOKBOOK
|
|
236
|
+
const RECIPES=[
|
|
237
|
+
{cat:'block',t:'Block rm -rf',d:'Destructive commands (#36339)',code:'CMD=$(cat|jq -r \'.tool_input.command // empty\')\n[ -z "$CMD" ] && exit 0\necho "$CMD"|grep -qE \'rm\\s+(-[rf]+\\s+)*/\' && echo "BLOCKED" >&2 && exit 2\nexit 0',trigger:'PreToolUse'},
|
|
238
|
+
{cat:'block',t:'Block force push',d:'Branch protection',code:'CMD=$(cat|jq -r \'.tool_input.command // empty\')\n[ -z "$CMD" ] && exit 0\necho "$CMD"|grep -qE \'git\\s+push.*--force\' && echo "BLOCKED" >&2 && exit 2\nexit 0',trigger:'PreToolUse'},
|
|
239
|
+
{cat:'block',t:'Block .env staging',d:'Secret leak prevention (#6527)',code:'CMD=$(cat|jq -r \'.tool_input.command // empty\')\n[ -z "$CMD" ] && exit 0\necho "$CMD"|grep -qiE \'git\\s+add.*\\.env\' && echo "BLOCKED" >&2 && exit 2\nexit 0',trigger:'PreToolUse'},
|
|
240
|
+
{cat:'block',t:'Block database wipe',d:'migrate:fresh, DROP DATABASE (#37405)',code:'CMD=$(cat|jq -r \'.tool_input.command // empty\')\n[ -z "$CMD" ] && exit 0\necho "$CMD"|grep -qiE \'migrate:fresh|DROP\\s+DATABASE|prisma\\s+migrate\\s+reset\' && echo "BLOCKED" >&2 && exit 2\nexit 0',trigger:'PreToolUse'},
|
|
241
|
+
{cat:'approve',t:'Auto-approve git read',d:'git status/log/diff with -C flag (#36900)',code:'CMD=$(cat|jq -r \'.tool_input.command // empty\')\n[ -z "$CMD" ] && exit 0\necho "$CMD"|grep -qE \'^\\s*git\\s+(-C\\s+\\S+\\s+)?(status|log|diff|branch|show)\' && jq -n \'{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}\'\nexit 0',trigger:'PreToolUse'},
|
|
242
|
+
{cat:'approve',t:'Compound commands',d:'cd && git log auto-approve (#30519)',code:'# Full: npx cc-safe-setup --install-example compound-command-approver',trigger:'PreToolUse'},
|
|
243
|
+
{cat:'detect',t:'Loop detector',d:'Break command repetition (5+ times)',code:'CMD=$(cat|jq -r \'.tool_input.command // empty\')\n[ -z "$CMD" ] && exit 0\nSTATE=/tmp/cc-loop\necho "$CMD">>$STATE\ntail -n 10 $STATE>${STATE}.tmp&&mv ${STATE}.tmp $STATE\nCOUNT=$(grep -cF "$CMD" $STATE||echo 0)\n[ "$COUNT" -ge 5 ]&&echo "BLOCKED: repeated $COUNT times" >&2&&exit 2\nexit 0',trigger:'PreToolUse'},
|
|
244
|
+
{cat:'utility',t:'Session handoff',d:'Save state for next session',code:'# npx cc-safe-setup --install-example session-handoff',trigger:'Stop'},
|
|
245
|
+
{cat:'utility',t:'Cost tracker',d:'Estimate session token cost',code:'# npx cc-safe-setup --install-example cost-tracker',trigger:'PostToolUse'},
|
|
246
|
+
{cat:'utility',t:'tmp cleanup',d:'/tmp/claude-*-cwd files (#8856)',code:'find /tmp -maxdepth 1 -name \'claude-*-cwd\' -type f -mmin +60 -delete 2>/dev/null\nexit 0',trigger:'Stop'},
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
function initCookbook() {
|
|
250
|
+
const cats=['all','block','approve','detect','utility'];
|
|
251
|
+
document.getElementById('cb-filters').innerHTML=cats.map(c=>'<button class="filter'+(c==='all'?' active':'')+'" onclick="setCbFilter(\''+c+'\')">'+c+'</button>').join('');
|
|
252
|
+
filterRecipes();
|
|
253
|
+
}
|
|
254
|
+
let cbFilter='all';
|
|
255
|
+
function setCbFilter(f){cbFilter=f;document.querySelectorAll('#cb-filters .filter').forEach(b=>b.classList.toggle('active',b.textContent===f));filterRecipes();}
|
|
256
|
+
function filterRecipes(){
|
|
257
|
+
const q=(document.getElementById('cb-search')?.value||'').toLowerCase();
|
|
258
|
+
const filtered=RECIPES.filter(r=>(cbFilter==='all'||r.cat===cbFilter)&&(!q||r.t.toLowerCase().includes(q)||r.d.toLowerCase().includes(q)||(r.code||'').toLowerCase().includes(q)));
|
|
259
|
+
document.getElementById('cb-count').textContent=filtered.length+' recipe(s)';
|
|
260
|
+
document.getElementById('cb-list').innerHTML=filtered.map(r=>'<details class="recipe"><summary>'+esc(r.t)+' <span style="color:#484f58;font-size:.7rem">['+r.trigger+']</span></summary><div class="recipe-body"><p style="color:#8b949e;font-size:.8rem">'+esc(r.d)+'</p>'+(r.code?'<pre>'+esc(r.code)+'</pre>':'')+'</div></details>').join('');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function esc(s){return(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
|
264
|
+
|
|
265
|
+
// URL param
|
|
266
|
+
(function(){const p=new URLSearchParams(location.search);const c=p.get('config');if(c){try{document.getElementById('settings').value=atob(c);runAudit();}catch{}}})();
|
|
267
|
+
|
|
268
|
+
initCookbook();
|
|
269
|
+
</script>
|
|
270
|
+
</body>
|
|
271
|
+
</html>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# env-source-guard.sh — Block sourcing .env into shell environment
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude Code sometimes sources .env files directly into bash,
|
|
7
|
+
# causing environment variables to leak across commands.
|
|
8
|
+
# This caused a Laravel test suite to use development database
|
|
9
|
+
# instead of test database, wiping real data.
|
|
10
|
+
#
|
|
11
|
+
# GitHub #401 (54 reactions)
|
|
12
|
+
#
|
|
13
|
+
# TRIGGER: PreToolUse
|
|
14
|
+
# MATCHER: "Bash"
|
|
15
|
+
#
|
|
16
|
+
# WHAT IT BLOCKS:
|
|
17
|
+
# - source .env
|
|
18
|
+
# - . .env
|
|
19
|
+
# - source .env.local
|
|
20
|
+
# - export $(cat .env)
|
|
21
|
+
#
|
|
22
|
+
# WHAT IT ALLOWS:
|
|
23
|
+
# - Reading .env with cat (no sourcing)
|
|
24
|
+
# - Framework commands that load env properly
|
|
25
|
+
# ================================================================
|
|
26
|
+
|
|
27
|
+
INPUT=$(cat)
|
|
28
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
29
|
+
|
|
30
|
+
if [[ -z "$COMMAND" ]]; then
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Block direct sourcing of .env files
|
|
35
|
+
if echo "$COMMAND" | grep -qE '(source|\.\s)\s+\.env'; then
|
|
36
|
+
echo "BLOCKED: Sourcing .env into shell environment." >&2
|
|
37
|
+
echo "Command: $COMMAND" >&2
|
|
38
|
+
echo "" >&2
|
|
39
|
+
echo "This loads all variables into the shell, affecting subsequent commands." >&2
|
|
40
|
+
echo "Use your framework's env loader (dotenv, etc.) instead." >&2
|
|
41
|
+
exit 2
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Block export $(cat .env) pattern
|
|
45
|
+
if echo "$COMMAND" | grep -qE 'export\s+\$\(cat\s+\.env'; then
|
|
46
|
+
echo "BLOCKED: Exporting .env contents into shell." >&2
|
|
47
|
+
echo "Command: $COMMAND" >&2
|
|
48
|
+
exit 2
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
exit 0
|
package/index.mjs
CHANGED
|
@@ -85,6 +85,7 @@ const DIFF_FILE = DIFF_IDX !== -1 ? process.argv[DIFF_IDX + 1] : null;
|
|
|
85
85
|
const SHARE = process.argv.includes('--share');
|
|
86
86
|
const BENCHMARK = process.argv.includes('--benchmark');
|
|
87
87
|
const DASHBOARD = process.argv.includes('--dashboard');
|
|
88
|
+
const ISSUES = process.argv.includes('--issues');
|
|
88
89
|
const CREATE_IDX = process.argv.findIndex(a => a === '--create');
|
|
89
90
|
const CREATE_DESC = CREATE_IDX !== -1 ? process.argv.slice(CREATE_IDX + 1).join(' ') : null;
|
|
90
91
|
|
|
@@ -106,6 +107,7 @@ if (HELP) {
|
|
|
106
107
|
npx cc-safe-setup --audit --json Machine-readable output for CI/CD
|
|
107
108
|
npx cc-safe-setup --scan Detect tech stack, recommend hooks
|
|
108
109
|
npx cc-safe-setup --learn Learn from your block history
|
|
110
|
+
npx cc-safe-setup --issues Show GitHub Issues each hook addresses
|
|
109
111
|
npx cc-safe-setup --dashboard Real-time status dashboard
|
|
110
112
|
npx cc-safe-setup --benchmark Measure hook execution time
|
|
111
113
|
npx cc-safe-setup --share Generate shareable URL for your setup
|
|
@@ -374,9 +376,18 @@ function examples() {
|
|
|
374
376
|
console.log();
|
|
375
377
|
|
|
376
378
|
for (const [cat, hooks] of Object.entries(CATEGORIES)) {
|
|
377
|
-
|
|
379
|
+
// Filter by category name OR hook name/description
|
|
380
|
+
const filteredHooks = filter
|
|
381
|
+
? Object.entries(hooks).filter(([file, desc]) =>
|
|
382
|
+
cat.toLowerCase().includes(filter) ||
|
|
383
|
+
file.toLowerCase().includes(filter) ||
|
|
384
|
+
desc.toLowerCase().includes(filter))
|
|
385
|
+
: Object.entries(hooks);
|
|
386
|
+
|
|
387
|
+
if (filteredHooks.length === 0) continue;
|
|
388
|
+
|
|
378
389
|
console.log(' ' + c.bold + c.blue + cat + c.reset);
|
|
379
|
-
for (const [file, desc] of
|
|
390
|
+
for (const [file, desc] of filteredHooks) {
|
|
380
391
|
console.log(' ' + c.green + '*' + c.reset + ' ' + c.bold + file + c.reset);
|
|
381
392
|
console.log(' ' + c.dim + desc + c.reset);
|
|
382
393
|
}
|
|
@@ -796,71 +807,236 @@ async function fullSetup() {
|
|
|
796
807
|
console.log();
|
|
797
808
|
}
|
|
798
809
|
|
|
810
|
+
function issues() {
|
|
811
|
+
// Map hooks to the GitHub Issues they address
|
|
812
|
+
const ISSUE_MAP = [
|
|
813
|
+
{ hook: 'destructive-guard', issues: ['#36339 rm -rf NTFS junction (93r)', '#36640 NFS mount deletion', '#37331 PowerShell Remove-Item (13r)', '#36233 Mac filesystem deleted (67r)'] },
|
|
814
|
+
{ hook: 'branch-guard', issues: ['Untested code pushed to main at 3am'] },
|
|
815
|
+
{ hook: 'secret-guard', issues: ['#6527 .env committed to public repo (94r)'] },
|
|
816
|
+
{ hook: 'syntax-check', issues: ['Syntax errors cascading through 30+ files'] },
|
|
817
|
+
{ hook: 'context-monitor', issues: ['Sessions dying at 3% context with no warning'] },
|
|
818
|
+
{ hook: 'comment-strip', issues: ['#29582 Bash comments break permissions (18r)'] },
|
|
819
|
+
{ hook: 'cd-git-allow', issues: ['#32985 cd+git permission spam (9r)', '#16561 Compound commands (101r)'] },
|
|
820
|
+
{ hook: 'api-error-alert', issues: ['Sessions silently dying from rate limits'] },
|
|
821
|
+
{ hook: 'block-database-wipe', issues: ['#37405 Database destroyed (0r)', '#34729 Prisma migrate reset data loss'] },
|
|
822
|
+
{ hook: 'compound-command-approver', issues: ['#30519 Permission matching broken (53r)', '#16561 Parse compound commands (101r)'] },
|
|
823
|
+
{ hook: 'case-sensitive-guard', issues: ['#37875 exFAT case collision (0r)'] },
|
|
824
|
+
{ hook: 'tmp-cleanup', issues: ['#8856 /tmp/claude-*-cwd leak (67r)', '#17609 tmp files not cleaned (29r)'] },
|
|
825
|
+
{ hook: 'loop-detector', issues: ['Command repetition loops wasting context'] },
|
|
826
|
+
{ hook: 'session-handoff', issues: ['#17428 Enhanced /compact (104r)', '#6354 CLAUDE.md lost after compact (27r)'] },
|
|
827
|
+
{ hook: 'cost-tracker', issues: ['No visibility into session token costs'] },
|
|
828
|
+
{ hook: 'deploy-guard', issues: ['#37314 Deploy without commit'] },
|
|
829
|
+
{ hook: 'protect-dotfiles', issues: ['#37478 .bashrc destroyed (3r)'] },
|
|
830
|
+
{ hook: 'scope-guard', issues: ['#36233 Files deleted outside project (67r)'] },
|
|
831
|
+
{ hook: 'env-source-guard', issues: ['#401 .env loaded into bash environment (54r)'] },
|
|
832
|
+
{ hook: 'diff-size-guard', issues: ['Unreviable mega-commits'] },
|
|
833
|
+
{ hook: 'dependency-audit', issues: ['Supply chain risk from unknown packages'] },
|
|
834
|
+
{ hook: 'read-before-edit', issues: ['old_string mismatch from editing unread files'] },
|
|
835
|
+
];
|
|
836
|
+
|
|
837
|
+
console.log();
|
|
838
|
+
console.log(c.bold + ' cc-safe-setup --issues' + c.reset);
|
|
839
|
+
console.log(c.dim + ' Which GitHub Issues each hook addresses' + c.reset);
|
|
840
|
+
console.log();
|
|
841
|
+
|
|
842
|
+
let totalIssues = 0;
|
|
843
|
+
for (const entry of ISSUE_MAP) {
|
|
844
|
+
console.log(' ' + c.green + entry.hook + c.reset);
|
|
845
|
+
for (const issue of entry.issues) {
|
|
846
|
+
const isLink = issue.startsWith('#');
|
|
847
|
+
if (isLink) {
|
|
848
|
+
const num = issue.match(/#(\d+)/)?.[1];
|
|
849
|
+
console.log(' ' + c.dim + 'https://github.com/anthropics/claude-code/issues/' + num + c.reset);
|
|
850
|
+
console.log(' ' + issue.replace(/#\d+\s*/, ''));
|
|
851
|
+
} else {
|
|
852
|
+
console.log(' ' + c.dim + issue + c.reset);
|
|
853
|
+
}
|
|
854
|
+
totalIssues++;
|
|
855
|
+
}
|
|
856
|
+
console.log();
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
console.log(c.bold + ' ' + ISSUE_MAP.length + ' hooks addressing ' + totalIssues + ' issues/problems' + c.reset);
|
|
860
|
+
console.log();
|
|
861
|
+
}
|
|
862
|
+
|
|
799
863
|
async function dashboard() {
|
|
800
|
-
const
|
|
801
|
-
const { createInterface: createRL } = await import('readline');
|
|
864
|
+
const fsModule = await import('fs');
|
|
802
865
|
|
|
803
866
|
const BLOCK_LOG = join(HOME, '.claude', 'blocked-commands.log');
|
|
867
|
+
const ERROR_LOG = join(HOME, '.claude', 'session-errors.log');
|
|
804
868
|
const COST_FILE = '/tmp/cc-cost-tracker-calls';
|
|
805
869
|
const CONTEXT_FILE = '/tmp/cc-context-pct';
|
|
870
|
+
const HANDOFF_FILE = join(HOME, '.claude', 'session-handoff.md');
|
|
871
|
+
const W = 60; // dashboard width
|
|
806
872
|
|
|
807
873
|
const clear = () => process.stdout.write('\x1b[2J\x1b[H');
|
|
808
874
|
|
|
809
|
-
//
|
|
810
|
-
|
|
811
|
-
|
|
875
|
+
// ANSI box drawing helpers
|
|
876
|
+
const box = {
|
|
877
|
+
tl: '┌', tr: '┐', bl: '└', br: '┘',
|
|
878
|
+
h: '─', v: '│', lt: '├', rt: '┤',
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
function hline(left, right, w) { return left + box.h.repeat(w - 2) + right; }
|
|
882
|
+
function pad(text, w) {
|
|
883
|
+
const stripped = text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
884
|
+
const padding = Math.max(0, w - 2 - stripped.length);
|
|
885
|
+
return box.v + ' ' + text + ' '.repeat(padding) + box.v;
|
|
886
|
+
}
|
|
887
|
+
function progressBar(pct, w, filledColor, emptyColor) {
|
|
888
|
+
const barW = w - 2;
|
|
889
|
+
const filled = Math.round(pct / 100 * barW);
|
|
890
|
+
return filledColor + '█'.repeat(filled) + emptyColor + '░'.repeat(barW - filled) + c.reset;
|
|
891
|
+
}
|
|
892
|
+
function sparkline(values, w) {
|
|
893
|
+
const chars = ' ▁▂▃▄▅▆▇';
|
|
894
|
+
const max = Math.max(...values, 1);
|
|
895
|
+
return values.slice(-w).map(v => chars[Math.min(7, Math.round(v / max * 7))]).join('');
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Collect hook info
|
|
899
|
+
let hooksByTrigger = {};
|
|
900
|
+
let totalHooks = 0;
|
|
901
|
+
let scriptCount = 0;
|
|
812
902
|
if (existsSync(SETTINGS_PATH)) {
|
|
813
903
|
try {
|
|
814
904
|
const s = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
815
|
-
for (const entries of Object.
|
|
816
|
-
|
|
905
|
+
for (const [trigger, entries] of Object.entries(s.hooks || {})) {
|
|
906
|
+
const count = entries.reduce((n, e) => n + (e.hooks || []).length, 0);
|
|
907
|
+
hooksByTrigger[trigger] = count;
|
|
908
|
+
totalHooks += count;
|
|
817
909
|
}
|
|
818
910
|
} catch {}
|
|
819
911
|
}
|
|
820
|
-
|
|
821
|
-
|
|
912
|
+
if (existsSync(HOOKS_DIR)) {
|
|
913
|
+
scriptCount = fsModule.readdirSync(HOOKS_DIR).filter(f => f.endsWith('.sh')).length;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Audit score (cached)
|
|
917
|
+
let auditScore = '?';
|
|
918
|
+
try {
|
|
919
|
+
// Quick inline audit
|
|
920
|
+
let risks = 0;
|
|
921
|
+
const s = existsSync(SETTINGS_PATH) ? JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')) : {};
|
|
922
|
+
const pre = s.hooks?.PreToolUse || [];
|
|
923
|
+
const post = s.hooks?.PostToolUse || [];
|
|
924
|
+
if (pre.length === 0) risks += 30;
|
|
925
|
+
const allCmds = JSON.stringify(pre).toLowerCase();
|
|
926
|
+
if (!allCmds.match(/destructive|guard|rm.*rf/)) risks += 20;
|
|
927
|
+
if (!allCmds.match(/branch|push|main/)) risks += 20;
|
|
928
|
+
if (!allCmds.match(/secret|env|credential/)) risks += 20;
|
|
929
|
+
if (post.length === 0) risks += 10;
|
|
930
|
+
auditScore = Math.max(0, 100 - risks);
|
|
931
|
+
} catch {}
|
|
822
932
|
|
|
823
933
|
function render() {
|
|
824
934
|
clear();
|
|
825
935
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
// Status row
|
|
831
|
-
const context = existsSync(CONTEXT_FILE) ? readFileSync(CONTEXT_FILE, 'utf-8').trim() + '%' : '?';
|
|
832
|
-
const calls = existsSync(COST_FILE) ? readFileSync(COST_FILE, 'utf-8').trim() : '0';
|
|
833
|
-
const cost = (parseInt(calls) * 0.105).toFixed(2);
|
|
936
|
+
const now = new Date();
|
|
937
|
+
const timeStr = now.toLocaleTimeString();
|
|
938
|
+
const dateStr = now.toLocaleDateString();
|
|
834
939
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
940
|
+
// Read live data
|
|
941
|
+
const contextPct = existsSync(CONTEXT_FILE) ? parseInt(readFileSync(CONTEXT_FILE, 'utf-8').trim()) || 0 : -1;
|
|
942
|
+
const toolCalls = existsSync(COST_FILE) ? parseInt(readFileSync(COST_FILE, 'utf-8').trim()) || 0 : 0;
|
|
943
|
+
const costEst = (toolCalls * 0.105).toFixed(2);
|
|
838
944
|
|
|
839
|
-
//
|
|
840
|
-
|
|
945
|
+
// Parse block log
|
|
946
|
+
let blocks = [];
|
|
947
|
+
let blocksByHour = new Array(24).fill(0);
|
|
948
|
+
let blockReasons = {};
|
|
841
949
|
if (existsSync(BLOCK_LOG)) {
|
|
842
950
|
const lines = readFileSync(BLOCK_LOG, 'utf-8').split('\n').filter(l => l.trim());
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
const m = line.match(/^\[([^\]]+)\]\s*BLOCKED:\s*(.+?)\s*\|/);
|
|
951
|
+
for (const line of lines) {
|
|
952
|
+
const m = line.match(/^\[([^\]]+)\]\s*BLOCKED:\s*(.+?)\s*\|\s*cmd:\s*(.+)$/);
|
|
846
953
|
if (m) {
|
|
847
|
-
const
|
|
848
|
-
|
|
954
|
+
const hour = new Date(m[1]).getHours();
|
|
955
|
+
if (!isNaN(hour)) blocksByHour[hour]++;
|
|
956
|
+
const reason = m[2].trim();
|
|
957
|
+
blockReasons[reason] = (blockReasons[reason] || 0) + 1;
|
|
958
|
+
blocks.push({ time: m[1], reason, cmd: m[3].trim() });
|
|
849
959
|
}
|
|
850
960
|
}
|
|
851
|
-
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const totalBlocks = blocks.length;
|
|
964
|
+
const todayBlocks = blocks.filter(b => b.time.startsWith(dateStr.split('/').reverse().join('-'))).length;
|
|
965
|
+
|
|
966
|
+
// === RENDER ===
|
|
967
|
+
console.log(hline(box.tl, box.tr, W));
|
|
968
|
+
console.log(pad(c.bold + 'cc-safe-setup dashboard' + c.reset + ' ' + c.dim + timeStr + c.reset, W));
|
|
969
|
+
console.log(hline(box.lt, box.rt, W));
|
|
970
|
+
|
|
971
|
+
// Status panel
|
|
972
|
+
const scoreColor = auditScore >= 80 ? c.green : auditScore >= 50 ? c.yellow : c.red;
|
|
973
|
+
const grade = auditScore >= 80 ? 'A' : auditScore >= 60 ? 'B' : auditScore >= 40 ? 'C' : 'F';
|
|
974
|
+
console.log(pad('Score: ' + scoreColor + auditScore + '/100' + c.reset + ' (Grade ' + grade + ') Hooks: ' + c.green + totalHooks + c.reset + ' Scripts: ' + scriptCount, W));
|
|
975
|
+
|
|
976
|
+
// Context bar
|
|
977
|
+
if (contextPct >= 0) {
|
|
978
|
+
const ctxColor = contextPct > 40 ? c.green : contextPct > 20 ? c.yellow : c.red;
|
|
979
|
+
console.log(pad('Context: ' + ctxColor + contextPct + '%' + c.reset + ' ' + progressBar(contextPct, 30, ctxColor, c.dim), W));
|
|
852
980
|
} else {
|
|
853
|
-
console.log(c.dim + '
|
|
981
|
+
console.log(pad('Context: ' + c.dim + 'unknown' + c.reset, W));
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Cost
|
|
985
|
+
console.log(pad('Cost: ~$' + costEst + ' (' + toolCalls + ' tool calls, Opus)', W));
|
|
986
|
+
console.log(pad('Blocks: ' + c.red + totalBlocks + c.reset + ' total | Today: ' + todayBlocks, W));
|
|
987
|
+
|
|
988
|
+
// Hooks by trigger
|
|
989
|
+
console.log(hline(box.lt, box.rt, W));
|
|
990
|
+
console.log(pad(c.bold + 'Hooks by Trigger' + c.reset, W));
|
|
991
|
+
for (const [trigger, count] of Object.entries(hooksByTrigger)) {
|
|
992
|
+
const bar = '█'.repeat(Math.min(count, 20));
|
|
993
|
+
console.log(pad(c.dim + trigger.padEnd(18) + c.reset + c.blue + bar + c.reset + ' ' + count, W));
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Hourly activity sparkline
|
|
997
|
+
console.log(hline(box.lt, box.rt, W));
|
|
998
|
+
console.log(pad(c.bold + 'Block Activity (24h)' + c.reset, W));
|
|
999
|
+
console.log(pad(c.yellow + sparkline(blocksByHour, 24) + c.reset + ' ' + c.dim + '0h' + ' '.repeat(20) + '23h' + c.reset, W));
|
|
1000
|
+
|
|
1001
|
+
// Top block reasons
|
|
1002
|
+
console.log(hline(box.lt, box.rt, W));
|
|
1003
|
+
console.log(pad(c.bold + 'Top Block Reasons' + c.reset, W));
|
|
1004
|
+
const sortedReasons = Object.entries(blockReasons).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
1005
|
+
const maxR = sortedReasons[0]?.[1] || 1;
|
|
1006
|
+
for (const [reason, count] of sortedReasons) {
|
|
1007
|
+
const bar = '▓'.repeat(Math.ceil(count / maxR * 15));
|
|
1008
|
+
console.log(pad(c.red + bar + c.reset + ' ' + count + ' ' + c.dim + reason.slice(0, 25) + c.reset, W));
|
|
1009
|
+
}
|
|
1010
|
+
if (sortedReasons.length === 0) {
|
|
1011
|
+
console.log(pad(c.dim + '(no blocks recorded)' + c.reset, W));
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Recent blocks
|
|
1015
|
+
console.log(hline(box.lt, box.rt, W));
|
|
1016
|
+
console.log(pad(c.bold + 'Recent Blocks' + c.reset, W));
|
|
1017
|
+
const recent = blocks.slice(-5);
|
|
1018
|
+
for (const b of recent) {
|
|
1019
|
+
const time = b.time.replace(/T/, ' ').replace(/\+.*/, '').slice(11, 16);
|
|
1020
|
+
console.log(pad(c.dim + time + c.reset + ' ' + c.red + b.reason.slice(0, 35) + c.reset, W));
|
|
1021
|
+
}
|
|
1022
|
+
if (recent.length === 0) console.log(pad(c.dim + '(none)' + c.reset, W));
|
|
1023
|
+
|
|
1024
|
+
// Session errors
|
|
1025
|
+
let errorCount = 0;
|
|
1026
|
+
if (existsSync(ERROR_LOG)) {
|
|
1027
|
+
errorCount = readFileSync(ERROR_LOG, 'utf-8').split('\n').filter(l => l.trim()).length;
|
|
1028
|
+
}
|
|
1029
|
+
if (errorCount > 0) {
|
|
1030
|
+
console.log(hline(box.lt, box.rt, W));
|
|
1031
|
+
console.log(pad(c.yellow + 'Session errors: ' + errorCount + c.reset, W));
|
|
854
1032
|
}
|
|
855
1033
|
|
|
856
|
-
console.log(
|
|
1034
|
+
console.log(hline(box.bl, box.br, W));
|
|
857
1035
|
console.log(c.dim + ' Refreshing every 3s. Ctrl+C to exit.' + c.reset);
|
|
858
1036
|
}
|
|
859
1037
|
|
|
860
1038
|
render();
|
|
861
1039
|
setInterval(render, 3000);
|
|
862
|
-
|
|
863
|
-
// Keep alive
|
|
864
1040
|
await new Promise(() => {});
|
|
865
1041
|
}
|
|
866
1042
|
|
|
@@ -2151,6 +2327,7 @@ async function main() {
|
|
|
2151
2327
|
if (FULL) return fullSetup();
|
|
2152
2328
|
if (DOCTOR) return doctor();
|
|
2153
2329
|
if (WATCH) return watch();
|
|
2330
|
+
if (ISSUES) return issues();
|
|
2154
2331
|
if (DASHBOARD) return dashboard();
|
|
2155
2332
|
if (BENCHMARK) return benchmark();
|
|
2156
2333
|
if (SHARE) return share();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "One command to make Claude Code safe for autonomous operation. 8 built-in +
|
|
3
|
+
"version": "5.1.0",
|
|
4
|
+
"description": "One command to make Claude Code safe for autonomous operation. 8 built-in + 39 examples. 23 commands including dashboard, issues, create, audit, lint, diff. 260 tests. 2,500+ daily npm downloads.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cc-safe-setup": "index.mjs"
|