cc-safe-setup 5.3.1 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/audit-web/index.html +228 -642
- package/docs/index-legacy.html +685 -0
- package/docs/index.html +228 -642
- package/examples/reinject-claudemd.sh +44 -0
- package/index.mjs +6 -0
- package/package.json +1 -1
- package/docs/app.html +0 -271
package/audit-web/index.html
CHANGED
|
@@ -3,683 +3,269 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>Claude Code Safety
|
|
7
|
-
<meta name="description" content="Audit
|
|
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
8
|
<style>
|
|
9
|
-
*
|
|
10
|
-
body
|
|
11
|
-
.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
.
|
|
30
|
-
.
|
|
31
|
-
.
|
|
32
|
-
.risk
|
|
33
|
-
.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
.
|
|
38
|
-
.
|
|
39
|
-
.
|
|
40
|
-
.
|
|
41
|
-
.
|
|
42
|
-
.
|
|
43
|
-
.
|
|
44
|
-
.
|
|
45
|
-
.
|
|
46
|
-
.
|
|
47
|
-
.
|
|
48
|
-
.
|
|
49
|
-
.
|
|
50
|
-
.hook-card .desc { color: #8b949e; font-size: 0.85rem; margin-top: 0.25rem; }
|
|
51
|
-
.setup-steps { counter-reset: step; list-style: none; padding: 0; }
|
|
52
|
-
.setup-steps li { counter-increment: step; padding: 0.75rem 0 0.75rem 2.5rem; position: relative; border-left: 2px solid #30363d; margin-left: 1rem; }
|
|
53
|
-
.setup-steps li::before { content: counter(step); position: absolute; left: -1rem; top: 0.65rem; width: 2rem; height: 2rem; background: #238636; color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.8rem; font-weight: bold; }
|
|
54
|
-
.setup-steps li:last-child { border-left: 2px solid transparent; }
|
|
55
|
-
.badge-row { display: flex; gap: 0.5rem; margin: 1rem 0; flex-wrap: wrap; }
|
|
56
|
-
.badge { display: inline-block; padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: bold; }
|
|
57
|
-
.badge-green { background: #238636; color: #fff; }
|
|
58
|
-
.badge-yellow { background: #9e6a03; color: #fff; }
|
|
59
|
-
.badge-red { background: #da3633; color: #fff; }
|
|
60
|
-
.empty-state { text-align: center; padding: 3rem 1rem; color: #484f58; }
|
|
61
|
-
.empty-state p { margin: 0.5rem 0; }
|
|
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}}
|
|
62
50
|
</style>
|
|
63
51
|
</head>
|
|
64
52
|
<body>
|
|
65
|
-
<div class="container">
|
|
66
|
-
<h1>Claude Code Safety Audit</h1>
|
|
67
|
-
<p class="subtitle">Audit your setup and generate safety hooks — 100% in your browser. No npm required.</p>
|
|
68
53
|
|
|
69
|
-
<
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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>
|
|
85
70
|
</div>
|
|
86
71
|
|
|
87
|
-
|
|
88
|
-
<div class="
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
<
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
<
|
|
95
|
-
<div style="flex:1;min-width:200px">
|
|
96
|
-
<label style="color:#8b949e;font-size:.8rem">What should the hook do?</label>
|
|
97
|
-
<select id="hb-action" style="width:100%;padding:.5rem;background:#161b22;border:1px solid #30363d;border-radius:4px;color:#c9d1d9;margin-top:.25rem">
|
|
98
|
-
<option value="block">Block a command</option>
|
|
99
|
-
<option value="warn">Warn about a command</option>
|
|
100
|
-
<option value="approve">Auto-approve a command</option>
|
|
101
|
-
</select>
|
|
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>
|
|
102
80
|
</div>
|
|
103
|
-
<div style="flex:2;min-width:
|
|
104
|
-
<label style="
|
|
105
|
-
<input id="hb-pattern" placeholder="e.g. rm\s+-rf, git push --force
|
|
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">
|
|
106
84
|
</div>
|
|
107
85
|
</div>
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
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>
|
|
111
90
|
</div>
|
|
112
|
-
<button class="btn" onclick="buildHook()">Generate Hook</button>
|
|
113
|
-
|
|
114
|
-
<div id="hb-result" style="margin-top:1rem"></div>
|
|
115
91
|
|
|
116
|
-
|
|
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>
|
|
117
100
|
</div>
|
|
118
101
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
name: 'cd+git Auto-Approver',
|
|
145
|
-
desc: 'Auto-approves read-only cd + git compound commands'
|
|
146
|
-
},
|
|
147
|
-
'syntax-check': {
|
|
148
|
-
trigger: 'PostToolUse', matcher: 'Edit|Write',
|
|
149
|
-
name: 'Post-Edit Syntax Validator',
|
|
150
|
-
desc: 'Checks Python, Shell, JSON, YAML, JS syntax after edits'
|
|
151
|
-
},
|
|
152
|
-
'context-monitor': {
|
|
153
|
-
trigger: 'PostToolUse', matcher: '',
|
|
154
|
-
name: 'Context Window Monitor',
|
|
155
|
-
desc: 'Graduated warnings at 40%/25%/20%/15% context remaining'
|
|
156
|
-
},
|
|
157
|
-
'api-error-alert': {
|
|
158
|
-
trigger: 'Stop', matcher: '',
|
|
159
|
-
name: 'API Error Alert',
|
|
160
|
-
desc: 'Desktop notification + log when session stops from API errors'
|
|
161
|
-
}
|
|
162
|
-
};
|
|
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>
|
|
163
127
|
|
|
164
|
-
|
|
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>
|
|
165
166
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
});
|
|
170
|
-
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
|
171
|
-
document.getElementById('tab-' + name).classList.add('active');
|
|
172
|
-
}
|
|
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>
|
|
173
170
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
});
|
|
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');
|
|
182
178
|
}
|
|
183
179
|
|
|
184
|
-
|
|
185
|
-
|
|
180
|
+
// AUDIT
|
|
186
181
|
function runAudit() {
|
|
187
182
|
const raw = document.getElementById('settings').value.trim();
|
|
188
|
-
const el = document.getElementById('results');
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
let settings;
|
|
196
|
-
try {
|
|
197
|
-
settings = JSON.parse(raw);
|
|
198
|
-
} catch(e) {
|
|
199
|
-
el.innerHTML = '<p style="color:#f85149">Invalid JSON. Paste your settings.json content.</p>';
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const {risks, good, score, missing} = analyzeSettings(settings);
|
|
204
|
-
lastAuditState = {settings, risks, good, score, missing};
|
|
205
|
-
|
|
206
|
-
// Show tabs
|
|
207
|
-
document.getElementById('tabs').style.display = 'flex';
|
|
208
|
-
|
|
209
|
-
renderAuditTab(risks, good, score);
|
|
210
|
-
renderFixTab(settings, risks, missing);
|
|
211
|
-
renderManualTab();
|
|
212
|
-
switchTab('audit');
|
|
213
|
-
el.innerHTML = '';
|
|
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);
|
|
214
188
|
}
|
|
215
|
-
|
|
216
189
|
function generateFresh() {
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
lastAuditState = {settings: emptySettings, risks, good, score, missing};
|
|
220
|
-
|
|
221
|
-
document.getElementById('tabs').style.display = 'flex';
|
|
222
|
-
document.getElementById('results').innerHTML = '';
|
|
223
|
-
|
|
224
|
-
renderAuditTab(risks, good, score);
|
|
225
|
-
renderFixTab(emptySettings, risks, Object.keys(HOOKS));
|
|
226
|
-
renderManualTab();
|
|
227
|
-
switchTab('fix');
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function analyzeSettings(settings) {
|
|
231
|
-
const risks = [];
|
|
232
|
-
const good = [];
|
|
233
|
-
const missing = [];
|
|
234
|
-
|
|
235
|
-
const preHooks = settings.hooks?.PreToolUse || [];
|
|
236
|
-
const postHooks = settings.hooks?.PostToolUse || [];
|
|
237
|
-
const stopHooks = settings.hooks?.Stop || [];
|
|
238
|
-
const allCmds = JSON.stringify(preHooks).toLowerCase();
|
|
239
|
-
const postCmds = JSON.stringify(postHooks).toLowerCase();
|
|
240
|
-
const stopCmds = JSON.stringify(stopHooks).toLowerCase();
|
|
241
|
-
|
|
242
|
-
if (preHooks.length === 0) {
|
|
243
|
-
risks.push({ severity: 'CRITICAL', issue: 'No PreToolUse hooks — rm -rf, git reset --hard run unchecked', fix: 'npx cc-safe-setup' });
|
|
244
|
-
missing.push('destructive-guard', 'branch-guard', 'secret-guard', 'comment-strip', 'cd-git-allow');
|
|
245
|
-
} else {
|
|
246
|
-
good.push('PreToolUse hooks (' + preHooks.length + ')');
|
|
247
|
-
if (!allCmds.match(/destructive|guard|block|rm.*rf|reset.*hard/)) {
|
|
248
|
-
risks.push({ severity: 'HIGH', issue: 'No destructive command blocker', fix: 'npx cc-safe-setup' });
|
|
249
|
-
missing.push('destructive-guard');
|
|
250
|
-
} else good.push('Destructive command protection');
|
|
251
|
-
|
|
252
|
-
if (!allCmds.match(/branch|push|main|master/)) {
|
|
253
|
-
risks.push({ severity: 'HIGH', issue: 'No branch push protection', fix: 'npx cc-safe-setup' });
|
|
254
|
-
missing.push('branch-guard');
|
|
255
|
-
} else good.push('Branch push protection');
|
|
256
|
-
|
|
257
|
-
if (!allCmds.match(/secret|env|credential/)) {
|
|
258
|
-
risks.push({ severity: 'HIGH', issue: 'No secret leak protection (.env commits)', fix: 'npx cc-safe-setup' });
|
|
259
|
-
missing.push('secret-guard');
|
|
260
|
-
} else good.push('Secret leak protection');
|
|
261
|
-
|
|
262
|
-
if (!allCmds.match(/comment|strip/)) missing.push('comment-strip');
|
|
263
|
-
if (!allCmds.match(/cd.*git.*allow|auto.*approv/)) missing.push('cd-git-allow');
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (!allCmds.match(/database|wipe|migrate|prisma/)) {
|
|
267
|
-
risks.push({ severity: 'MEDIUM', issue: 'No database wipe protection', fix: 'npx cc-safe-setup --install-example block-database-wipe' });
|
|
268
|
-
} else good.push('Database wipe protection');
|
|
269
|
-
|
|
270
|
-
if (postHooks.length === 0) {
|
|
271
|
-
risks.push({ severity: 'MEDIUM', issue: 'No PostToolUse hooks (no syntax checking)', fix: 'npx cc-safe-setup' });
|
|
272
|
-
missing.push('syntax-check', 'context-monitor');
|
|
273
|
-
} else {
|
|
274
|
-
good.push('PostToolUse hooks (' + postHooks.length + ')');
|
|
275
|
-
if (!postCmds.match(/syntax|check|py_compile|node.*check/)) missing.push('syntax-check');
|
|
276
|
-
if (!postCmds.match(/context|monitor|capacity/)) missing.push('context-monitor');
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (stopHooks.length === 0) {
|
|
280
|
-
missing.push('api-error-alert');
|
|
281
|
-
} else {
|
|
282
|
-
good.push('Stop hooks (' + stopHooks.length + ')');
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const allows = settings.permissions?.allow || [];
|
|
286
|
-
if (allows.includes('Bash(*)')) {
|
|
287
|
-
risks.push({ severity: 'MEDIUM', issue: 'Bash(*) in allow list — all commands auto-approved', fix: 'Use specific patterns like Bash(git:*) instead' });
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const denies = settings.permissions?.deny || [];
|
|
291
|
-
if (denies.length > 0) good.push('Deny rules (' + denies.length + ')');
|
|
292
|
-
|
|
293
|
-
const mode = settings.defaultMode;
|
|
294
|
-
if (mode === 'bypassPermissions') {
|
|
295
|
-
risks.push({ severity: 'HIGH', issue: 'bypassPermissions — all permission checks disabled', fix: 'Use dontAsk with hooks instead' });
|
|
296
|
-
} else if (mode === 'dontAsk') {
|
|
297
|
-
good.push('dontAsk mode (auto-approve, hooks still run)');
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (settings.sandbox?.enabled === false) {
|
|
301
|
-
risks.push({ severity: 'MEDIUM', issue: 'Sandbox disabled', fix: 'Re-enable or use excludedCommands' });
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const score = Math.max(0, 100 - risks.reduce((s, r) => {
|
|
305
|
-
if (r.severity === 'CRITICAL') return s + 30;
|
|
306
|
-
if (r.severity === 'HIGH') return s + 20;
|
|
307
|
-
if (r.severity === 'MEDIUM') return s + 10;
|
|
308
|
-
return s + 5;
|
|
309
|
-
}, 0));
|
|
310
|
-
|
|
311
|
-
return {risks, good, score, missing: [...new Set(missing)]};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function renderAuditTab(risks, good, score) {
|
|
315
|
-
const scoreClass = score >= 80 ? 'good' : score >= 50 ? 'mid' : 'bad';
|
|
316
|
-
let html = '<div class="score ' + scoreClass + '">Safety Score: ' + score + '/100</div>';
|
|
317
|
-
|
|
318
|
-
if (good.length > 0) {
|
|
319
|
-
html += '<h3 style="color:#3fb950">Working</h3>';
|
|
320
|
-
html += good.map(g => '<div class="good-item">' + g + '</div>').join('');
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (risks.length > 0) {
|
|
324
|
-
html += '<h3 style="margin-top:1rem">Risks (' + risks.length + ')</h3>';
|
|
325
|
-
html += risks.map(r =>
|
|
326
|
-
'<div class="risk"><span class="severity ' + r.severity.toLowerCase() + '">[' + r.severity + ']</span> ' + escHtml(r.issue) +
|
|
327
|
-
'<div class="fix">Fix: ' + escHtml(r.fix) + '</div></div>'
|
|
328
|
-
).join('');
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (risks.length === 0) {
|
|
332
|
-
html += '<p style="color:#3fb950;margin-top:1rem">No risks detected. Your setup looks solid.</p>';
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
document.getElementById('tab-audit').innerHTML = html;
|
|
190
|
+
const {risks,good,score} = analyze({});
|
|
191
|
+
renderAudit(risks, good, score, document.getElementById('audit-results'));
|
|
336
192
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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');
|
|
342
205
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
let html = '<h2>Quick Fix</h2>';
|
|
349
|
-
html += '<p style="color:#8b949e;margin-bottom:1rem">Missing ' + missing.length + ' hook(s). Choose your install method:</p>';
|
|
350
|
-
|
|
351
|
-
// Option 1: npx
|
|
352
|
-
html += '<h3>Option 1: One command (recommended)</h3>';
|
|
353
|
-
html += '<div class="code-block"><button class="copy-btn" onclick="copyText(\'npx-cmd\')">Copy</button><pre id="npx-cmd">npx cc-safe-setup</pre></div>';
|
|
354
|
-
|
|
355
|
-
// Option 2: Generated settings.json
|
|
356
|
-
html += '<h3>Option 2: Copy settings.json patch</h3>';
|
|
357
|
-
html += '<p style="color:#8b949e;font-size:0.85rem;margin:0.25rem 0">Merge this into your <code>~/.claude/settings.json</code>:</p>';
|
|
358
|
-
html += '<div class="code-block"><button class="copy-btn" onclick="copyText(\'settings-patch\')">Copy</button><pre id="settings-patch">' + escHtml(JSON.stringify(patch, null, 2)) + '</pre></div>';
|
|
359
|
-
|
|
360
|
-
// Option 3: Full install script
|
|
361
|
-
html += '<h3>Option 3: Shell script (no npm needed)</h3>';
|
|
362
|
-
html += '<p style="color:#8b949e;font-size:0.85rem;margin:0.25rem 0">Run this in your terminal. Creates hook scripts and updates settings.json:</p>';
|
|
363
|
-
html += '<div class="code-block" style="max-height:400px;overflow-y:auto"><button class="copy-btn" onclick="copyText(\'install-script\')">Copy</button><pre id="install-script">' + escHtml(installScript) + '</pre></div>';
|
|
364
|
-
|
|
365
|
-
// Hook details
|
|
366
|
-
html += '<h3 style="margin-top:1.5rem">Hooks being added</h3>';
|
|
367
|
-
for (const id of missing) {
|
|
368
|
-
const h = HOOKS[id];
|
|
369
|
-
if (!h) continue;
|
|
370
|
-
html += '<div class="hook-card"><summary>' + escHtml(h.name) + '</summary>';
|
|
371
|
-
html += '<div class="desc">' + escHtml(h.desc) + ' · Trigger: ' + h.trigger + (h.matcher ? ' · Matcher: ' + h.matcher : '') + '</div></div>';
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
document.getElementById('tab-fix').innerHTML = html;
|
|
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};
|
|
375
210
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
let
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
html += '<li><strong>Create hook scripts</strong><br><span style="color:#8b949e;font-size:0.85rem">Use the "Fix & Generate" tab to get the script content, or run <code>npx cc-safe-setup --dry-run</code> to preview.</span></li>';
|
|
384
|
-
html += '<li><strong>Make scripts executable</strong><div class="code-block"><pre>chmod +x ~/.claude/hooks/*.sh</pre></div></li>';
|
|
385
|
-
html += '<li><strong>Update settings.json</strong><br><span style="color:#8b949e;font-size:0.85rem">Add the hook entries from "Fix & Generate" tab to <code>~/.claude/settings.json</code>.</span></li>';
|
|
386
|
-
html += '<li><strong>Restart Claude Code</strong><br><span style="color:#8b949e;font-size:0.85rem">Close and reopen Claude Code. Hooks activate on restart.</span></li>';
|
|
387
|
-
html += '<li><strong>Verify</strong><div class="code-block"><pre>npx cc-safe-setup --verify</pre></div><span style="color:#8b949e;font-size:0.85rem">Or test manually: try <code>rm -rf /</code> in Claude Code — the hook should block it.</span></li>';
|
|
388
|
-
html += '</ol>';
|
|
389
|
-
|
|
390
|
-
html += '<h3 style="margin-top:1.5rem">Requirements</h3>';
|
|
391
|
-
html += '<ul style="margin-left:1.5rem;color:#8b949e;font-size:0.85rem">';
|
|
392
|
-
html += '<li><code>jq</code> — for JSON parsing in hooks (<code>brew install jq</code> / <code>apt install jq</code>)</li>';
|
|
393
|
-
html += '<li><code>bash</code> — hooks are bash scripts</li>';
|
|
394
|
-
html += '<li>Claude Code v2.1+ — for hooks support</li>';
|
|
395
|
-
html += '</ul>';
|
|
396
|
-
|
|
397
|
-
html += '<h3 style="margin-top:1.5rem">Hooks Reference</h3>';
|
|
398
|
-
for (const [id, h] of Object.entries(HOOKS)) {
|
|
399
|
-
html += '<div class="hook-card"><details><summary>' + escHtml(h.name) + ' <span style="color:#484f58;font-weight:normal;font-size:0.8rem">(' + id + '.sh)</span></summary>';
|
|
400
|
-
html += '<div class="desc" style="margin-top:0.5rem">' + escHtml(h.desc) + '</div>';
|
|
401
|
-
html += '<div style="margin-top:0.25rem;font-size:0.8rem;color:#484f58">Trigger: ' + h.trigger + (h.matcher ? ' | Matcher: ' + h.matcher : ' | Matcher: (all)') + '</div>';
|
|
402
|
-
html += '</details></div>';
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
document.getElementById('tab-manual').innerHTML = html;
|
|
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;
|
|
406
218
|
}
|
|
407
219
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
};
|
|
422
|
-
if (h.trigger === 'PreToolUse') patch.hooks.PreToolUse.push(entry);
|
|
423
|
-
else if (h.trigger === 'PostToolUse') patch.hooks.PostToolUse.push(entry);
|
|
424
|
-
else if (h.trigger === 'Stop') patch.hooks.Stop.push(entry);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Clean empty arrays
|
|
428
|
-
if (patch.hooks.PreToolUse.length === 0) delete patch.hooks.PreToolUse;
|
|
429
|
-
if (patch.hooks.PostToolUse.length === 0) delete patch.hooks.PostToolUse;
|
|
430
|
-
if (patch.hooks.Stop.length === 0) delete patch.hooks.Stop;
|
|
431
|
-
|
|
432
|
-
return patch;
|
|
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>';
|
|
433
233
|
}
|
|
434
234
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
script += '# --- ' + h.name + ' ---\n';
|
|
454
|
-
script += 'cat > "$HOOK_DIR/' + id + '.sh" << \'HOOKEOF\'\n';
|
|
455
|
-
script += getMinimalHookScript(id);
|
|
456
|
-
script += '\nHOOKEOF\n';
|
|
457
|
-
script += 'chmod +x "$HOOK_DIR/' + id + '.sh"\n';
|
|
458
|
-
script += 'echo "Installed: ' + id + '.sh"\n\n';
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Update settings.json
|
|
462
|
-
script += '# --- Update settings.json ---\n';
|
|
463
|
-
script += 'if [ ! -f "$SETTINGS" ]; then\n';
|
|
464
|
-
script += ' echo "{}" > "$SETTINGS"\n';
|
|
465
|
-
script += 'fi\n\n';
|
|
466
|
-
|
|
467
|
-
// Build jq command to merge hooks
|
|
468
|
-
const pre = missing.filter(id => HOOKS[id]?.trigger === 'PreToolUse');
|
|
469
|
-
const post = missing.filter(id => HOOKS[id]?.trigger === 'PostToolUse');
|
|
470
|
-
const stop = missing.filter(id => HOOKS[id]?.trigger === 'Stop');
|
|
471
|
-
|
|
472
|
-
if (pre.length > 0) {
|
|
473
|
-
script += '# Add PreToolUse hooks\n';
|
|
474
|
-
for (const id of pre) {
|
|
475
|
-
const h = HOOKS[id];
|
|
476
|
-
script += 'jq \'.hooks.PreToolUse += [{"matcher":"' + h.matcher + '","hooks":[{"type":"command","command":"\'$HOOK_DIR\'/' + id + '.sh"}]}]\' "$SETTINGS" > /tmp/cc-settings-tmp.json && mv /tmp/cc-settings-tmp.json "$SETTINGS"\n';
|
|
477
|
-
}
|
|
478
|
-
script += '\n';
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
if (post.length > 0) {
|
|
482
|
-
script += '# Add PostToolUse hooks\n';
|
|
483
|
-
for (const id of post) {
|
|
484
|
-
const h = HOOKS[id];
|
|
485
|
-
script += 'jq \'.hooks.PostToolUse += [{"matcher":"' + h.matcher + '","hooks":[{"type":"command","command":"\'$HOOK_DIR\'/' + id + '.sh"}]}]\' "$SETTINGS" > /tmp/cc-settings-tmp.json && mv /tmp/cc-settings-tmp.json "$SETTINGS"\n';
|
|
486
|
-
}
|
|
487
|
-
script += '\n';
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (stop.length > 0) {
|
|
491
|
-
script += '# Add Stop hooks\n';
|
|
492
|
-
for (const id of stop) {
|
|
493
|
-
script += 'jq \'.hooks.Stop += [{"matcher":"","hooks":[{"type":"command","command":"\'$HOOK_DIR\'/' + id + '.sh"}]}]\' "$SETTINGS" > /tmp/cc-settings-tmp.json && mv /tmp/cc-settings-tmp.json "$SETTINGS"\n';
|
|
494
|
-
}
|
|
495
|
-
script += '\n';
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
script += 'echo ""\n';
|
|
499
|
-
script += 'echo "Done. ' + missing.length + ' safety hooks installed."\n';
|
|
500
|
-
script += 'echo "Restart Claude Code to activate."\n';
|
|
501
|
-
|
|
502
|
-
return script;
|
|
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();
|
|
503
253
|
}
|
|
504
|
-
|
|
505
|
-
function
|
|
506
|
-
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
[ -z "$COMMAND" ] && exit 0
|
|
512
|
-
[ "\${CC_ALLOW_DESTRUCTIVE:-0}" = "1" ] && exit 0
|
|
513
|
-
SAFE_DIRS="\${CC_SAFE_DELETE_DIRS:-node_modules:dist:build:.cache:__pycache__:coverage:.next}"
|
|
514
|
-
# rm -rf on sensitive paths
|
|
515
|
-
if echo "$COMMAND" | grep -qE 'rm\\s+(-[rf]+\\s+)*(\\/$|\\/\\s|\\/home|\\/etc|\\/usr|~\\/|~\\s*$|\\.\\.\\/|\\.\\s*$)'; then
|
|
516
|
-
SAFE=0
|
|
517
|
-
IFS=':' read -ra D <<< "$SAFE_DIRS"
|
|
518
|
-
for d in "\${D[@]}"; do echo "$COMMAND" | grep -qE "rm\\s+.*\${d}\\s*$" && SAFE=1 && break; done
|
|
519
|
-
[ "$SAFE" = 0 ] && echo "BLOCKED: rm on sensitive path. Command: $COMMAND" >&2 && exit 2
|
|
520
|
-
fi
|
|
521
|
-
# git reset --hard
|
|
522
|
-
echo "$COMMAND" | grep -qE '(^|;|&&)\\s*git\\s+reset\\s+--hard' && echo "BLOCKED: git reset --hard" >&2 && exit 2
|
|
523
|
-
# git clean
|
|
524
|
-
echo "$COMMAND" | grep -qE '(^|;|&&)\\s*git\\s+clean\\s+-[a-z]*[fd]' && echo "BLOCKED: git clean" >&2 && exit 2
|
|
525
|
-
# PowerShell destructive
|
|
526
|
-
echo "$COMMAND" | grep -qiE 'Remove-Item.*-Recurse.*-Force|rd\\s+/s\\s+/q' && echo "BLOCKED: Destructive PowerShell command" >&2 && exit 2
|
|
527
|
-
# git checkout --force
|
|
528
|
-
echo "$COMMAND" | grep -qE '(^|;|&&)\\s*git\\s+(checkout|switch)\\s+.*(--force|-f\\b)' && echo "BLOCKED: git checkout --force" >&2 && exit 2
|
|
529
|
-
exit 0`,
|
|
530
|
-
|
|
531
|
-
'branch-guard': `#!/bin/bash
|
|
532
|
-
INPUT=$(cat)
|
|
533
|
-
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
534
|
-
[ -z "$COMMAND" ] && exit 0
|
|
535
|
-
echo "$COMMAND" | grep -qE '^\\s*git\\s+push' || exit 0
|
|
536
|
-
# Force push
|
|
537
|
-
if [ "\${CC_ALLOW_FORCE_PUSH:-0}" != "1" ]; then
|
|
538
|
-
echo "$COMMAND" | grep -qE 'git\\s+push\\s+.*(-f\\b|--force\\b|--force-with-lease)' && echo "BLOCKED: Force push" >&2 && exit 2
|
|
539
|
-
fi
|
|
540
|
-
# Protected branches
|
|
541
|
-
PROTECTED="\${CC_PROTECT_BRANCHES:-main:master}"
|
|
542
|
-
IFS=':' read -ra B <<< "$PROTECTED"
|
|
543
|
-
for b in "\${B[@]}"; do
|
|
544
|
-
echo "$COMMAND" | grep -qwE "origin\\s+\${b}|\${b}\\s|\${b}$" && echo "BLOCKED: Push to protected branch $b" >&2 && exit 2
|
|
545
|
-
done
|
|
546
|
-
exit 0`,
|
|
547
|
-
|
|
548
|
-
'secret-guard': `#!/bin/bash
|
|
549
|
-
INPUT=$(cat)
|
|
550
|
-
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
551
|
-
[ -z "$COMMAND" ] && exit 0
|
|
552
|
-
echo "$COMMAND" | grep -qE '^\\s*git\\s+add' || exit 0
|
|
553
|
-
# Direct .env staging
|
|
554
|
-
echo "$COMMAND" | grep -qiE 'git\\s+add\\s+.*\\.env(\\s|$|\\.)' && echo "BLOCKED: .env file staging" >&2 && exit 2
|
|
555
|
-
# Credential files
|
|
556
|
-
echo "$COMMAND" | grep -qiE 'git\\s+add\\s+.*(credentials|\\.pem|\\.key|\\.p12|id_rsa|id_ed25519)' && echo "BLOCKED: Credential file staging" >&2 && exit 2
|
|
557
|
-
# git add . with .env present
|
|
558
|
-
if echo "$COMMAND" | grep -qE 'git\\s+add\\s+(-A|--all|\\.)(\\s|$)'; then
|
|
559
|
-
[ -f ".env" ] || [ -f ".env.local" ] && echo "BLOCKED: git add . with .env present" >&2 && exit 2
|
|
560
|
-
fi
|
|
561
|
-
exit 0`,
|
|
562
|
-
|
|
563
|
-
'comment-strip': `#!/bin/bash
|
|
564
|
-
INPUT=$(cat)
|
|
565
|
-
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
566
|
-
[ -z "$COMMAND" ] && exit 0
|
|
567
|
-
CLEAN=$(echo "$COMMAND" | sed '/^[[:space:]]*#/d; /^[[:space:]]*$/d')
|
|
568
|
-
[ "$CLEAN" = "$COMMAND" ] && exit 0
|
|
569
|
-
[ -z "$CLEAN" ] && exit 0
|
|
570
|
-
jq -n --arg cmd "$CLEAN" '{"hookSpecificOutput":{"hookEventName":"PreToolUse","updatedInput":{"command":$cmd}}}'`,
|
|
571
|
-
|
|
572
|
-
'cd-git-allow': `#!/bin/bash
|
|
573
|
-
INPUT=$(cat)
|
|
574
|
-
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
575
|
-
[ -z "$COMMAND" ] && exit 0
|
|
576
|
-
echo "$COMMAND" | grep -qE '^\\s*cd\\s+.*&&\\s*git\\s' || exit 0
|
|
577
|
-
GIT_CMD=$(echo "$COMMAND" | grep -oP '&&\\s*git\\s+\\K\\S+')
|
|
578
|
-
for safe in log diff status branch show rev-parse tag remote; do
|
|
579
|
-
[ "$GIT_CMD" = "$safe" ] && jq -n '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"cd+git auto-approved"}}' && exit 0
|
|
580
|
-
done
|
|
581
|
-
exit 0`,
|
|
582
|
-
|
|
583
|
-
'syntax-check': `#!/bin/bash
|
|
584
|
-
INPUT=$(cat)
|
|
585
|
-
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
586
|
-
[ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ] && exit 0
|
|
587
|
-
EXT="\${FILE_PATH##*.}"
|
|
588
|
-
case "$EXT" in
|
|
589
|
-
py) python3 -m py_compile "$FILE_PATH" 2>&1 || echo "SYNTAX ERROR (Python): $FILE_PATH" >&2 ;;
|
|
590
|
-
sh|bash) bash -n "$FILE_PATH" 2>&1 || echo "SYNTAX ERROR (Shell): $FILE_PATH" >&2 ;;
|
|
591
|
-
json) command -v jq &>/dev/null && { jq empty "$FILE_PATH" 2>&1 || echo "SYNTAX ERROR (JSON): $FILE_PATH" >&2; } ;;
|
|
592
|
-
js) command -v node &>/dev/null && { node --check "$FILE_PATH" 2>&1 || echo "SYNTAX ERROR (JS): $FILE_PATH" >&2; } ;;
|
|
593
|
-
esac
|
|
594
|
-
exit 0`,
|
|
595
|
-
|
|
596
|
-
'context-monitor': `#!/bin/bash
|
|
597
|
-
COUNTER_FILE="/tmp/cc-context-monitor-count"
|
|
598
|
-
COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo 0)
|
|
599
|
-
COUNT=$((COUNT + 1))
|
|
600
|
-
echo "$COUNT" > "$COUNTER_FILE"
|
|
601
|
-
[ $((COUNT % 5)) -ne 0 ] && exit 0
|
|
602
|
-
# Estimate context usage from tool call count (~180 calls = full context)
|
|
603
|
-
PCT=$(( 100 - (COUNT * 100 / 180) ))
|
|
604
|
-
[ "$PCT" -lt 0 ] && PCT=0
|
|
605
|
-
if [ "$PCT" -le 15 ]; then
|
|
606
|
-
echo "EMERGENCY: Context ~\${PCT}% remaining. Run /compact NOW." >&2
|
|
607
|
-
elif [ "$PCT" -le 25 ]; then
|
|
608
|
-
echo "WARNING: Context ~\${PCT}% remaining. Finish current task." >&2
|
|
609
|
-
elif [ "$PCT" -le 40 ]; then
|
|
610
|
-
echo "CAUTION: Context ~\${PCT}% remaining." >&2
|
|
611
|
-
fi
|
|
612
|
-
exit 0`,
|
|
613
|
-
|
|
614
|
-
'api-error-alert': `#!/bin/bash
|
|
615
|
-
INPUT=$(cat)
|
|
616
|
-
REASON=$(echo "$INPUT" | jq -r '.stop_reason // "unknown"' 2>/dev/null)
|
|
617
|
-
[ "$REASON" = "user" ] || [ "$REASON" = "normal" ] || [ -z "$REASON" ] && exit 0
|
|
618
|
-
LOG="\${CC_ERROR_ALERT_LOG:-$HOME/.claude/session-errors.log}"
|
|
619
|
-
mkdir -p "$(dirname "$LOG")" 2>/dev/null
|
|
620
|
-
echo "[$(date -Iseconds)] Session stopped: reason=$REASON" >> "$LOG"
|
|
621
|
-
exit 0`
|
|
622
|
-
};
|
|
623
|
-
|
|
624
|
-
return scripts[id] || '#!/bin/bash\nexit 0';
|
|
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('');
|
|
625
261
|
}
|
|
626
262
|
|
|
627
|
-
function
|
|
628
|
-
const action = document.getElementById('hb-action').value;
|
|
629
|
-
const pattern = document.getElementById('hb-pattern').value.trim();
|
|
630
|
-
const message = document.getElementById('hb-message').value.trim() || 'Blocked by hook';
|
|
631
|
-
const el = document.getElementById('hb-result');
|
|
632
|
-
|
|
633
|
-
if (!pattern) { el.innerHTML = '<p style="color:#f85149">Enter a command pattern.</p>'; return; }
|
|
263
|
+
function esc(s){return(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
|
634
264
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
script += 'COMMAND=$(echo "$INPUT" | jq -r \'.tool_input.command // empty\' 2>/dev/null)\n';
|
|
638
|
-
script += '[ -z "$COMMAND" ] && exit 0\n\n';
|
|
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{}}})();
|
|
639
267
|
|
|
640
|
-
|
|
641
|
-
script += 'if echo "$COMMAND" | grep -qE \'' + pattern.replace(/'/g, "'\\''") + '\'; then\n';
|
|
642
|
-
script += ' echo "BLOCKED: ' + message.replace(/"/g, '\\"') + '" >&2\n';
|
|
643
|
-
script += ' echo "Command: $COMMAND" >&2\n';
|
|
644
|
-
script += ' exit 2\n';
|
|
645
|
-
script += 'fi\n';
|
|
646
|
-
} else if (action === 'warn') {
|
|
647
|
-
script += 'if echo "$COMMAND" | grep -qE \'' + pattern.replace(/'/g, "'\\''") + '\'; then\n';
|
|
648
|
-
script += ' echo "WARNING: ' + message.replace(/"/g, '\\"') + '" >&2\n';
|
|
649
|
-
script += ' echo "Command: $COMMAND" >&2\n';
|
|
650
|
-
script += 'fi\n';
|
|
651
|
-
} else if (action === 'approve') {
|
|
652
|
-
script += 'if echo "$COMMAND" | grep -qE \'' + pattern.replace(/'/g, "'\\''") + '\'; then\n';
|
|
653
|
-
script += ' jq -n \'{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"' + message.replace(/"/g, '\\"') + '"}}\'\n';
|
|
654
|
-
script += 'fi\n';
|
|
655
|
-
}
|
|
656
|
-
script += 'exit 0';
|
|
657
|
-
|
|
658
|
-
const settings = {
|
|
659
|
-
hooks: { PreToolUse: [{ matcher: 'Bash', hooks: [{ type: 'command', command: '~/.claude/hooks/custom-hook.sh' }] }] }
|
|
660
|
-
};
|
|
661
|
-
|
|
662
|
-
el.innerHTML = '<h3>Hook Script</h3>' +
|
|
663
|
-
'<div class="code-block"><button class="copy-btn" onclick="copyText(\'hb-script\')">Copy</button><pre id="hb-script">' + escHtml(script) + '</pre></div>' +
|
|
664
|
-
'<h3>settings.json entry</h3>' +
|
|
665
|
-
'<div class="code-block"><button class="copy-btn" onclick="copyText(\'hb-settings\')">Copy</button><pre id="hb-settings">' + escHtml(JSON.stringify(settings, null, 2)) + '</pre></div>' +
|
|
666
|
-
'<p style="color:#8b949e;font-size:.8rem;margin-top:.5rem">Save the script as <code>~/.claude/hooks/custom-hook.sh</code>, run <code>chmod +x</code>, and merge the settings entry.</p>';
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// Auto-load from URL parameter: ?config=base64encodedJSON
|
|
670
|
-
(function() {
|
|
671
|
-
const params = new URLSearchParams(window.location.search);
|
|
672
|
-
const config = params.get('config');
|
|
673
|
-
if (config) {
|
|
674
|
-
try {
|
|
675
|
-
const json = atob(config);
|
|
676
|
-
document.getElementById('settings').value = json;
|
|
677
|
-
runAudit();
|
|
678
|
-
} catch(e) {
|
|
679
|
-
console.error('Invalid config parameter');
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
})();
|
|
268
|
+
initCookbook();
|
|
683
269
|
</script>
|
|
684
270
|
</body>
|
|
685
271
|
</html>
|