cc-safe-setup 5.0.0 → 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/docs/app.html +271 -0
- package/examples/env-source-guard.sh +51 -0
- package/index.mjs +56 -0
- package/package.json +2 -2
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
|
|
@@ -805,6 +807,59 @@ async function fullSetup() {
|
|
|
805
807
|
console.log();
|
|
806
808
|
}
|
|
807
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
|
+
|
|
808
863
|
async function dashboard() {
|
|
809
864
|
const fsModule = await import('fs');
|
|
810
865
|
|
|
@@ -2272,6 +2327,7 @@ async function main() {
|
|
|
2272
2327
|
if (FULL) return fullSetup();
|
|
2273
2328
|
if (DOCTOR) return doctor();
|
|
2274
2329
|
if (WATCH) return watch();
|
|
2330
|
+
if (ISSUES) return issues();
|
|
2275
2331
|
if (DASHBOARD) return dashboard();
|
|
2276
2332
|
if (BENCHMARK) return benchmark();
|
|
2277
2333
|
if (SHARE) return share();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "5.
|
|
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"
|