cc-safe-setup 7.5.0 → 7.7.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 + 72 examples = **80 hooks**. 28 CLI commands. 409 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,49 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# error-memory-guard.sh — Detect repeated failed commands
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude often retries the exact same command that just failed,
|
|
7
|
+
# sometimes 5-10 times before trying a different approach.
|
|
8
|
+
# This hook tracks command+error pairs and blocks retries of
|
|
9
|
+
# commands that have already failed with the same error.
|
|
10
|
+
#
|
|
11
|
+
# TRIGGER: PostToolUse MATCHER: "Bash"
|
|
12
|
+
#
|
|
13
|
+
# Unlike loop-detector (which catches any repetition), this
|
|
14
|
+
# specifically targets the "same command, same error" pattern.
|
|
15
|
+
# ================================================================
|
|
16
|
+
|
|
17
|
+
INPUT=$(cat)
|
|
18
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
19
|
+
[ -z "$COMMAND" ] && exit 0
|
|
20
|
+
|
|
21
|
+
EXIT_CODE=$(echo "$INPUT" | jq -r '.tool_result_exit_code // 0' 2>/dev/null)
|
|
22
|
+
ERROR=$(echo "$INPUT" | jq -r '.tool_result // empty' 2>/dev/null | tail -5)
|
|
23
|
+
|
|
24
|
+
# Only track failures
|
|
25
|
+
[ "$EXIT_CODE" = "0" ] && exit 0
|
|
26
|
+
|
|
27
|
+
STATE="/tmp/cc-error-memory-$(echo "$PWD" | md5sum | cut -c1-8)"
|
|
28
|
+
HASH=$(echo "$COMMAND" | md5sum | cut -c1-16)
|
|
29
|
+
|
|
30
|
+
# Check if this exact command already failed
|
|
31
|
+
if grep -q "^$HASH:" "$STATE" 2>/dev/null; then
|
|
32
|
+
PREV_COUNT=$(grep "^$HASH:" "$STATE" | cut -d: -f2)
|
|
33
|
+
NEW_COUNT=$((PREV_COUNT + 1))
|
|
34
|
+
sed -i "s/^$HASH:.*/$HASH:$NEW_COUNT/" "$STATE"
|
|
35
|
+
|
|
36
|
+
if [ "$NEW_COUNT" -ge 3 ]; then
|
|
37
|
+
echo "BLOCKED: This command has failed $NEW_COUNT times with the same error." >&2
|
|
38
|
+
echo "Try a different approach instead of retrying." >&2
|
|
39
|
+
echo "Command: $(echo "$COMMAND" | head -c 80)" >&2
|
|
40
|
+
echo "Reset: rm $STATE" >&2
|
|
41
|
+
exit 2
|
|
42
|
+
elif [ "$NEW_COUNT" -ge 2 ]; then
|
|
43
|
+
echo "WARNING: Command failed $NEW_COUNT times. Consider a different approach." >&2
|
|
44
|
+
fi
|
|
45
|
+
else
|
|
46
|
+
echo "$HASH:1" >> "$STATE"
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
exit 0
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# large-read-guard.sh — Warn before reading large files
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude sometimes cats entire log files, database dumps, or
|
|
7
|
+
# minified bundles into context, wasting tokens and accelerating
|
|
8
|
+
# context exhaustion. This hook warns before reading files larger
|
|
9
|
+
# than a threshold.
|
|
10
|
+
#
|
|
11
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
12
|
+
#
|
|
13
|
+
# CONFIG:
|
|
14
|
+
# CC_MAX_READ_KB=100 (warn above 100KB)
|
|
15
|
+
# ================================================================
|
|
16
|
+
|
|
17
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
18
|
+
[ -z "$COMMAND" ] && exit 0
|
|
19
|
+
|
|
20
|
+
MAX_KB="${CC_MAX_READ_KB:-100}"
|
|
21
|
+
|
|
22
|
+
# Detect file-reading commands
|
|
23
|
+
FILE=""
|
|
24
|
+
if echo "$COMMAND" | grep -qE '^\s*cat\s+'; then
|
|
25
|
+
FILE=$(echo "$COMMAND" | grep -oE 'cat\s+([^ |>]+)' | awk '{print $2}')
|
|
26
|
+
elif echo "$COMMAND" | grep -qE '^\s*less\s+|^\s*more\s+'; then
|
|
27
|
+
FILE=$(echo "$COMMAND" | awk '{print $2}')
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
[ -z "$FILE" ] && exit 0
|
|
31
|
+
[ ! -f "$FILE" ] && exit 0
|
|
32
|
+
|
|
33
|
+
# Check file size
|
|
34
|
+
SIZE_KB=$(du -k "$FILE" 2>/dev/null | cut -f1)
|
|
35
|
+
[ -z "$SIZE_KB" ] && exit 0
|
|
36
|
+
|
|
37
|
+
if [ "$SIZE_KB" -gt "$MAX_KB" ]; then
|
|
38
|
+
LINES=$(wc -l < "$FILE" 2>/dev/null || echo "?")
|
|
39
|
+
echo "WARNING: $FILE is ${SIZE_KB}KB ($LINES lines)." >&2
|
|
40
|
+
echo "Reading large files wastes context tokens." >&2
|
|
41
|
+
echo "Consider: head -100 $FILE, grep pattern $FILE, or tail -50 $FILE" >&2
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
exit 0
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# parallel-edit-guard.sh — Detect concurrent edits to same file
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# When Claude uses subagents (Agent tool), multiple agents can
|
|
7
|
+
# try to edit the same file simultaneously, causing conflicts
|
|
8
|
+
# and lost changes. This hook uses lock files to detect and
|
|
9
|
+
# warn about concurrent edits.
|
|
10
|
+
#
|
|
11
|
+
# TRIGGER: PreToolUse MATCHER: "Edit|Write"
|
|
12
|
+
# ================================================================
|
|
13
|
+
|
|
14
|
+
INPUT=$(cat)
|
|
15
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
16
|
+
[ -z "$FILE" ] && exit 0
|
|
17
|
+
|
|
18
|
+
# Create a lock directory for tracking
|
|
19
|
+
LOCK_DIR="/tmp/cc-edit-locks"
|
|
20
|
+
mkdir -p "$LOCK_DIR"
|
|
21
|
+
|
|
22
|
+
# Normalize file path for lock name
|
|
23
|
+
LOCK_FILE="$LOCK_DIR/$(echo "$FILE" | md5sum | cut -c1-16).lock"
|
|
24
|
+
|
|
25
|
+
# Check if another process has a lock
|
|
26
|
+
if [ -f "$LOCK_FILE" ]; then
|
|
27
|
+
LOCK_AGE=$(($(date +%s) - $(stat -c %Y "$LOCK_FILE" 2>/dev/null || echo 0)))
|
|
28
|
+
LOCK_PID=$(cat "$LOCK_FILE" 2>/dev/null)
|
|
29
|
+
|
|
30
|
+
# Lock is stale if older than 30 seconds
|
|
31
|
+
if [ "$LOCK_AGE" -lt 30 ] && [ "$LOCK_PID" != "$$" ]; then
|
|
32
|
+
echo "WARNING: File $FILE may be edited by another agent." >&2
|
|
33
|
+
echo "Lock age: ${LOCK_AGE}s, PID: $LOCK_PID" >&2
|
|
34
|
+
echo "Wait for the other edit to complete." >&2
|
|
35
|
+
fi
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Set our lock
|
|
39
|
+
echo "$$" > "$LOCK_FILE"
|
|
40
|
+
|
|
41
|
+
# Clean up lock after 30s in background
|
|
42
|
+
(sleep 30 && rm -f "$LOCK_FILE") &>/dev/null &
|
|
43
|
+
|
|
44
|
+
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.7.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": {
|