cc-safe-setup 7.4.0 → 7.6.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
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 + 71 examples = **79 hooks**. 28 CLI commands. 405 tests. [Web Tool](https://yurukusa.github.io/cc-safe-setup/) · [Hooks Cheat Sheet](https://yurukusa.github.io/cc-safe-setup/hooks-cheatsheet.html) · [Playground](https://yurukusa.github.io/cc-hook-registry/playground.html) · [Troubleshooting](TROUBLESHOOTING.md)
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npx cc-safe-setup
|
|
@@ -87,7 +87,7 @@ Each hook exists because a real incident happened without it.
|
|
|
87
87
|
| `--scan [--apply]` | Tech stack detection |
|
|
88
88
|
| `--export / --import` | Team config sharing |
|
|
89
89
|
| `--verify` | Test each hook |
|
|
90
|
-
| `--install-example <name>` | Install from
|
|
90
|
+
| `--install-example <name>` | Install from 71 examples |
|
|
91
91
|
| `--examples [filter]` | Browse examples by keyword |
|
|
92
92
|
| `--full` | All-in-one setup |
|
|
93
93
|
| `--status` | Check installed hooks |
|
|
@@ -0,0 +1,319 @@
|
|
|
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 Cheat Sheet — Copy-Paste Patterns</title>
|
|
7
|
+
<meta name="description" content="30+ copy-paste hook patterns for Claude Code. Block dangerous commands, auto-approve safe ones, monitor costs. One page, zero fluff.">
|
|
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.5}
|
|
11
|
+
.c{max-width:900px;margin:0 auto}
|
|
12
|
+
h1{color:#f0f6fc;font-size:1.5rem;margin-bottom:.3rem}
|
|
13
|
+
h2{color:#f0f6fc;font-size:1.1rem;margin:1.5rem 0 .5rem;padding-bottom:.3rem;border-bottom:1px solid #21262d}
|
|
14
|
+
h3{color:#c9d1d9;font-size:.9rem;margin:.8rem 0 .3rem}
|
|
15
|
+
.sub{color:#8b949e;font-size:.85rem;margin-bottom:1.5rem}
|
|
16
|
+
a{color:#58a6ff;text-decoration:none}
|
|
17
|
+
code{background:#161b22;padding:.15rem .3rem;border-radius:3px;font-size:.85rem;color:#e6edf3}
|
|
18
|
+
pre{background:#161b22;border:1px solid #30363d;border-radius:6px;padding:.8rem;overflow-x:auto;font-size:.8rem;color:#e6edf3;margin:.4rem 0 .8rem;position:relative}
|
|
19
|
+
.copy{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}
|
|
20
|
+
.copy:hover{color:#f0f6fc;border-color:#58a6ff}
|
|
21
|
+
.pattern{background:#161b22;border:1px solid #30363d;border-radius:6px;padding:.8rem;margin:.5rem 0}
|
|
22
|
+
.pattern-title{font-weight:600;color:#f0f6fc;font-size:.85rem}
|
|
23
|
+
.pattern-desc{color:#8b949e;font-size:.78rem;margin:.2rem 0}
|
|
24
|
+
.badge{display:inline-block;padding:.1rem .3rem;border-radius:3px;font-size:.6rem;font-weight:bold;margin-left:.3rem}
|
|
25
|
+
.b-block{background:#da363322;color:#f85149;border:1px solid #da363344}
|
|
26
|
+
.b-warn{background:#d2992222;color:#d29922;border:1px solid #d2992244}
|
|
27
|
+
.b-approve{background:#23863622;color:#3fb950;border:1px solid #23863644}
|
|
28
|
+
.b-monitor{background:#1f6feb22;color:#58a6ff;border:1px solid #1f6feb44}
|
|
29
|
+
.toc{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:.3rem;margin:.8rem 0}
|
|
30
|
+
.toc a{display:block;background:#161b22;border:1px solid #30363d;border-radius:4px;padding:.3rem .5rem;font-size:.75rem}
|
|
31
|
+
.toc a:hover{border-color:#58a6ff}
|
|
32
|
+
.footer{text-align:center;color:#484f58;font-size:.7rem;margin-top:2rem;padding-top:1rem;border-top:1px solid #21262d}
|
|
33
|
+
.quick{background:#238636;color:#fff;padding:.5rem 1rem;border-radius:6px;font-family:monospace;font-size:.85rem;cursor:pointer;border:none;display:block;width:100%;text-align:center;margin:.5rem 0}
|
|
34
|
+
.quick:hover{background:#2ea043}
|
|
35
|
+
table{width:100%;border-collapse:collapse;margin:.5rem 0;font-size:.8rem}
|
|
36
|
+
th,td{padding:.3rem .5rem;border:1px solid #21262d;text-align:left}
|
|
37
|
+
th{background:#161b22;color:#f0f6fc}
|
|
38
|
+
.note{background:#161b22;border-left:3px solid #58a6ff;padding:.5rem .8rem;margin:.5rem 0;font-size:.78rem;color:#8b949e}
|
|
39
|
+
</style>
|
|
40
|
+
</head>
|
|
41
|
+
<body>
|
|
42
|
+
<div class="c">
|
|
43
|
+
|
|
44
|
+
<h1>Claude Code Hooks Cheat Sheet</h1>
|
|
45
|
+
<p class="sub">Copy-paste patterns. Zero fluff. <a href="https://docs.anthropic.com/en/docs/claude-code/hooks">Official docs</a></p>
|
|
46
|
+
|
|
47
|
+
<button class="quick" onclick="navigator.clipboard.writeText('npx cc-safe-setup');this.textContent='Copied!';setTimeout(()=>this.textContent='npx cc-safe-setup — install 8 safety hooks instantly',1500)">npx cc-safe-setup — install 8 safety hooks instantly</button>
|
|
48
|
+
|
|
49
|
+
<div class="toc">
|
|
50
|
+
<a href="#basics">Basics</a>
|
|
51
|
+
<a href="#block">Block Patterns</a>
|
|
52
|
+
<a href="#approve">Auto-Approve</a>
|
|
53
|
+
<a href="#monitor">Monitor</a>
|
|
54
|
+
<a href="#settings">settings.json</a>
|
|
55
|
+
<a href="#debug">Debug</a>
|
|
56
|
+
<a href="#recipes">Quick Recipes</a>
|
|
57
|
+
<a href="#exit-codes">Exit Codes</a>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<h2 id="basics">How Hooks Work</h2>
|
|
61
|
+
|
|
62
|
+
<table>
|
|
63
|
+
<tr><th>Event</th><th>When</th><th>Use for</th></tr>
|
|
64
|
+
<tr><td><code>PreToolUse</code></td><td>Before any tool runs</td><td>Block/approve commands</td></tr>
|
|
65
|
+
<tr><td><code>PostToolUse</code></td><td>After tool completes</td><td>Check results, monitor</td></tr>
|
|
66
|
+
<tr><td><code>Stop</code></td><td>Claude finishes responding</td><td>Notifications, cleanup</td></tr>
|
|
67
|
+
<tr><td><code>SubagentStop</code></td><td>Subagent finishes</td><td>Monitor sub-agents</td></tr>
|
|
68
|
+
</table>
|
|
69
|
+
|
|
70
|
+
<table>
|
|
71
|
+
<tr><th>Exit Code</th><th>Effect</th></tr>
|
|
72
|
+
<tr><td><code>0</code></td><td>Allow (no action)</td></tr>
|
|
73
|
+
<tr><td><code>2</code></td><td><strong>BLOCK</strong> — model cannot bypass</td></tr>
|
|
74
|
+
<tr><td>stdout JSON</td><td>Override decision (approve/block with reason)</td></tr>
|
|
75
|
+
</table>
|
|
76
|
+
|
|
77
|
+
<div class="note">
|
|
78
|
+
<strong>Key insight:</strong> CLAUDE.md rules degrade as context fills up. Hooks run every single time, enforced at the process level.
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<h2 id="block">Block Patterns</h2>
|
|
82
|
+
|
|
83
|
+
<h3>Block a specific command <span class="badge b-block">BLOCK</span></h3>
|
|
84
|
+
<pre><code>#!/bin/bash
|
|
85
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
86
|
+
[ -z "$COMMAND" ] && exit 0
|
|
87
|
+
if echo "$COMMAND" | grep -qE 'rm\s+.*-rf\s+/'; then
|
|
88
|
+
echo "BLOCKED: rm -rf on root" >&2
|
|
89
|
+
exit 2
|
|
90
|
+
fi
|
|
91
|
+
exit 0</code><button class="copy" onclick="copyBlock(this)">Copy</button></pre>
|
|
92
|
+
|
|
93
|
+
<h3>Block file edits to specific paths <span class="badge b-block">BLOCK</span></h3>
|
|
94
|
+
<pre><code>#!/bin/bash
|
|
95
|
+
FILE=$(cat | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
96
|
+
[ -z "$FILE" ] && exit 0
|
|
97
|
+
case "$FILE" in
|
|
98
|
+
*/.env*|*/.ssh/*|*/.aws/*|*/credentials*)
|
|
99
|
+
echo "BLOCKED: Protected file" >&2; exit 2 ;;
|
|
100
|
+
esac
|
|
101
|
+
exit 0</code><button class="copy" onclick="copyBlock(this)">Copy</button></pre>
|
|
102
|
+
|
|
103
|
+
<h3>Block git push to main <span class="badge b-block">BLOCK</span></h3>
|
|
104
|
+
<pre><code>#!/bin/bash
|
|
105
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
106
|
+
[ -z "$COMMAND" ] && exit 0
|
|
107
|
+
if echo "$COMMAND" | grep -qE 'git\s+push\s+.*\b(main|master)\b'; then
|
|
108
|
+
echo "BLOCKED: Direct push to main" >&2; exit 2
|
|
109
|
+
fi
|
|
110
|
+
if echo "$COMMAND" | grep -qE 'git\s+push\s+.*--force'; then
|
|
111
|
+
echo "BLOCKED: Force push" >&2; exit 2
|
|
112
|
+
fi
|
|
113
|
+
exit 0</code><button class="copy" onclick="copyBlock(this)">Copy</button></pre>
|
|
114
|
+
|
|
115
|
+
<h3>Block destructive git when dirty <span class="badge b-block">BLOCK</span></h3>
|
|
116
|
+
<pre><code>#!/bin/bash
|
|
117
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
118
|
+
[ -z "$COMMAND" ] && exit 0
|
|
119
|
+
echo "$COMMAND" | grep -qE 'git\s+(checkout\s+--|reset\s+--hard|clean\s+-f)' || exit 0
|
|
120
|
+
DIRTY=$(git status --porcelain 2>/dev/null)
|
|
121
|
+
if [ -n "$DIRTY" ]; then
|
|
122
|
+
echo "BLOCKED: Uncommitted changes would be lost" >&2; exit 2
|
|
123
|
+
fi
|
|
124
|
+
exit 0</code><button class="copy" onclick="copyBlock(this)">Copy</button></pre>
|
|
125
|
+
|
|
126
|
+
<h3>Block commits with conflict markers <span class="badge b-block">BLOCK</span></h3>
|
|
127
|
+
<pre><code>#!/bin/bash
|
|
128
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
129
|
+
echo "$COMMAND" | grep -qE 'git\s+commit' || exit 0
|
|
130
|
+
CONFLICTS=$(git diff --cached --name-only 2>/dev/null | while read f; do
|
|
131
|
+
[ -f "$f" ] && grep -lE '^(<{7}|={7}|>{7})' "$f" 2>/dev/null
|
|
132
|
+
done)
|
|
133
|
+
if [ -n "$CONFLICTS" ]; then
|
|
134
|
+
echo "BLOCKED: Conflict markers in staged files" >&2; exit 2
|
|
135
|
+
fi
|
|
136
|
+
exit 0</code><button class="copy" onclick="copyBlock(this)">Copy</button></pre>
|
|
137
|
+
|
|
138
|
+
<h2 id="approve">Auto-Approve Patterns</h2>
|
|
139
|
+
|
|
140
|
+
<h3>Auto-approve safe commands <span class="badge b-approve">APPROVE</span></h3>
|
|
141
|
+
<pre><code>#!/bin/bash
|
|
142
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
143
|
+
[ -z "$COMMAND" ] && exit 0
|
|
144
|
+
if echo "$COMMAND" | grep -qE '^\s*(npm\s+test|cargo\s+test|go\s+test|pytest|make\s+test)'; then
|
|
145
|
+
echo '{"decision":"approve","reason":"Safe test command"}'
|
|
146
|
+
exit 0
|
|
147
|
+
fi
|
|
148
|
+
exit 0</code><button class="copy" onclick="copyBlock(this)">Copy</button></pre>
|
|
149
|
+
|
|
150
|
+
<h3>Auto-approve read-only git <span class="badge b-approve">APPROVE</span></h3>
|
|
151
|
+
<pre><code>#!/bin/bash
|
|
152
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
153
|
+
[ -z "$COMMAND" ] && exit 0
|
|
154
|
+
if echo "$COMMAND" | grep -qE '^\s*git\s+(status|log|diff|show|branch|remote|tag\s+-l)'; then
|
|
155
|
+
echo '{"decision":"approve","reason":"Read-only git"}'
|
|
156
|
+
fi
|
|
157
|
+
exit 0</code><button class="copy" onclick="copyBlock(this)">Copy</button></pre>
|
|
158
|
+
|
|
159
|
+
<h2 id="monitor">Monitor Patterns</h2>
|
|
160
|
+
|
|
161
|
+
<h3>Log all tool calls <span class="badge b-monitor">MONITOR</span></h3>
|
|
162
|
+
<pre><code>#!/bin/bash
|
|
163
|
+
INPUT=$(cat)
|
|
164
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // "unknown"' 2>/dev/null)
|
|
165
|
+
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // .tool_input.file_path // ""' 2>/dev/null)
|
|
166
|
+
echo "[$(date -Iseconds)] $TOOL: $CMD" >> ~/.claude/tool-calls.log
|
|
167
|
+
exit 0</code><button class="copy" onclick="copyBlock(this)">Copy</button></pre>
|
|
168
|
+
|
|
169
|
+
<h3>Warn on large output <span class="badge b-warn">WARN</span></h3>
|
|
170
|
+
<pre><code>#!/bin/bash
|
|
171
|
+
OUTPUT=$(cat | jq -r '.tool_result // empty' 2>/dev/null)
|
|
172
|
+
LEN=${#OUTPUT}
|
|
173
|
+
if [ "$LEN" -gt 50000 ]; then
|
|
174
|
+
echo "WARNING: Output is ${LEN} chars — consuming context fast" >&2
|
|
175
|
+
fi
|
|
176
|
+
exit 0</code><button class="copy" onclick="copyBlock(this)">Copy</button></pre>
|
|
177
|
+
|
|
178
|
+
<h3>Desktop notification on session end <span class="badge b-monitor">MONITOR</span></h3>
|
|
179
|
+
<pre><code>#!/bin/bash
|
|
180
|
+
# TRIGGER: Stop MATCHER: ""
|
|
181
|
+
MSG=$(cat | jq -r '.stop_reason // "completed"' 2>/dev/null)
|
|
182
|
+
notify-send "Claude Code" "Session $MSG" 2>/dev/null || # Linux
|
|
183
|
+
osascript -e "display notification \"$MSG\"" 2>/dev/null # macOS
|
|
184
|
+
exit 0</code><button class="copy" onclick="copyBlock(this)">Copy</button></pre>
|
|
185
|
+
|
|
186
|
+
<h2 id="settings">settings.json Reference</h2>
|
|
187
|
+
|
|
188
|
+
<h3>Minimal setup (one hook)</h3>
|
|
189
|
+
<pre><code>{
|
|
190
|
+
"hooks": {
|
|
191
|
+
"PreToolUse": [{
|
|
192
|
+
"matcher": "Bash",
|
|
193
|
+
"hooks": [{
|
|
194
|
+
"type": "command",
|
|
195
|
+
"command": "bash ~/.claude/hooks/my-guard.sh"
|
|
196
|
+
}]
|
|
197
|
+
}]
|
|
198
|
+
}
|
|
199
|
+
}</code><button class="copy" onclick="copyBlock(this)">Copy</button></pre>
|
|
200
|
+
|
|
201
|
+
<h3>Multiple hooks on different triggers</h3>
|
|
202
|
+
<pre><code>{
|
|
203
|
+
"hooks": {
|
|
204
|
+
"PreToolUse": [
|
|
205
|
+
{
|
|
206
|
+
"matcher": "Bash",
|
|
207
|
+
"hooks": [
|
|
208
|
+
{"type":"command","command":"bash ~/.claude/hooks/destructive-guard.sh"},
|
|
209
|
+
{"type":"command","command":"bash ~/.claude/hooks/branch-guard.sh"}
|
|
210
|
+
]
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"matcher": "Edit|Write",
|
|
214
|
+
"hooks": [
|
|
215
|
+
{"type":"command","command":"bash ~/.claude/hooks/scope-guard.sh"}
|
|
216
|
+
]
|
|
217
|
+
}
|
|
218
|
+
],
|
|
219
|
+
"PostToolUse": [{
|
|
220
|
+
"matcher": "",
|
|
221
|
+
"hooks": [{"type":"command","command":"bash ~/.claude/hooks/monitor.sh"}]
|
|
222
|
+
}],
|
|
223
|
+
"Stop": [{
|
|
224
|
+
"matcher": "",
|
|
225
|
+
"hooks": [{"type":"command","command":"bash ~/.claude/hooks/notify.sh"}]
|
|
226
|
+
}]
|
|
227
|
+
}
|
|
228
|
+
}</code><button class="copy" onclick="copyBlock(this)">Copy</button></pre>
|
|
229
|
+
|
|
230
|
+
<div class="note">
|
|
231
|
+
<strong>Matcher:</strong> Empty string <code>""</code> matches all tools. <code>"Bash"</code> matches Bash only. <code>"Edit|Write"</code> matches either. Matcher is a regex against the tool name.
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<h2 id="debug">Debug Hooks</h2>
|
|
235
|
+
|
|
236
|
+
<h3>Test a hook manually</h3>
|
|
237
|
+
<pre><code># Test PreToolUse hook with sample input
|
|
238
|
+
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | bash ~/.claude/hooks/my-guard.sh
|
|
239
|
+
echo "Exit code: $?"</code><button class="copy" onclick="copyBlock(this)">Copy</button></pre>
|
|
240
|
+
|
|
241
|
+
<h3>Check hook is executable</h3>
|
|
242
|
+
<pre><code>ls -la ~/.claude/hooks/
|
|
243
|
+
# Fix: chmod +x ~/.claude/hooks/*.sh</code><button class="copy" onclick="copyBlock(this)">Copy</button></pre>
|
|
244
|
+
|
|
245
|
+
<h3>Validate settings.json</h3>
|
|
246
|
+
<pre><code>python3 -c "import json; json.load(open('$HOME/.claude/settings.json')); print('Valid')"</code><button class="copy" onclick="copyBlock(this)">Copy</button></pre>
|
|
247
|
+
|
|
248
|
+
<h3>Quick health check</h3>
|
|
249
|
+
<pre><code>npx cc-safe-setup --quickfix # Auto-detect and fix problems
|
|
250
|
+
npx cc-safe-setup --doctor # Detailed diagnosis
|
|
251
|
+
npx cc-safe-setup --verify # Test each hook</code><button class="copy" onclick="copyBlock(this)">Copy</button></pre>
|
|
252
|
+
|
|
253
|
+
<h2 id="recipes">Quick Recipes</h2>
|
|
254
|
+
|
|
255
|
+
<div class="pattern">
|
|
256
|
+
<div class="pattern-title">I want to block all sudo commands</div>
|
|
257
|
+
<div class="pattern-desc"><code>npx cc-safe-setup --install-example no-sudo-guard</code></div>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div class="pattern">
|
|
261
|
+
<div class="pattern-title">I want to block database drops</div>
|
|
262
|
+
<div class="pattern-desc"><code>npx cc-safe-setup --install-example block-database-wipe</code></div>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<div class="pattern">
|
|
266
|
+
<div class="pattern-title">I want to auto-approve Python tools</div>
|
|
267
|
+
<div class="pattern-desc"><code>npx cc-safe-setup --install-example auto-approve-python</code></div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<div class="pattern">
|
|
271
|
+
<div class="pattern-title">I want to prevent deploys on Fridays</div>
|
|
272
|
+
<div class="pattern-desc"><code>npx cc-safe-setup --install-example no-deploy-friday</code></div>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
<div class="pattern">
|
|
276
|
+
<div class="pattern-title">I want to track session cost</div>
|
|
277
|
+
<div class="pattern-desc"><code>npx cc-safe-setup --install-example token-budget-guard</code></div>
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
<div class="pattern">
|
|
281
|
+
<div class="pattern-title">I want to generate a custom hook</div>
|
|
282
|
+
<div class="pattern-desc"><code>npx cc-safe-setup --create "block npm publish without tests"</code></div>
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
<div class="pattern">
|
|
286
|
+
<div class="pattern-title">I want to detect what my stack needs</div>
|
|
287
|
+
<div class="pattern-desc"><code>npx cc-safe-setup --scan</code></div>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<h2 id="exit-codes">Exit Code Reference</h2>
|
|
291
|
+
<table>
|
|
292
|
+
<tr><th>Code</th><th>Meaning</th><th>When to use</th></tr>
|
|
293
|
+
<tr><td><code>0</code></td><td>Allow / no opinion</td><td>Default — hook has nothing to say</td></tr>
|
|
294
|
+
<tr><td><code>2</code></td><td>BLOCK</td><td>Dangerous command detected</td></tr>
|
|
295
|
+
<tr><td><code>0</code> + stdout JSON</td><td>Override decision</td><td>Auto-approve with reason</td></tr>
|
|
296
|
+
<tr><td><code>1</code></td><td>Hook error (ignored)</td><td>Don't use — hook failures are silent</td></tr>
|
|
297
|
+
</table>
|
|
298
|
+
|
|
299
|
+
<div class="note">
|
|
300
|
+
<strong>stdout JSON format:</strong> <code>{"decision":"approve","reason":"Safe command"}</code> or <code>{"decision":"block","reason":"Too dangerous"}</code>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
<div class="footer">
|
|
304
|
+
<a href="https://github.com/yurukusa/cc-safe-setup">cc-safe-setup</a> — 71 hooks, 28 commands, 405 tests ·
|
|
305
|
+
<a href="https://yurukusa.github.io/cc-hook-registry/playground.html">Playground</a> ·
|
|
306
|
+
<a href="https://yurukusa.github.io/cc-hook-registry/">Registry</a>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
<script>
|
|
311
|
+
function copyBlock(btn) {
|
|
312
|
+
const code = btn.parentElement.querySelector('code').textContent;
|
|
313
|
+
navigator.clipboard.writeText(code);
|
|
314
|
+
btn.textContent = 'Copied!';
|
|
315
|
+
setTimeout(() => btn.textContent = 'Copy', 1500);
|
|
316
|
+
}
|
|
317
|
+
</script>
|
|
318
|
+
</body>
|
|
319
|
+
</html>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# conflict-marker-guard.sh — Block commits with conflict markers
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude sometimes resolves merge conflicts incorrectly, leaving
|
|
7
|
+
# <<<<<<< / ======= / >>>>>>> markers in files. This hook checks
|
|
8
|
+
# staged files for conflict markers before allowing a commit.
|
|
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 on git commit
|
|
17
|
+
echo "$COMMAND" | grep -qE '^\s*git\s+commit' || exit 0
|
|
18
|
+
|
|
19
|
+
# Check staged files for conflict markers
|
|
20
|
+
CONFLICTS=$(git diff --cached --name-only 2>/dev/null | while read -r f; do
|
|
21
|
+
[ -f "$f" ] && grep -lE '^(<{7}|={7}|>{7})' "$f" 2>/dev/null
|
|
22
|
+
done)
|
|
23
|
+
|
|
24
|
+
if [ -n "$CONFLICTS" ]; then
|
|
25
|
+
COUNT=$(echo "$CONFLICTS" | wc -l)
|
|
26
|
+
echo "BLOCKED: $COUNT file(s) contain merge conflict markers:" >&2
|
|
27
|
+
echo "$CONFLICTS" | head -5 | sed 's/^/ /' >&2
|
|
28
|
+
echo "" >&2
|
|
29
|
+
echo "Resolve conflicts before committing." >&2
|
|
30
|
+
exit 2
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
exit 0
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# fact-check-gate.sh — Warn when docs reference unread source files
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude writes documentation that references source code without
|
|
7
|
+
# actually reading the files first. This leads to hallucinated
|
|
8
|
+
# function signatures, wrong parameter names, and false claims.
|
|
9
|
+
#
|
|
10
|
+
# This hook tracks which files were Read in the session, and warns
|
|
11
|
+
# when a doc edit mentions source files that weren't read.
|
|
12
|
+
#
|
|
13
|
+
# TRIGGER: PostToolUse MATCHER: "Edit|Write"
|
|
14
|
+
#
|
|
15
|
+
# Born from: https://github.com/anthropics/claude-code/issues/38057
|
|
16
|
+
# "Claude produces false claims in technical docs"
|
|
17
|
+
# ================================================================
|
|
18
|
+
|
|
19
|
+
INPUT=$(cat)
|
|
20
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
21
|
+
[ -z "$FILE" ] && exit 0
|
|
22
|
+
|
|
23
|
+
# Only check documentation files
|
|
24
|
+
case "$FILE" in
|
|
25
|
+
*.md|*.rst|*.txt|*/docs/*|*/doc/*|*README*|*CHANGELOG*|*CONTRIBUTING*)
|
|
26
|
+
;;
|
|
27
|
+
*)
|
|
28
|
+
exit 0
|
|
29
|
+
;;
|
|
30
|
+
esac
|
|
31
|
+
|
|
32
|
+
# Track reads in a session state file
|
|
33
|
+
STATE="/tmp/cc-fact-check-reads-$(echo "$PWD" | md5sum | cut -c1-8)"
|
|
34
|
+
|
|
35
|
+
# Get the content being written
|
|
36
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null)
|
|
37
|
+
[ -z "$CONTENT" ] && exit 0
|
|
38
|
+
|
|
39
|
+
# Extract referenced source files from the doc content
|
|
40
|
+
# Looks for: `filename.ext`, filename.ext, import from, require()
|
|
41
|
+
REFS=$(echo "$CONTENT" | grep -oE '`[a-zA-Z0-9_/-]+\.(js|ts|py|go|rs|java|rb|sh|mjs|cjs|jsx|tsx)`' | tr -d '`' | sort -u)
|
|
42
|
+
[ -z "$REFS" ] && exit 0
|
|
43
|
+
|
|
44
|
+
# Check if referenced files were read in this session
|
|
45
|
+
if [ ! -f "$STATE" ]; then
|
|
46
|
+
# No reads tracked yet — warn about all references
|
|
47
|
+
COUNT=$(echo "$REFS" | wc -l)
|
|
48
|
+
echo "WARNING: Doc references $COUNT source file(s) that may not have been read:" >&2
|
|
49
|
+
echo "$REFS" | head -5 | sed 's/^/ /' >&2
|
|
50
|
+
echo "Read the source files before documenting them to avoid hallucination." >&2
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
exit 0
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# strict-allowlist.sh — Only allow explicitly permitted commands
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Instead of blocking known-bad commands (denylist), this hook
|
|
7
|
+
# only allows known-good commands (allowlist). Every command not
|
|
8
|
+
# on the list requires explicit approval.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
11
|
+
#
|
|
12
|
+
# CONFIG:
|
|
13
|
+
# CC_ALLOWLIST_FILE=~/.claude/allowlist.txt
|
|
14
|
+
# One pattern per line, regex supported.
|
|
15
|
+
# Empty file = block everything.
|
|
16
|
+
#
|
|
17
|
+
# Born from: https://github.com/anthropics/claude-code/issues/37471
|
|
18
|
+
# "Immutable session manifest with allowlist-only enforcement"
|
|
19
|
+
# ================================================================
|
|
20
|
+
|
|
21
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
22
|
+
[ -z "$COMMAND" ] && exit 0
|
|
23
|
+
|
|
24
|
+
ALLOWLIST="${CC_ALLOWLIST_FILE:-$HOME/.claude/allowlist.txt}"
|
|
25
|
+
|
|
26
|
+
# If no allowlist file exists, create a default one
|
|
27
|
+
if [ ! -f "$ALLOWLIST" ]; then
|
|
28
|
+
mkdir -p "$(dirname "$ALLOWLIST")"
|
|
29
|
+
cat > "$ALLOWLIST" <<'DEFAULT'
|
|
30
|
+
# Claude Code Strict Allowlist
|
|
31
|
+
# One regex pattern per line. Commands matching any pattern are allowed.
|
|
32
|
+
# Lines starting with # are comments.
|
|
33
|
+
# Empty = block all Bash commands.
|
|
34
|
+
|
|
35
|
+
# Read-only operations
|
|
36
|
+
^ls\b
|
|
37
|
+
^cat\b
|
|
38
|
+
^head\b
|
|
39
|
+
^tail\b
|
|
40
|
+
^wc\b
|
|
41
|
+
^grep\b
|
|
42
|
+
^find\b
|
|
43
|
+
^which\b
|
|
44
|
+
^echo\b
|
|
45
|
+
^pwd$
|
|
46
|
+
^date$
|
|
47
|
+
|
|
48
|
+
# Git read
|
|
49
|
+
^git\s+(status|log|diff|show|branch|remote|tag\s+-l)
|
|
50
|
+
^git\s+add\b
|
|
51
|
+
^git\s+commit\b
|
|
52
|
+
|
|
53
|
+
# Build/test
|
|
54
|
+
^npm\s+(test|run\s+(build|lint|check|format))
|
|
55
|
+
^pytest\b
|
|
56
|
+
^cargo\s+(build|test|check|clippy)
|
|
57
|
+
^go\s+(build|test|vet|fmt)
|
|
58
|
+
^make\s*(build|test|lint|check|all)?$
|
|
59
|
+
|
|
60
|
+
# Package info
|
|
61
|
+
^npm\s+(ls|list|info|view|outdated)
|
|
62
|
+
^pip\s+(list|show|freeze)
|
|
63
|
+
DEFAULT
|
|
64
|
+
echo "NOTE: Created default allowlist at $ALLOWLIST" >&2
|
|
65
|
+
echo "Edit it to customize permitted commands." >&2
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# Check command against allowlist
|
|
69
|
+
ALLOWED=0
|
|
70
|
+
while IFS= read -r pattern; do
|
|
71
|
+
# Skip comments and empty lines
|
|
72
|
+
[[ "$pattern" =~ ^[[:space:]]*# ]] && continue
|
|
73
|
+
[[ -z "$pattern" ]] && continue
|
|
74
|
+
|
|
75
|
+
if echo "$COMMAND" | grep -qE "$pattern"; then
|
|
76
|
+
ALLOWED=1
|
|
77
|
+
break
|
|
78
|
+
fi
|
|
79
|
+
done < "$ALLOWLIST"
|
|
80
|
+
|
|
81
|
+
if [ "$ALLOWED" -eq 0 ]; then
|
|
82
|
+
echo "BLOCKED: Command not in allowlist." >&2
|
|
83
|
+
echo "Command: $COMMAND" >&2
|
|
84
|
+
echo "Add a matching pattern to $ALLOWLIST to permit." >&2
|
|
85
|
+
exit 2
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
exit 0
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# token-budget-guard.sh — Estimate and limit session token cost
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude Code sessions can consume hundreds of dollars in tokens
|
|
7
|
+
# without the user realizing it. This hook estimates cumulative
|
|
8
|
+
# cost and warns/blocks when a budget threshold is exceeded.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PostToolUse MATCHER: ""
|
|
11
|
+
#
|
|
12
|
+
# CONFIG:
|
|
13
|
+
# CC_TOKEN_BUDGET=10 (warn at $10 estimated cost)
|
|
14
|
+
# CC_TOKEN_BLOCK=50 (block at $50 estimated cost)
|
|
15
|
+
#
|
|
16
|
+
# Born from: https://github.com/anthropics/claude-code/issues/38029
|
|
17
|
+
# "652k output tokens ($342) without user input"
|
|
18
|
+
# ================================================================
|
|
19
|
+
|
|
20
|
+
WARN_BUDGET="${CC_TOKEN_BUDGET:-10}"
|
|
21
|
+
BLOCK_BUDGET="${CC_TOKEN_BLOCK:-50}"
|
|
22
|
+
STATE="/tmp/cc-token-budget-$(echo "$PWD" | md5sum | cut -c1-8)"
|
|
23
|
+
|
|
24
|
+
# Estimate tokens from tool output size
|
|
25
|
+
INPUT=$(cat)
|
|
26
|
+
OUTPUT=$(echo "$INPUT" | jq -r '.tool_result // empty' 2>/dev/null)
|
|
27
|
+
OUTPUT_LEN=${#OUTPUT}
|
|
28
|
+
|
|
29
|
+
# Rough estimation: 1 token ≈ 4 chars, $15/M input + $75/M output for Opus
|
|
30
|
+
# This is approximate — actual costs depend on model and caching
|
|
31
|
+
TOKENS=$((OUTPUT_LEN / 4))
|
|
32
|
+
|
|
33
|
+
# Accumulate
|
|
34
|
+
TOTAL=0
|
|
35
|
+
[ -f "$STATE" ] && TOTAL=$(cat "$STATE" 2>/dev/null || echo 0)
|
|
36
|
+
TOTAL=$((TOTAL + TOKENS))
|
|
37
|
+
echo "$TOTAL" > "$STATE"
|
|
38
|
+
|
|
39
|
+
# Estimate cost (output tokens at $75/M for Opus)
|
|
40
|
+
# Using integer math: cost_cents = tokens * 75 / 10000
|
|
41
|
+
COST_CENTS=$((TOTAL * 75 / 10000))
|
|
42
|
+
|
|
43
|
+
if [ "$COST_CENTS" -ge "$((BLOCK_BUDGET * 100))" ]; then
|
|
44
|
+
echo "BLOCKED: Estimated session cost ~\$${COST_CENTS%??}.${COST_CENTS: -2} exceeds \$$BLOCK_BUDGET budget." >&2
|
|
45
|
+
echo "Reset: rm $STATE" >&2
|
|
46
|
+
exit 2
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
if [ "$COST_CENTS" -ge "$((WARN_BUDGET * 100))" ]; then
|
|
50
|
+
echo "WARNING: Estimated session cost ~\$${COST_CENTS%??}.${COST_CENTS: -2} approaching \$$BLOCK_BUDGET limit." >&2
|
|
51
|
+
echo "Consider using /compact or starting a new session." >&2
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
exit 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.6.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": {
|