cc-safe-setup 7.8.0 → 7.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/builder.html +283 -0
- package/docs/faq.html +244 -0
- package/examples/auto-stash-before-pull.sh +27 -0
- package/examples/compact-reminder.sh +36 -0
- package/package.json +1 -1
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,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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.9.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": {
|