cc-safe-setup 7.8.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +18 -0
- package/README.md +1 -1
- package/docs/builder.html +283 -0
- package/docs/faq.html +244 -0
- package/examples/auto-stash-before-pull.sh +27 -0
- package/examples/commit-scope-guard.sh +32 -0
- package/examples/compact-reminder.sh +36 -0
- package/examples/worktree-guard.sh +28 -0
- package/index.mjs +207 -0
- package/package.json +1 -1
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Project Rules
|
|
2
|
+
|
|
3
|
+
## Safety
|
|
4
|
+
- Do not push to main/master directly
|
|
5
|
+
- Do not force-push
|
|
6
|
+
- Do not delete files outside this project
|
|
7
|
+
- Do not commit .env or credential files
|
|
8
|
+
- Run tests before committing
|
|
9
|
+
|
|
10
|
+
## Code Style
|
|
11
|
+
- Follow existing conventions
|
|
12
|
+
- Keep functions small and focused
|
|
13
|
+
- Add comments only when the logic isn't obvious
|
|
14
|
+
|
|
15
|
+
## Git
|
|
16
|
+
- Use descriptive commit messages
|
|
17
|
+
- One logical change per commit
|
|
18
|
+
- Create feature branches for new work
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**One command to make Claude Code safe for autonomous operation.** [日本語](docs/README.ja.md)
|
|
8
8
|
|
|
9
|
-
8 built-in +
|
|
9
|
+
8 built-in + 77 examples = **85 hooks**. 28 CLI commands. 423 tests. [Web Tool](https://yurukusa.github.io/cc-safe-setup/) · [Cheat Sheet](https://yurukusa.github.io/cc-safe-setup/hooks-cheatsheet.html) · [Builder](https://yurukusa.github.io/cc-safe-setup/builder.html) · [FAQ](https://yurukusa.github.io/cc-safe-setup/faq.html) · [Playground](https://yurukusa.github.io/cc-hook-registry/playground.html)
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npx cc-safe-setup
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Hook Builder — Claude Code</title>
|
|
7
|
+
<meta name="description" content="Generate Claude Code hooks from plain English. No coding required.">
|
|
8
|
+
<style>
|
|
9
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
10
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0d1117;color:#c9d1d9;min-height:100vh;padding:1.5rem}
|
|
11
|
+
.c{max-width:800px;margin:0 auto}
|
|
12
|
+
h1{color:#f0f6fc;font-size:1.4rem;margin-bottom:.3rem}
|
|
13
|
+
.sub{color:#8b949e;font-size:.85rem;margin-bottom:1.2rem}
|
|
14
|
+
a{color:#58a6ff;text-decoration:none}
|
|
15
|
+
input,select{width:100%;padding:.6rem;background:#161b22;border:1px solid #30363d;border-radius:6px;color:#c9d1d9;font-size:.9rem;margin-bottom:.6rem}
|
|
16
|
+
input:focus,select:focus{outline:none;border-color:#58a6ff}
|
|
17
|
+
input::placeholder{color:#484f58}
|
|
18
|
+
label{display:block;color:#8b949e;font-size:.78rem;margin-bottom:.2rem}
|
|
19
|
+
pre{background:#161b22;border:1px solid #30363d;border-radius:6px;padding:.8rem;font-size:.8rem;color:#e6edf3;overflow-x:auto;white-space:pre-wrap;position:relative;margin:.5rem 0}
|
|
20
|
+
.copy-btn{position:absolute;top:.4rem;right:.4rem;background:#21262d;border:1px solid #30363d;color:#8b949e;padding:.2rem .5rem;border-radius:4px;cursor:pointer;font-size:.7rem}
|
|
21
|
+
.copy-btn:hover{color:#f0f6fc;border-color:#58a6ff}
|
|
22
|
+
.btn{background:#238636;color:#fff;border:none;padding:.6rem 1.2rem;border-radius:6px;cursor:pointer;font-size:.9rem;width:100%;margin:.5rem 0}
|
|
23
|
+
.btn:hover{background:#2ea043}
|
|
24
|
+
.templates{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:.4rem;margin:.8rem 0}
|
|
25
|
+
.tmpl{background:#161b22;border:1px solid #30363d;border-radius:6px;padding:.5rem;cursor:pointer;font-size:.78rem;color:#8b949e;transition:border-color .15s}
|
|
26
|
+
.tmpl:hover{border-color:#58a6ff;color:#c9d1d9}
|
|
27
|
+
.tmpl-title{font-weight:600;color:#c9d1d9;font-size:.82rem}
|
|
28
|
+
.section{margin:1.2rem 0}
|
|
29
|
+
.footer{text-align:center;color:#484f58;font-size:.7rem;margin-top:2rem}
|
|
30
|
+
.note{background:#161b22;border-left:3px solid #58a6ff;padding:.5rem .8rem;font-size:.78rem;color:#8b949e;margin:.5rem 0}
|
|
31
|
+
.hidden{display:none}
|
|
32
|
+
</style>
|
|
33
|
+
</head>
|
|
34
|
+
<body>
|
|
35
|
+
<div class="c">
|
|
36
|
+
|
|
37
|
+
<h1>Hook Builder</h1>
|
|
38
|
+
<p class="sub">Describe what you want in plain English. Get a working hook.</p>
|
|
39
|
+
|
|
40
|
+
<div class="section">
|
|
41
|
+
<label>What should the hook do?</label>
|
|
42
|
+
<input id="desc" placeholder="e.g., Block npm publish without running tests first" oninput="clearOutput()">
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="section">
|
|
46
|
+
<label>Trigger event</label>
|
|
47
|
+
<select id="trigger">
|
|
48
|
+
<option value="PreToolUse">PreToolUse (before tool runs — block/approve)</option>
|
|
49
|
+
<option value="PostToolUse">PostToolUse (after tool runs — check results)</option>
|
|
50
|
+
<option value="Stop">Stop (when Claude finishes — notify/cleanup)</option>
|
|
51
|
+
</select>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div class="section">
|
|
55
|
+
<label>Tool matcher (regex)</label>
|
|
56
|
+
<input id="matcher" placeholder='e.g., Bash, Edit|Write, or empty for all' value="Bash">
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<button class="btn" onclick="generate()">Generate Hook</button>
|
|
60
|
+
|
|
61
|
+
<div id="output" class="hidden">
|
|
62
|
+
<h2 style="color:#f0f6fc;font-size:1rem;margin:1rem 0 .5rem">Generated Hook</h2>
|
|
63
|
+
<pre id="hook-code"><code></code><button class="copy-btn" onclick="copyCode()">Copy</button></pre>
|
|
64
|
+
|
|
65
|
+
<h2 style="color:#f0f6fc;font-size:1rem;margin:1rem 0 .5rem">settings.json entry</h2>
|
|
66
|
+
<pre id="settings-code"><code></code><button class="copy-btn" onclick="copySettings()">Copy Settings</button></pre>
|
|
67
|
+
|
|
68
|
+
<div class="note">
|
|
69
|
+
<strong>Install:</strong> Save the hook to <code>~/.claude/hooks/your-hook.sh</code>, run <code>chmod +x</code>, and add the settings.json entry.
|
|
70
|
+
<br>Or use the CLI: <code id="cli-cmd"></code>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div class="section">
|
|
75
|
+
<h2 style="color:#8b949e;font-size:.9rem;margin-bottom:.5rem">Templates</h2>
|
|
76
|
+
<div class="templates" id="templates"></div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div class="footer">
|
|
80
|
+
<a href="hooks-cheatsheet.html">Cheat Sheet</a> ·
|
|
81
|
+
<a href="https://yurukusa.github.io/cc-hook-registry/playground.html">Playground</a> ·
|
|
82
|
+
<a href="https://github.com/yurukusa/cc-safe-setup">GitHub</a>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<script>
|
|
87
|
+
const TEMPLATES = [
|
|
88
|
+
{ title: 'Block rm -rf /', desc: 'Block rm -rf on root directory', trigger: 'PreToolUse', matcher: 'Bash', pattern: 'rm\\s+.*-rf\\s+/', action: 'block', msg: 'rm -rf on root directory' },
|
|
89
|
+
{ title: 'Block force push', desc: 'Block git push --force', trigger: 'PreToolUse', matcher: 'Bash', pattern: 'git\\s+push\\s+.*--force', action: 'block', msg: 'Force push' },
|
|
90
|
+
{ title: 'Block .env commit', desc: 'Block git add .env files', trigger: 'PreToolUse', matcher: 'Bash', pattern: 'git\\s+add\\s+.*\\.env', action: 'block', msg: 'Adding .env to git' },
|
|
91
|
+
{ title: 'Block sudo', desc: 'Block all sudo commands', trigger: 'PreToolUse', matcher: 'Bash', pattern: '^\\s*sudo\\b', action: 'block', msg: 'sudo command' },
|
|
92
|
+
{ title: 'Block DB drop', desc: 'Block DROP DATABASE/TABLE', trigger: 'PreToolUse', matcher: 'Bash', pattern: 'DROP\\s+(DATABASE|TABLE)', action: 'block', msg: 'Database drop' },
|
|
93
|
+
{ title: 'Auto-approve tests', desc: 'Auto-approve npm test, pytest, cargo test', trigger: 'PreToolUse', matcher: 'Bash', pattern: '^\\s*(npm\\s+test|pytest|cargo\\s+test)', action: 'approve', msg: 'Safe test command' },
|
|
94
|
+
{ title: 'Warn large output', desc: 'Warn when output exceeds 50KB', trigger: 'PostToolUse', matcher: '', pattern: null, action: 'warn-output', msg: 'Large output consuming context' },
|
|
95
|
+
{ title: 'Session end notify', desc: 'Desktop notification on session end', trigger: 'Stop', matcher: '', pattern: null, action: 'notify', msg: 'Session completed' },
|
|
96
|
+
{ title: 'Block npm -g', desc: 'Block global npm install', trigger: 'PreToolUse', matcher: 'Bash', pattern: 'npm\\s+install\\s+(-g|--global)', action: 'block', msg: 'Global npm install' },
|
|
97
|
+
{ title: 'Block deploy Friday', desc: 'Block deploy commands on Fridays', trigger: 'PreToolUse', matcher: 'Bash', pattern: null, action: 'friday', msg: 'No deploys on Friday' },
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
document.getElementById('templates').innerHTML = TEMPLATES.map((t,i) => `
|
|
101
|
+
<div class="tmpl" onclick="useTemplate(${i})">
|
|
102
|
+
<div class="tmpl-title">${t.title}</div>
|
|
103
|
+
<div>${t.desc}</div>
|
|
104
|
+
</div>
|
|
105
|
+
`).join('');
|
|
106
|
+
|
|
107
|
+
function useTemplate(i) {
|
|
108
|
+
const t = TEMPLATES[i];
|
|
109
|
+
document.getElementById('desc').value = t.desc;
|
|
110
|
+
document.getElementById('trigger').value = t.trigger;
|
|
111
|
+
document.getElementById('matcher').value = t.matcher;
|
|
112
|
+
generate(t);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function clearOutput() {
|
|
116
|
+
document.getElementById('output').classList.add('hidden');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function generate(template) {
|
|
120
|
+
const desc = document.getElementById('desc').value.trim();
|
|
121
|
+
const trigger = document.getElementById('trigger').value;
|
|
122
|
+
const matcher = document.getElementById('matcher').value.trim();
|
|
123
|
+
|
|
124
|
+
if (!desc && !template) return;
|
|
125
|
+
|
|
126
|
+
let hookCode, hookName;
|
|
127
|
+
|
|
128
|
+
if (template) {
|
|
129
|
+
hookName = template.title.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
130
|
+
hookCode = generateFromTemplate(template);
|
|
131
|
+
} else {
|
|
132
|
+
hookName = desc.toLowerCase().replace(/[^a-z0-9]+/g, '-').substring(0, 30);
|
|
133
|
+
hookCode = generateFromDesc(desc, trigger, matcher);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const settingsCode = JSON.stringify({
|
|
137
|
+
hooks: {
|
|
138
|
+
[trigger]: [{
|
|
139
|
+
matcher: matcher,
|
|
140
|
+
hooks: [{ type: "command", command: `bash ~/.claude/hooks/${hookName}.sh` }]
|
|
141
|
+
}]
|
|
142
|
+
}
|
|
143
|
+
}, null, 2);
|
|
144
|
+
|
|
145
|
+
document.getElementById('hook-code').querySelector('code').textContent = hookCode;
|
|
146
|
+
document.getElementById('settings-code').querySelector('code').textContent = settingsCode;
|
|
147
|
+
document.getElementById('cli-cmd').textContent = `npx cc-safe-setup --create "${desc}"`;
|
|
148
|
+
document.getElementById('output').classList.remove('hidden');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function generateFromTemplate(t) {
|
|
152
|
+
if (t.action === 'block') {
|
|
153
|
+
return `#!/bin/bash
|
|
154
|
+
# ${t.desc}
|
|
155
|
+
# TRIGGER: ${t.trigger} MATCHER: "${t.matcher}"
|
|
156
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
157
|
+
[ -z "$COMMAND" ] && exit 0
|
|
158
|
+
if echo "$COMMAND" | grep -qE '${t.pattern}'; then
|
|
159
|
+
echo "BLOCKED: ${t.msg}" >&2
|
|
160
|
+
exit 2
|
|
161
|
+
fi
|
|
162
|
+
exit 0`;
|
|
163
|
+
}
|
|
164
|
+
if (t.action === 'approve') {
|
|
165
|
+
return `#!/bin/bash
|
|
166
|
+
# ${t.desc}
|
|
167
|
+
# TRIGGER: ${t.trigger} MATCHER: "${t.matcher}"
|
|
168
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
169
|
+
[ -z "$COMMAND" ] && exit 0
|
|
170
|
+
if echo "$COMMAND" | grep -qE '${t.pattern}'; then
|
|
171
|
+
echo '{"decision":"approve","reason":"${t.msg}"}'
|
|
172
|
+
fi
|
|
173
|
+
exit 0`;
|
|
174
|
+
}
|
|
175
|
+
if (t.action === 'warn-output') {
|
|
176
|
+
return `#!/bin/bash
|
|
177
|
+
# ${t.desc}
|
|
178
|
+
# TRIGGER: PostToolUse MATCHER: ""
|
|
179
|
+
OUTPUT=$(cat | jq -r '.tool_result // empty' 2>/dev/null)
|
|
180
|
+
LEN=\${#OUTPUT}
|
|
181
|
+
if [ "$LEN" -gt 50000 ]; then
|
|
182
|
+
echo "WARNING: Output is \${LEN} chars — consuming context fast" >&2
|
|
183
|
+
fi
|
|
184
|
+
exit 0`;
|
|
185
|
+
}
|
|
186
|
+
if (t.action === 'notify') {
|
|
187
|
+
return `#!/bin/bash
|
|
188
|
+
# ${t.desc}
|
|
189
|
+
# TRIGGER: Stop MATCHER: ""
|
|
190
|
+
notify-send "Claude Code" "Session completed" 2>/dev/null || \\
|
|
191
|
+
osascript -e 'display notification "Session completed"' 2>/dev/null
|
|
192
|
+
exit 0`;
|
|
193
|
+
}
|
|
194
|
+
if (t.action === 'friday') {
|
|
195
|
+
return `#!/bin/bash
|
|
196
|
+
# ${t.desc}
|
|
197
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
198
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
199
|
+
[ -z "$COMMAND" ] && exit 0
|
|
200
|
+
if [ "$(date +%u)" = "5" ]; then
|
|
201
|
+
if echo "$COMMAND" | grep -qE '(deploy|publish|release)'; then
|
|
202
|
+
echo "BLOCKED: No deploys on Friday" >&2
|
|
203
|
+
exit 2
|
|
204
|
+
fi
|
|
205
|
+
fi
|
|
206
|
+
exit 0`;
|
|
207
|
+
}
|
|
208
|
+
return '#!/bin/bash\n# Custom hook\nexit 0';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function generateFromDesc(desc, trigger, matcher) {
|
|
212
|
+
const d = desc.toLowerCase();
|
|
213
|
+
let pattern = '', action = 'block', msg = desc;
|
|
214
|
+
|
|
215
|
+
if (d.includes('block') || d.includes('prevent') || d.includes('stop')) {
|
|
216
|
+
action = 'block';
|
|
217
|
+
if (d.includes('rm') || d.includes('delete')) pattern = 'rm\\s+.*-rf';
|
|
218
|
+
else if (d.includes('push') && d.includes('main')) pattern = 'git\\s+push\\s+.*\\b(main|master)\\b';
|
|
219
|
+
else if (d.includes('force')) pattern = '--force';
|
|
220
|
+
else if (d.includes('sudo')) pattern = '^\\s*sudo\\b';
|
|
221
|
+
else if (d.includes('drop')) pattern = 'DROP\\s+(DATABASE|TABLE)';
|
|
222
|
+
else if (d.includes('publish')) pattern = 'npm\\s+publish';
|
|
223
|
+
else if (d.includes('deploy')) pattern = '(deploy|vercel|netlify|firebase)';
|
|
224
|
+
else pattern = d.split(' ').pop();
|
|
225
|
+
} else if (d.includes('approve') || d.includes('allow') || d.includes('auto')) {
|
|
226
|
+
action = 'approve';
|
|
227
|
+
if (d.includes('test')) pattern = '(npm\\s+test|pytest|cargo\\s+test)';
|
|
228
|
+
else if (d.includes('build')) pattern = '(npm\\s+run\\s+build|cargo\\s+build|go\\s+build)';
|
|
229
|
+
else if (d.includes('git')) pattern = 'git\\s+(status|log|diff)';
|
|
230
|
+
else pattern = d.split(' ').pop();
|
|
231
|
+
} else if (d.includes('warn') || d.includes('alert') || d.includes('notify')) {
|
|
232
|
+
action = 'warn';
|
|
233
|
+
pattern = d.split(' ').pop();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (action === 'block') {
|
|
237
|
+
return `#!/bin/bash
|
|
238
|
+
# ${desc}
|
|
239
|
+
# TRIGGER: ${trigger} MATCHER: "${matcher}"
|
|
240
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
241
|
+
[ -z "$COMMAND" ] && exit 0
|
|
242
|
+
if echo "$COMMAND" | grep -qiE '${pattern}'; then
|
|
243
|
+
echo "BLOCKED: ${msg}" >&2
|
|
244
|
+
exit 2
|
|
245
|
+
fi
|
|
246
|
+
exit 0`;
|
|
247
|
+
}
|
|
248
|
+
if (action === 'approve') {
|
|
249
|
+
return `#!/bin/bash
|
|
250
|
+
# ${desc}
|
|
251
|
+
# TRIGGER: ${trigger} MATCHER: "${matcher}"
|
|
252
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
253
|
+
[ -z "$COMMAND" ] && exit 0
|
|
254
|
+
if echo "$COMMAND" | grep -qiE '${pattern}'; then
|
|
255
|
+
echo '{"decision":"approve","reason":"${msg}"}'
|
|
256
|
+
fi
|
|
257
|
+
exit 0`;
|
|
258
|
+
}
|
|
259
|
+
return `#!/bin/bash
|
|
260
|
+
# ${desc}
|
|
261
|
+
# TRIGGER: ${trigger} MATCHER: "${matcher}"
|
|
262
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
263
|
+
[ -z "$COMMAND" ] && exit 0
|
|
264
|
+
if echo "$COMMAND" | grep -qiE '${pattern}'; then
|
|
265
|
+
echo "WARNING: ${msg}" >&2
|
|
266
|
+
fi
|
|
267
|
+
exit 0`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function copyCode() {
|
|
271
|
+
navigator.clipboard.writeText(document.getElementById('hook-code').querySelector('code').textContent);
|
|
272
|
+
document.getElementById('hook-code').querySelector('.copy-btn').textContent = 'Copied!';
|
|
273
|
+
setTimeout(() => document.getElementById('hook-code').querySelector('.copy-btn').textContent = 'Copy', 1500);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function copySettings() {
|
|
277
|
+
navigator.clipboard.writeText(document.getElementById('settings-code').querySelector('code').textContent);
|
|
278
|
+
document.getElementById('settings-code').querySelector('.copy-btn').textContent = 'Copied!';
|
|
279
|
+
setTimeout(() => document.getElementById('settings-code').querySelector('.copy-btn').textContent = 'Copy Settings', 1500);
|
|
280
|
+
}
|
|
281
|
+
</script>
|
|
282
|
+
</body>
|
|
283
|
+
</html>
|
package/docs/faq.html
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
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>Claude Code Hooks FAQ — Every Question Answered</title>
|
|
7
|
+
<meta name="description" content="Answers to every common question about Claude Code hooks. Why hooks? How do they work? What about performance?">
|
|
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;padding:1.5rem;line-height:1.6}
|
|
11
|
+
.c{max-width:800px;margin:0 auto}
|
|
12
|
+
h1{color:#f0f6fc;font-size:1.5rem;margin-bottom:.3rem}
|
|
13
|
+
.sub{color:#8b949e;font-size:.85rem;margin-bottom:1.5rem}
|
|
14
|
+
a{color:#58a6ff;text-decoration:none}
|
|
15
|
+
code{background:#161b22;padding:.15rem .3rem;border-radius:3px;font-size:.85rem}
|
|
16
|
+
pre{background:#161b22;border:1px solid #30363d;border-radius:6px;padding:.7rem;font-size:.8rem;color:#e6edf3;overflow-x:auto;margin:.4rem 0}
|
|
17
|
+
details{background:#161b22;border:1px solid #30363d;border-radius:6px;margin:.5rem 0}
|
|
18
|
+
summary{padding:.7rem .9rem;cursor:pointer;font-weight:600;color:#f0f6fc;font-size:.9rem;list-style:none}
|
|
19
|
+
summary::before{content:"▸ ";color:#58a6ff}
|
|
20
|
+
details[open] summary::before{content:"▾ "}
|
|
21
|
+
details[open] summary{border-bottom:1px solid #21262d}
|
|
22
|
+
.answer{padding:.7rem .9rem;font-size:.85rem}
|
|
23
|
+
.answer p{margin:.4rem 0}
|
|
24
|
+
.cat{color:#58a6ff;font-size:.75rem;font-weight:bold;text-transform:uppercase;margin:1.5rem 0 .5rem;letter-spacing:.05em}
|
|
25
|
+
.footer{text-align:center;color:#484f58;font-size:.7rem;margin-top:2rem;padding-top:1rem;border-top:1px solid #21262d}
|
|
26
|
+
.quick{background:#161b22;border:1px solid #30363d;border-radius:6px;padding:.8rem;margin:.8rem 0;font-size:.82rem}
|
|
27
|
+
</style>
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<div class="c">
|
|
31
|
+
|
|
32
|
+
<h1>Claude Code Hooks FAQ</h1>
|
|
33
|
+
<p class="sub">Every question we've been asked, answered. <a href="https://docs.anthropic.com/en/docs/claude-code/hooks">Official docs</a></p>
|
|
34
|
+
|
|
35
|
+
<div class="quick">
|
|
36
|
+
<strong>TL;DR:</strong> Hooks are shell scripts that run before/after Claude Code tools. <code>exit 2</code> = block. <code>exit 0</code> = allow. Install 8 safety hooks: <code>npx cc-safe-setup</code>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="cat">Basics</div>
|
|
40
|
+
|
|
41
|
+
<details>
|
|
42
|
+
<summary>What are Claude Code hooks?</summary>
|
|
43
|
+
<div class="answer">
|
|
44
|
+
<p>Shell scripts that run at specific points in Claude Code's lifecycle:</p>
|
|
45
|
+
<ul>
|
|
46
|
+
<li><strong>PreToolUse</strong> — before any tool (Bash, Edit, Write) executes</li>
|
|
47
|
+
<li><strong>PostToolUse</strong> — after a tool completes</li>
|
|
48
|
+
<li><strong>Stop</strong> — when Claude finishes responding</li>
|
|
49
|
+
</ul>
|
|
50
|
+
<p>They're configured in <code>~/.claude/settings.json</code> and enforced at the process level — the model cannot bypass them.</p>
|
|
51
|
+
</div>
|
|
52
|
+
</details>
|
|
53
|
+
|
|
54
|
+
<details>
|
|
55
|
+
<summary>How is this different from CLAUDE.md?</summary>
|
|
56
|
+
<div class="answer">
|
|
57
|
+
<p><strong>CLAUDE.md</strong> is a prompt-level instruction. It works well at the start of a session but degrades as context fills up. After 100+ tool calls, Claude may "forget" rules.</p>
|
|
58
|
+
<p><strong>Hooks</strong> run on every single tool call, enforced by the runtime. Even if Claude wants to ignore them, <code>exit 2</code> physically prevents the action.</p>
|
|
59
|
+
<p>Use both: CLAUDE.md for guidelines, hooks for hard constraints.</p>
|
|
60
|
+
</div>
|
|
61
|
+
</details>
|
|
62
|
+
|
|
63
|
+
<details>
|
|
64
|
+
<summary>Do I need to know bash to use hooks?</summary>
|
|
65
|
+
<div class="answer">
|
|
66
|
+
<p>Not really. You can:</p>
|
|
67
|
+
<ol>
|
|
68
|
+
<li>Use <code>npx cc-safe-setup</code> to install pre-built hooks (zero coding)</li>
|
|
69
|
+
<li>Use <code>npx cc-safe-setup --create "your description"</code> to generate hooks from English</li>
|
|
70
|
+
<li>Use the <a href="builder.html">Hook Builder</a> web tool</li>
|
|
71
|
+
<li>Copy-paste from the <a href="hooks-cheatsheet.html">Cheat Sheet</a></li>
|
|
72
|
+
</ol>
|
|
73
|
+
</div>
|
|
74
|
+
</details>
|
|
75
|
+
|
|
76
|
+
<div class="cat">How It Works</div>
|
|
77
|
+
|
|
78
|
+
<details>
|
|
79
|
+
<summary>What does exit code 2 do?</summary>
|
|
80
|
+
<div class="answer">
|
|
81
|
+
<p><code>exit 2</code> blocks the tool call. The model receives a message that the operation was blocked and must try a different approach.</p>
|
|
82
|
+
<p><code>exit 0</code> allows the operation (default).</p>
|
|
83
|
+
<p><code>exit 1</code> is treated as a hook error and is silently ignored — don't use it for blocking.</p>
|
|
84
|
+
</div>
|
|
85
|
+
</details>
|
|
86
|
+
|
|
87
|
+
<details>
|
|
88
|
+
<summary>What JSON does the hook receive on stdin?</summary>
|
|
89
|
+
<div class="answer">
|
|
90
|
+
<p>For <strong>PreToolUse</strong> (Bash):</p>
|
|
91
|
+
<pre><code>{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}</code></pre>
|
|
92
|
+
<p>For <strong>PreToolUse</strong> (Edit):</p>
|
|
93
|
+
<pre><code>{"tool_name":"Edit","tool_input":{"file_path":"app.js","old_string":"...","new_string":"..."}}</code></pre>
|
|
94
|
+
<p>For <strong>PostToolUse</strong>:</p>
|
|
95
|
+
<pre><code>{"tool_name":"Bash","tool_input":{"command":"ls"},"tool_result":"file1.txt\nfile2.txt"}</code></pre>
|
|
96
|
+
<p>Extract with: <code>cat | jq -r '.tool_input.command // empty'</code></p>
|
|
97
|
+
</div>
|
|
98
|
+
</details>
|
|
99
|
+
|
|
100
|
+
<details>
|
|
101
|
+
<summary>Can I auto-approve commands via hooks?</summary>
|
|
102
|
+
<div class="answer">
|
|
103
|
+
<p>Yes. Output JSON to stdout:</p>
|
|
104
|
+
<pre><code>echo '{"decision":"approve","reason":"Safe test command"}'
|
|
105
|
+
exit 0</code></pre>
|
|
106
|
+
<p>This skips the permission prompt for that specific tool call.</p>
|
|
107
|
+
</div>
|
|
108
|
+
</details>
|
|
109
|
+
|
|
110
|
+
<details>
|
|
111
|
+
<summary>What does the "matcher" field do?</summary>
|
|
112
|
+
<div class="answer">
|
|
113
|
+
<p>It's a regex matched against the tool name:</p>
|
|
114
|
+
<ul>
|
|
115
|
+
<li><code>"Bash"</code> — only Bash commands</li>
|
|
116
|
+
<li><code>"Edit|Write"</code> — file modifications</li>
|
|
117
|
+
<li><code>""</code> (empty) — matches ALL tools</li>
|
|
118
|
+
<li><code>"Read"</code> — file reads</li>
|
|
119
|
+
</ul>
|
|
120
|
+
</div>
|
|
121
|
+
</details>
|
|
122
|
+
|
|
123
|
+
<div class="cat">Performance & Safety</div>
|
|
124
|
+
|
|
125
|
+
<details>
|
|
126
|
+
<summary>Do hooks slow down Claude Code?</summary>
|
|
127
|
+
<div class="answer">
|
|
128
|
+
<p>No. Each hook runs in ~5ms (bash + jq). Even with 10 hooks chained, total overhead is <50ms per tool call — imperceptible compared to the API latency.</p>
|
|
129
|
+
<p>You can measure: <code>npx cc-safe-setup --benchmark</code></p>
|
|
130
|
+
</div>
|
|
131
|
+
</details>
|
|
132
|
+
|
|
133
|
+
<details>
|
|
134
|
+
<summary>Can Claude disable or modify hooks?</summary>
|
|
135
|
+
<div class="answer">
|
|
136
|
+
<p>Claude can edit <code>settings.json</code> via the Edit tool. To prevent this:</p>
|
|
137
|
+
<pre><code>npx cc-safe-setup --install-example protect-claudemd</code></pre>
|
|
138
|
+
<p>This hook blocks edits to CLAUDE.md and settings files.</p>
|
|
139
|
+
</div>
|
|
140
|
+
</details>
|
|
141
|
+
|
|
142
|
+
<details>
|
|
143
|
+
<summary>What if my hook has a bug?</summary>
|
|
144
|
+
<div class="answer">
|
|
145
|
+
<p>If a hook exits with code 1 (error), Claude Code treats it as a hook failure and <strong>ignores it</strong> — the tool call proceeds. This is by design: buggy hooks don't block all operations.</p>
|
|
146
|
+
<p>Only <code>exit 2</code> blocks. If your hook accidentally exits 2 for everything, use <code>npx cc-safe-setup --doctor</code> to diagnose.</p>
|
|
147
|
+
</div>
|
|
148
|
+
</details>
|
|
149
|
+
|
|
150
|
+
<details>
|
|
151
|
+
<summary>Can I use Python/TypeScript instead of bash?</summary>
|
|
152
|
+
<div class="answer">
|
|
153
|
+
<p>Yes. The <code>command</code> field can run anything:</p>
|
|
154
|
+
<pre><code>{"type": "command", "command": "python3 ~/.claude/hooks/my-guard.py"}</code></pre>
|
|
155
|
+
<p>cc-safe-setup includes Python examples: <code>examples/python/destructive_guard.py</code></p>
|
|
156
|
+
<p>For TypeScript, see <a href="https://www.npmjs.com/package/@anthropic-ai/claude-code-safety-net">safety-net</a> (1,185★).</p>
|
|
157
|
+
</div>
|
|
158
|
+
</details>
|
|
159
|
+
|
|
160
|
+
<div class="cat">Common Issues</div>
|
|
161
|
+
|
|
162
|
+
<details>
|
|
163
|
+
<summary>My hook doesn't fire — what's wrong?</summary>
|
|
164
|
+
<div class="answer">
|
|
165
|
+
<p>Run diagnostics: <code>npx cc-safe-setup --doctor</code></p>
|
|
166
|
+
<p>Common causes:</p>
|
|
167
|
+
<ol>
|
|
168
|
+
<li><strong>Not executable:</strong> <code>chmod +x ~/.claude/hooks/your-hook.sh</code></li>
|
|
169
|
+
<li><strong>Missing shebang:</strong> First line must be <code>#!/bin/bash</code></li>
|
|
170
|
+
<li><strong>Wrong matcher:</strong> <code>"Bash"</code> won't fire for Edit/Write tools</li>
|
|
171
|
+
<li><strong>settings.json syntax error:</strong> Validate with <code>python3 -c "import json; json.load(open('$HOME/.claude/settings.json'))"</code></li>
|
|
172
|
+
<li><strong>jq not installed:</strong> <code>which jq</code> — install if missing</li>
|
|
173
|
+
</ol>
|
|
174
|
+
<p>Quick fix: <code>npx cc-safe-setup --quickfix</code></p>
|
|
175
|
+
</div>
|
|
176
|
+
</details>
|
|
177
|
+
|
|
178
|
+
<details>
|
|
179
|
+
<summary>My hook blocks everything</summary>
|
|
180
|
+
<div class="answer">
|
|
181
|
+
<p>Your hook is returning <code>exit 2</code> for all inputs. Debug by testing manually:</p>
|
|
182
|
+
<pre><code>echo '{"tool_input":{"command":"ls"}}' | bash ~/.claude/hooks/your-hook.sh
|
|
183
|
+
echo "Exit: $?"</code></pre>
|
|
184
|
+
<p>If it exits 2 for <code>ls</code>, your regex is too broad. Check the <code>grep -qE</code> pattern.</p>
|
|
185
|
+
</div>
|
|
186
|
+
</details>
|
|
187
|
+
|
|
188
|
+
<details>
|
|
189
|
+
<summary>Multiple hooks — does order matter?</summary>
|
|
190
|
+
<div class="answer">
|
|
191
|
+
<p>Hooks in the same group run sequentially. If any hook exits 2, the tool call is blocked — remaining hooks don't run.</p>
|
|
192
|
+
<p><strong>Important:</strong> Each hook consumes stdin. If you chain multiple hooks in the same group, the second hook gets empty stdin. Put each hook in a separate matcher group, or use <code>tee</code>.</p>
|
|
193
|
+
</div>
|
|
194
|
+
</details>
|
|
195
|
+
|
|
196
|
+
<details>
|
|
197
|
+
<summary>Can hooks access the internet?</summary>
|
|
198
|
+
<div class="answer">
|
|
199
|
+
<p>Yes. Hooks are regular shell scripts — they can make HTTP requests, send notifications, write to databases, etc.</p>
|
|
200
|
+
<p>Example: send a Slack notification when a dangerous command is blocked:</p>
|
|
201
|
+
<pre><code>curl -s -X POST "$SLACK_WEBHOOK" -d "{\"text\":\"Blocked: $COMMAND\"}"</code></pre>
|
|
202
|
+
</div>
|
|
203
|
+
</details>
|
|
204
|
+
|
|
205
|
+
<div class="cat">Installation</div>
|
|
206
|
+
|
|
207
|
+
<details>
|
|
208
|
+
<summary>How do I install hooks?</summary>
|
|
209
|
+
<div class="answer">
|
|
210
|
+
<p>Three ways:</p>
|
|
211
|
+
<ol>
|
|
212
|
+
<li><strong>One command:</strong> <code>npx cc-safe-setup</code> (installs 8 safety hooks)</li>
|
|
213
|
+
<li><strong>Individual:</strong> <code>npx cc-safe-setup --install-example block-database-wipe</code></li>
|
|
214
|
+
<li><strong>Manual:</strong> Save script to <code>~/.claude/hooks/</code>, <code>chmod +x</code>, add to <code>settings.json</code></li>
|
|
215
|
+
</ol>
|
|
216
|
+
</div>
|
|
217
|
+
</details>
|
|
218
|
+
|
|
219
|
+
<details>
|
|
220
|
+
<summary>How do I uninstall hooks?</summary>
|
|
221
|
+
<div class="answer">
|
|
222
|
+
<pre><code>npx cc-safe-setup --uninstall</code></pre>
|
|
223
|
+
<p>Or manually: remove the hook entries from <code>~/.claude/settings.json</code> and delete the script files.</p>
|
|
224
|
+
</div>
|
|
225
|
+
</details>
|
|
226
|
+
|
|
227
|
+
<details>
|
|
228
|
+
<summary>Where are hooks stored?</summary>
|
|
229
|
+
<div class="answer">
|
|
230
|
+
<p>Scripts: <code>~/.claude/hooks/</code></p>
|
|
231
|
+
<p>Configuration: <code>~/.claude/settings.json</code> (global) or <code>.claude/settings.json</code> (project)</p>
|
|
232
|
+
<p>Project-level hooks override global hooks for the same matcher.</p>
|
|
233
|
+
</div>
|
|
234
|
+
</details>
|
|
235
|
+
|
|
236
|
+
<div class="footer">
|
|
237
|
+
<a href="hooks-cheatsheet.html">Cheat Sheet</a> ·
|
|
238
|
+
<a href="builder.html">Hook Builder</a> ·
|
|
239
|
+
<a href="https://yurukusa.github.io/cc-hook-registry/playground.html">Playground</a> ·
|
|
240
|
+
<a href="https://github.com/yurukusa/cc-safe-setup">GitHub</a>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</body>
|
|
244
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# auto-stash-before-pull.sh — Suggest stash before git pull/merge
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude runs git pull/merge with uncommitted changes, causing
|
|
7
|
+
# merge conflicts or lost work. This hook warns and suggests
|
|
8
|
+
# git stash before pull/merge/rebase operations.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
11
|
+
# ================================================================
|
|
12
|
+
|
|
13
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
14
|
+
[ -z "$COMMAND" ] && exit 0
|
|
15
|
+
|
|
16
|
+
# Only check pull/merge/rebase
|
|
17
|
+
echo "$COMMAND" | grep -qE '\bgit\s+(pull|merge|rebase)\b' || exit 0
|
|
18
|
+
|
|
19
|
+
# Check for uncommitted changes
|
|
20
|
+
DIRTY=$(git status --porcelain 2>/dev/null)
|
|
21
|
+
if [ -n "$DIRTY" ]; then
|
|
22
|
+
COUNT=$(echo "$DIRTY" | wc -l)
|
|
23
|
+
echo "WARNING: git pull/merge/rebase with $COUNT uncommitted change(s)." >&2
|
|
24
|
+
echo "Consider running: git stash && git pull && git stash pop" >&2
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
exit 0
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# commit-scope-guard.sh — Warn when committing too many files
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude Code can modify dozens of files and commit them all at
|
|
7
|
+
# once, making the commit hard to review and revert. This hook
|
|
8
|
+
# warns when staging more than a configurable number of files.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
11
|
+
#
|
|
12
|
+
# CONFIG:
|
|
13
|
+
# CC_MAX_COMMIT_FILES=15 (warn above 15 files)
|
|
14
|
+
# ================================================================
|
|
15
|
+
|
|
16
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
17
|
+
[ -z "$COMMAND" ] && exit 0
|
|
18
|
+
|
|
19
|
+
echo "$COMMAND" | grep -qE '^\s*git\s+commit' || exit 0
|
|
20
|
+
|
|
21
|
+
MAX="${CC_MAX_COMMIT_FILES:-15}"
|
|
22
|
+
STAGED=$(git diff --cached --name-only 2>/dev/null | wc -l)
|
|
23
|
+
|
|
24
|
+
if [ "$STAGED" -gt "$MAX" ]; then
|
|
25
|
+
echo "WARNING: Committing $STAGED files (threshold: $MAX)." >&2
|
|
26
|
+
echo "Consider splitting into smaller, focused commits." >&2
|
|
27
|
+
echo "Files:" >&2
|
|
28
|
+
git diff --cached --name-only 2>/dev/null | head -10 | sed 's/^/ /' >&2
|
|
29
|
+
[ "$STAGED" -gt 10 ] && echo " ... and $((STAGED-10)) more" >&2
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
exit 0
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# compact-reminder.sh — Remind to /compact when context is low
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# After Claude responds (Stop event), check how many tool calls
|
|
7
|
+
# have been made in the session. If the count exceeds a threshold,
|
|
8
|
+
# suggest running /compact to free up context space.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: Stop MATCHER: ""
|
|
11
|
+
#
|
|
12
|
+
# CONFIG:
|
|
13
|
+
# CC_COMPACT_THRESHOLD=100 (suggest after 100 tool calls)
|
|
14
|
+
# ================================================================
|
|
15
|
+
|
|
16
|
+
THRESHOLD="${CC_COMPACT_THRESHOLD:-100}"
|
|
17
|
+
STATE="/tmp/cc-tool-count-$(echo "$PWD" | md5sum | cut -c1-8)"
|
|
18
|
+
|
|
19
|
+
# Increment counter
|
|
20
|
+
COUNT=1
|
|
21
|
+
[ -f "$STATE" ] && COUNT=$(($(cat "$STATE") + 1))
|
|
22
|
+
echo "$COUNT" > "$STATE"
|
|
23
|
+
|
|
24
|
+
if [ "$COUNT" -eq "$THRESHOLD" ]; then
|
|
25
|
+
echo "" >&2
|
|
26
|
+
echo "NOTE: $COUNT tool calls in this session." >&2
|
|
27
|
+
echo "Consider running /compact to free context space." >&2
|
|
28
|
+
echo "Reset counter: rm $STATE" >&2
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Repeat reminder every 50 calls after threshold
|
|
32
|
+
if [ "$COUNT" -gt "$THRESHOLD" ] && [ $(( (COUNT - THRESHOLD) % 50 )) -eq 0 ]; then
|
|
33
|
+
echo "REMINDER: $COUNT tool calls. /compact recommended." >&2
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
exit 0
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# worktree-guard.sh — Warn when operating in a git worktree
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Git worktrees share the same .git directory. Destructive operations
|
|
7
|
+
# in one worktree (git clean, reset) can affect the main working tree.
|
|
8
|
+
# This hook warns when Claude is operating inside a worktree.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
11
|
+
# ================================================================
|
|
12
|
+
|
|
13
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
14
|
+
[ -z "$COMMAND" ] && exit 0
|
|
15
|
+
|
|
16
|
+
# Only check destructive git commands
|
|
17
|
+
echo "$COMMAND" | grep -qE '\bgit\s+(clean|reset|checkout\s+--|stash\s+drop)' || exit 0
|
|
18
|
+
|
|
19
|
+
# Check if we're in a worktree
|
|
20
|
+
GITDIR=$(git rev-parse --git-dir 2>/dev/null)
|
|
21
|
+
if echo "$GITDIR" | grep -q "worktrees"; then
|
|
22
|
+
MAIN_DIR=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null | sed 's|/.git$||')
|
|
23
|
+
echo "WARNING: You are in a git worktree." >&2
|
|
24
|
+
echo "Main working tree: $MAIN_DIR" >&2
|
|
25
|
+
echo "Destructive git operations may affect the main tree." >&2
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
exit 0
|
package/index.mjs
CHANGED
|
@@ -90,6 +90,7 @@ const MIGRATE = process.argv.includes('--migrate');
|
|
|
90
90
|
const GENERATE_CI = process.argv.includes('--generate-ci');
|
|
91
91
|
const REPORT = process.argv.includes('--report');
|
|
92
92
|
const QUICKFIX = process.argv.includes('--quickfix');
|
|
93
|
+
const SHIELD = process.argv.includes('--shield');
|
|
93
94
|
const COMPARE_IDX = process.argv.findIndex(a => a === '--compare');
|
|
94
95
|
const COMPARE = COMPARE_IDX !== -1 ? { a: process.argv[COMPARE_IDX + 1], b: process.argv[COMPARE_IDX + 2] } : null;
|
|
95
96
|
const CREATE_IDX = process.argv.findIndex(a => a === '--create');
|
|
@@ -125,6 +126,7 @@ if (HELP) {
|
|
|
125
126
|
npx cc-safe-setup --doctor Diagnose why hooks aren't working
|
|
126
127
|
npx cc-safe-setup --watch Live dashboard of blocked commands
|
|
127
128
|
npx cc-safe-setup --create "<desc>" Generate a custom hook from description
|
|
129
|
+
npx cc-safe-setup --shield Maximum safety in one command (fix + scan + install + CLAUDE.md)
|
|
128
130
|
npx cc-safe-setup --quickfix Auto-detect and fix common Claude Code problems
|
|
129
131
|
npx cc-safe-setup --stats Block statistics and patterns report
|
|
130
132
|
npx cc-safe-setup --export Export hooks config for team sharing
|
|
@@ -831,6 +833,210 @@ async function fullSetup() {
|
|
|
831
833
|
console.log();
|
|
832
834
|
}
|
|
833
835
|
|
|
836
|
+
async function shield() {
|
|
837
|
+
const { execSync } = await import('child_process');
|
|
838
|
+
const { readdirSync } = await import('fs');
|
|
839
|
+
console.log();
|
|
840
|
+
console.log(c.bold + ' 🛡️ cc-safe-setup --shield' + c.reset);
|
|
841
|
+
console.log(c.dim + ' Maximum safety in one command' + c.reset);
|
|
842
|
+
console.log();
|
|
843
|
+
|
|
844
|
+
// Step 1: Fix environment issues
|
|
845
|
+
console.log(c.bold + ' Step 1: Fix environment' + c.reset);
|
|
846
|
+
await quickfix();
|
|
847
|
+
|
|
848
|
+
// Step 2: Install core safety hooks
|
|
849
|
+
console.log();
|
|
850
|
+
console.log(c.bold + ' Step 2: Install safety hooks' + c.reset);
|
|
851
|
+
// Run the default install
|
|
852
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
853
|
+
let installed = 0;
|
|
854
|
+
for (const [hookId, hookMeta] of Object.entries(HOOKS)) {
|
|
855
|
+
const hookPath = join(HOOKS_DIR, `${hookId}.sh`);
|
|
856
|
+
if (!existsSync(hookPath)) {
|
|
857
|
+
writeFileSync(hookPath, SCRIPTS[hookId]);
|
|
858
|
+
chmodSync(hookPath, 0o755);
|
|
859
|
+
installed++;
|
|
860
|
+
console.log(c.green + ' +' + c.reset + ` ${hookMeta.name}`);
|
|
861
|
+
} else {
|
|
862
|
+
console.log(c.dim + ' ✓' + c.reset + ` ${hookMeta.name} (already installed)`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Step 3: Detect project stack and install recommended examples
|
|
867
|
+
console.log();
|
|
868
|
+
console.log(c.bold + ' Step 3: Project-aware hooks' + c.reset);
|
|
869
|
+
const cwd = process.cwd();
|
|
870
|
+
const extras = [];
|
|
871
|
+
if (existsSync(join(cwd, 'package.json'))) {
|
|
872
|
+
extras.push('auto-approve-build');
|
|
873
|
+
try {
|
|
874
|
+
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8'));
|
|
875
|
+
if (pkg.dependencies?.prisma || pkg.devDependencies?.prisma) extras.push('block-database-wipe');
|
|
876
|
+
if (pkg.scripts?.deploy) extras.push('deploy-guard');
|
|
877
|
+
} catch {}
|
|
878
|
+
}
|
|
879
|
+
if (existsSync(join(cwd, 'requirements.txt')) || existsSync(join(cwd, 'pyproject.toml'))) extras.push('auto-approve-python');
|
|
880
|
+
if (existsSync(join(cwd, 'Dockerfile'))) extras.push('auto-approve-docker');
|
|
881
|
+
if (existsSync(join(cwd, 'go.mod'))) extras.push('auto-approve-go');
|
|
882
|
+
if (existsSync(join(cwd, 'Cargo.toml'))) extras.push('auto-approve-cargo');
|
|
883
|
+
if (existsSync(join(cwd, 'Makefile'))) extras.push('auto-approve-make');
|
|
884
|
+
if (existsSync(join(cwd, '.env'))) extras.push('env-source-guard');
|
|
885
|
+
|
|
886
|
+
// Always include these for maximum safety
|
|
887
|
+
extras.push('scope-guard', 'no-sudo-guard', 'protect-claudemd');
|
|
888
|
+
|
|
889
|
+
for (const ex of extras) {
|
|
890
|
+
const exPath = join(__dirname, 'examples', `${ex}.sh`);
|
|
891
|
+
const hookPath = join(HOOKS_DIR, `${ex}.sh`);
|
|
892
|
+
if (existsSync(exPath) && !existsSync(hookPath)) {
|
|
893
|
+
copyFileSync(exPath, hookPath);
|
|
894
|
+
chmodSync(hookPath, 0o755);
|
|
895
|
+
console.log(c.green + ' +' + c.reset + ` ${ex}`);
|
|
896
|
+
installed++;
|
|
897
|
+
} else if (existsSync(hookPath)) {
|
|
898
|
+
console.log(c.dim + ' ✓' + c.reset + ` ${ex} (already installed)`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Step 4: Update settings.json
|
|
903
|
+
console.log();
|
|
904
|
+
console.log(c.bold + ' Step 4: Configure settings.json' + c.reset);
|
|
905
|
+
let settings = {};
|
|
906
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
907
|
+
try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
|
|
908
|
+
}
|
|
909
|
+
if (!settings.hooks) settings.hooks = {};
|
|
910
|
+
|
|
911
|
+
// Collect all installed hooks
|
|
912
|
+
const hookFiles = existsSync(HOOKS_DIR)
|
|
913
|
+
? readdirSync(HOOKS_DIR).filter(f => f.endsWith('.sh'))
|
|
914
|
+
: [];
|
|
915
|
+
|
|
916
|
+
// Build hook entries by trigger type
|
|
917
|
+
const preToolHooks = [];
|
|
918
|
+
const postToolHooks = [];
|
|
919
|
+
const stopHooks = [];
|
|
920
|
+
|
|
921
|
+
for (const f of hookFiles) {
|
|
922
|
+
const content = readFileSync(join(HOOKS_DIR, f), 'utf-8');
|
|
923
|
+
const cmd = `bash ${join(HOOKS_DIR, f)}`;
|
|
924
|
+
|
|
925
|
+
// Check if already in settings
|
|
926
|
+
const alreadyConfigured = JSON.stringify(settings.hooks).includes(f);
|
|
927
|
+
if (alreadyConfigured) continue;
|
|
928
|
+
|
|
929
|
+
// Determine trigger from file content
|
|
930
|
+
if (content.includes('TRIGGER: Stop') || f.includes('api-error') || f.includes('revert-helper') || f.includes('session-handoff') || f.includes('compact-reminder') || f.includes('notify') || f.includes('tmp-cleanup')) {
|
|
931
|
+
stopHooks.push({ type: 'command', command: cmd });
|
|
932
|
+
} else if (content.includes('TRIGGER: PostToolUse') || f.includes('syntax-check') || f.includes('context-monitor') || f.includes('output-length') || f.includes('error-memory') || f.includes('cost-tracker')) {
|
|
933
|
+
postToolHooks.push({ type: 'command', command: cmd });
|
|
934
|
+
} else {
|
|
935
|
+
// Default: PreToolUse
|
|
936
|
+
const matcher = (f.includes('edit-guard') || f.includes('protect-dotfiles') || f.includes('overwrite-guard') || f.includes('binary-file') || f.includes('parallel-edit') || f.includes('test-deletion') || f.includes('memory-write'))
|
|
937
|
+
? 'Edit|Write'
|
|
938
|
+
: 'Bash';
|
|
939
|
+
preToolHooks.push({ type: 'command', command: cmd, _matcher: matcher });
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Group PreToolUse hooks by matcher
|
|
944
|
+
if (preToolHooks.length > 0) {
|
|
945
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
946
|
+
const bashHooks = preToolHooks.filter(h => h._matcher === 'Bash').map(({ _matcher, ...h }) => h);
|
|
947
|
+
const editHooks = preToolHooks.filter(h => h._matcher === 'Edit|Write').map(({ _matcher, ...h }) => h);
|
|
948
|
+
if (bashHooks.length > 0) {
|
|
949
|
+
const existing = settings.hooks.PreToolUse.find(e => e.matcher === 'Bash');
|
|
950
|
+
if (existing) {
|
|
951
|
+
const existingCmds = new Set(existing.hooks.map(h => h.command));
|
|
952
|
+
for (const h of bashHooks) {
|
|
953
|
+
if (!existingCmds.has(h.command)) existing.hooks.push(h);
|
|
954
|
+
}
|
|
955
|
+
} else {
|
|
956
|
+
settings.hooks.PreToolUse.push({ matcher: 'Bash', hooks: bashHooks });
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
if (editHooks.length > 0) {
|
|
960
|
+
const existing = settings.hooks.PreToolUse.find(e => e.matcher === 'Edit|Write');
|
|
961
|
+
if (existing) {
|
|
962
|
+
const existingCmds = new Set(existing.hooks.map(h => h.command));
|
|
963
|
+
for (const h of editHooks) {
|
|
964
|
+
if (!existingCmds.has(h.command)) existing.hooks.push(h);
|
|
965
|
+
}
|
|
966
|
+
} else {
|
|
967
|
+
settings.hooks.PreToolUse.push({ matcher: 'Edit|Write', hooks: editHooks });
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (postToolHooks.length > 0) {
|
|
972
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
973
|
+
const existing = settings.hooks.PostToolUse.find(e => e.matcher === '');
|
|
974
|
+
if (existing) {
|
|
975
|
+
const existingCmds = new Set(existing.hooks.map(h => h.command));
|
|
976
|
+
for (const h of postToolHooks) {
|
|
977
|
+
if (!existingCmds.has(h.command)) existing.hooks.push(h);
|
|
978
|
+
}
|
|
979
|
+
} else {
|
|
980
|
+
settings.hooks.PostToolUse.push({ matcher: '', hooks: postToolHooks });
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (stopHooks.length > 0) {
|
|
984
|
+
if (!settings.hooks.Stop) settings.hooks.Stop = [];
|
|
985
|
+
const existing = settings.hooks.Stop.find(e => e.matcher === '');
|
|
986
|
+
if (existing) {
|
|
987
|
+
const existingCmds = new Set(existing.hooks.map(h => h.command));
|
|
988
|
+
for (const h of stopHooks) {
|
|
989
|
+
if (!existingCmds.has(h.command)) existing.hooks.push(h);
|
|
990
|
+
}
|
|
991
|
+
} else {
|
|
992
|
+
settings.hooks.Stop.push({ matcher: '', hooks: stopHooks });
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
997
|
+
console.log(c.green + ' ✓' + c.reset + ' settings.json updated');
|
|
998
|
+
|
|
999
|
+
// Step 5: Generate CLAUDE.md template if none exists
|
|
1000
|
+
console.log();
|
|
1001
|
+
console.log(c.bold + ' Step 5: CLAUDE.md' + c.reset);
|
|
1002
|
+
if (!existsSync(join(cwd, 'CLAUDE.md'))) {
|
|
1003
|
+
const template = `# Project Rules
|
|
1004
|
+
|
|
1005
|
+
## Safety
|
|
1006
|
+
- Do not push to main/master directly
|
|
1007
|
+
- Do not force-push
|
|
1008
|
+
- Do not delete files outside this project
|
|
1009
|
+
- Do not commit .env or credential files
|
|
1010
|
+
- Run tests before committing
|
|
1011
|
+
|
|
1012
|
+
## Code Style
|
|
1013
|
+
- Follow existing conventions
|
|
1014
|
+
- Keep functions small and focused
|
|
1015
|
+
- Add comments only when the logic isn't obvious
|
|
1016
|
+
|
|
1017
|
+
## Git
|
|
1018
|
+
- Use descriptive commit messages
|
|
1019
|
+
- One logical change per commit
|
|
1020
|
+
- Create feature branches for new work
|
|
1021
|
+
`;
|
|
1022
|
+
writeFileSync(join(cwd, 'CLAUDE.md'), template);
|
|
1023
|
+
console.log(c.green + ' +' + c.reset + ' Created CLAUDE.md with safety rules template');
|
|
1024
|
+
} else {
|
|
1025
|
+
console.log(c.dim + ' ✓' + c.reset + ' CLAUDE.md already exists');
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Summary
|
|
1029
|
+
console.log();
|
|
1030
|
+
const totalHooks = hookFiles.length;
|
|
1031
|
+
console.log(c.bold + c.green + ' 🛡️ Shield activated!' + c.reset);
|
|
1032
|
+
console.log(c.dim + ` ${totalHooks} hooks installed and configured.` + c.reset);
|
|
1033
|
+
console.log(c.dim + ' Your Claude Code sessions are now protected.' + c.reset);
|
|
1034
|
+
console.log();
|
|
1035
|
+
console.log(c.dim + ' Verify: npx cc-safe-setup --verify' + c.reset);
|
|
1036
|
+
console.log(c.dim + ' Status: npx cc-safe-setup --status' + c.reset);
|
|
1037
|
+
console.log();
|
|
1038
|
+
}
|
|
1039
|
+
|
|
834
1040
|
async function quickfix() {
|
|
835
1041
|
const { execSync } = await import('child_process');
|
|
836
1042
|
console.log();
|
|
@@ -2836,6 +3042,7 @@ async function main() {
|
|
|
2836
3042
|
if (FULL) return fullSetup();
|
|
2837
3043
|
if (DOCTOR) return doctor();
|
|
2838
3044
|
if (WATCH) return watch();
|
|
3045
|
+
if (SHIELD) return shield();
|
|
2839
3046
|
if (QUICKFIX) return quickfix();
|
|
2840
3047
|
if (REPORT) return report();
|
|
2841
3048
|
if (GENERATE_CI) return generateCI();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"description": "One command to make Claude Code safe. 59 hooks (8 built-in + 51 examples). 26 CLI commands: dashboard, create, audit, lint, diff, migrate, compare, generate-ci. 284 tests.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"bin": {
|