cc-safe-setup 3.1.0 → 3.2.1
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 +6 -0
- package/SETTINGS_REFERENCE.md +282 -0
- package/docs/cheatsheet.html +187 -0
- package/examples/tmp-cleanup.sh +36 -0
- package/index.mjs +146 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -215,11 +215,16 @@ Or browse all available examples in [`examples/`](examples/):
|
|
|
215
215
|
- **path-traversal-guard.sh** — Block Edit/Write with `../../` path traversal and system directories
|
|
216
216
|
- **case-sensitive-guard.sh** — Detect case-insensitive filesystems (exFAT, NTFS, HFS+) and block rm/mkdir that would collide due to case folding ([#37875](https://github.com/anthropics/claude-code/issues/37875))
|
|
217
217
|
- **compound-command-approver.sh** — Auto-approve safe compound commands (`cd && git log`, `cd && npm test`) that the permission system can't match ([#30519](https://github.com/anthropics/claude-code/issues/30519) [#16561](https://github.com/anthropics/claude-code/issues/16561))
|
|
218
|
+
- **tmp-cleanup.sh** — Clean up accumulated `/tmp/claude-*-cwd` files on session end ([#8856](https://github.com/anthropics/claude-code/issues/8856))
|
|
218
219
|
|
|
219
220
|
## Safety Checklist
|
|
220
221
|
|
|
221
222
|
**[SAFETY_CHECKLIST.md](SAFETY_CHECKLIST.md)** — Copy-paste checklist for before/during/after autonomous sessions.
|
|
222
223
|
|
|
224
|
+
## settings.json Reference
|
|
225
|
+
|
|
226
|
+
**[SETTINGS_REFERENCE.md](SETTINGS_REFERENCE.md)** — Complete reference for permissions, hooks, modes, and common configurations. Includes known limitations and workarounds.
|
|
227
|
+
|
|
223
228
|
## Migration Guide
|
|
224
229
|
|
|
225
230
|
**[MIGRATION.md](MIGRATION.md)** — Step-by-step guide for moving from permissions-only to permissions + hooks. Keep your existing config, add safety layers on top.
|
|
@@ -230,6 +235,7 @@ Or browse all available examples in [`examples/`](examples/):
|
|
|
230
235
|
- [Hooks Cookbook](https://github.com/yurukusa/claude-code-hooks/blob/main/COOKBOOK.md) — 19 ready-to-use recipes from real GitHub Issues
|
|
231
236
|
- [Japanese guide (Qiita)](https://qiita.com/yurukusa/items/a9714b33f5d974e8f1e8) — この記事の日本語解説
|
|
232
237
|
- [Hook Test Runner](https://github.com/yurukusa/cc-hook-test) — `npx cc-hook-test <hook.sh>` to auto-test any hook
|
|
238
|
+
- [Hooks Cheat Sheet](https://yurukusa.github.io/cc-safe-setup/cheatsheet.html) — printable A4 quick reference
|
|
233
239
|
- [Ecosystem Comparison](https://yurukusa.github.io/cc-safe-setup/ecosystem.html) — all Claude Code hook projects compared
|
|
234
240
|
- [The incident that inspired this tool](https://github.com/anthropics/claude-code/issues/36339) — NTFS junction rm -rf
|
|
235
241
|
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# Claude Code settings.json Reference
|
|
2
|
+
|
|
3
|
+
Everything you can put in `~/.claude/settings.json`, documented from real usage and GitHub Issues.
|
|
4
|
+
|
|
5
|
+
## File Locations
|
|
6
|
+
|
|
7
|
+
| File | Scope | Precedence |
|
|
8
|
+
|------|-------|------------|
|
|
9
|
+
| `~/.claude/settings.json` | All projects (user-level) | Lowest |
|
|
10
|
+
| `.claude/settings.json` | Current project | Overrides user |
|
|
11
|
+
| `.claude/settings.local.json` | Current project (gitignored) | Highest |
|
|
12
|
+
|
|
13
|
+
## Permissions
|
|
14
|
+
|
|
15
|
+
### allow
|
|
16
|
+
|
|
17
|
+
Commands that auto-execute without prompting.
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"permissions": {
|
|
22
|
+
"allow": [
|
|
23
|
+
"Bash(git status:*)",
|
|
24
|
+
"Bash(git log:*)",
|
|
25
|
+
"Bash(git diff:*)",
|
|
26
|
+
"Bash(npm test:*)",
|
|
27
|
+
"Bash(npm run:*)",
|
|
28
|
+
"Read(*)",
|
|
29
|
+
"Edit(*)",
|
|
30
|
+
"Write(*)",
|
|
31
|
+
"Glob(*)",
|
|
32
|
+
"Grep(*)"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Pattern syntax:**
|
|
39
|
+
- `Tool(pattern)` — match tool name and argument pattern
|
|
40
|
+
- `*` — wildcard (matches anything)
|
|
41
|
+
- `:` — separator between command and arguments
|
|
42
|
+
- `Bash(git:*)` — any command starting with `git`
|
|
43
|
+
- `Bash(git status:*)` — `git status` with any args
|
|
44
|
+
- `Bash(*)` — all bash commands (dangerous — use with hooks)
|
|
45
|
+
|
|
46
|
+
**Known limitations (as of v2.1.81):**
|
|
47
|
+
- Compound commands don't match: `Bash(git:*)` won't match `cd /path && git log` ([#30519](https://github.com/anthropics/claude-code/issues/30519), [#16561](https://github.com/anthropics/claude-code/issues/16561))
|
|
48
|
+
- "Always Allow" saves exact strings, not patterns ([#6850](https://github.com/anthropics/claude-code/issues/6850))
|
|
49
|
+
- User-level settings may not apply at project level ([#5140](https://github.com/anthropics/claude-code/issues/5140))
|
|
50
|
+
- **Workaround:** Use `compound-command-approver` hook: `npx cc-safe-setup --install-example compound-command-approver`
|
|
51
|
+
|
|
52
|
+
### deny
|
|
53
|
+
|
|
54
|
+
Commands that are always blocked.
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"permissions": {
|
|
59
|
+
"deny": [
|
|
60
|
+
"Bash(rm -rf:*)",
|
|
61
|
+
"Bash(git push --force:*)",
|
|
62
|
+
"Bash(sudo:*)"
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Note:** Deny rules have the same compound-command limitation as allow rules. Hooks are more reliable for blocking.
|
|
69
|
+
|
|
70
|
+
## Hooks
|
|
71
|
+
|
|
72
|
+
### Structure
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"hooks": {
|
|
77
|
+
"PreToolUse": [
|
|
78
|
+
{
|
|
79
|
+
"matcher": "Bash",
|
|
80
|
+
"hooks": [
|
|
81
|
+
{
|
|
82
|
+
"type": "command",
|
|
83
|
+
"command": "~/.claude/hooks/my-hook.sh"
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Hook Events
|
|
93
|
+
|
|
94
|
+
| Event | When | Use Case |
|
|
95
|
+
|-------|------|----------|
|
|
96
|
+
| `PreToolUse` | Before tool executes | Block/modify commands |
|
|
97
|
+
| `PostToolUse` | After tool executes | Validate output, check syntax |
|
|
98
|
+
| `Stop` | Session ends | Log data, notify |
|
|
99
|
+
| `UserPromptSubmit` | User presses Enter | Validate prompts |
|
|
100
|
+
| `Notification` | Claude shows notification | Custom alerts |
|
|
101
|
+
| `PreCompact` | Before context compaction | Save state |
|
|
102
|
+
| `SessionStart` | Session begins | Initialize |
|
|
103
|
+
| `SessionEnd` | Session ends | Cleanup |
|
|
104
|
+
|
|
105
|
+
### Matcher Values
|
|
106
|
+
|
|
107
|
+
| Matcher | Matches |
|
|
108
|
+
|---------|---------|
|
|
109
|
+
| `"Bash"` | Bash tool only |
|
|
110
|
+
| `"Edit\|Write"` | Edit or Write tool |
|
|
111
|
+
| `"Read"` | Read tool only |
|
|
112
|
+
| `""` (empty) | All tools |
|
|
113
|
+
|
|
114
|
+
### Hook Exit Codes
|
|
115
|
+
|
|
116
|
+
| Code | Meaning |
|
|
117
|
+
|------|---------|
|
|
118
|
+
| 0 | Allow (or no opinion) |
|
|
119
|
+
| 2 | **Block** — tool call cancelled |
|
|
120
|
+
| Other | Error (treated as allow) |
|
|
121
|
+
|
|
122
|
+
### Hook Input (stdin JSON)
|
|
123
|
+
|
|
124
|
+
**PreToolUse/PostToolUse:**
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"tool_name": "Bash",
|
|
128
|
+
"tool_input": {
|
|
129
|
+
"command": "git push origin main"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**For Edit/Write:**
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"tool_name": "Edit",
|
|
138
|
+
"tool_input": {
|
|
139
|
+
"file_path": "/path/to/file.py",
|
|
140
|
+
"old_string": "...",
|
|
141
|
+
"new_string": "..."
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Stop:**
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"stop_reason": "user",
|
|
150
|
+
"hook_event_name": "Stop"
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Hook Output (stdout JSON)
|
|
155
|
+
|
|
156
|
+
**Auto-approve:**
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"hookSpecificOutput": {
|
|
160
|
+
"hookEventName": "PreToolUse",
|
|
161
|
+
"permissionDecision": "allow",
|
|
162
|
+
"permissionDecisionReason": "auto-approved by hook"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Modify input:**
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"hookSpecificOutput": {
|
|
171
|
+
"hookEventName": "PreToolUse",
|
|
172
|
+
"updatedInput": {
|
|
173
|
+
"command": "modified command here"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## defaultMode
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"defaultMode": "default"
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
| Mode | Behavior |
|
|
188
|
+
|------|----------|
|
|
189
|
+
| `"default"` | Prompt for unrecognized commands |
|
|
190
|
+
| `"dontAsk"` | Auto-approve everything (hooks still run) |
|
|
191
|
+
| `"bypassPermissions"` | Skip everything including hooks (dangerous) |
|
|
192
|
+
|
|
193
|
+
**Recommendation:** Use `"dontAsk"` + hooks instead of `"bypassPermissions"`.
|
|
194
|
+
|
|
195
|
+
## Common Configurations
|
|
196
|
+
|
|
197
|
+
### Minimal Safe Setup
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
{
|
|
201
|
+
"permissions": {
|
|
202
|
+
"allow": ["Read(*)", "Glob(*)", "Grep(*)"]
|
|
203
|
+
},
|
|
204
|
+
"hooks": {
|
|
205
|
+
"PreToolUse": [
|
|
206
|
+
{
|
|
207
|
+
"matcher": "Bash",
|
|
208
|
+
"hooks": [
|
|
209
|
+
{ "type": "command", "command": "~/.claude/hooks/destructive-guard.sh" },
|
|
210
|
+
{ "type": "command", "command": "~/.claude/hooks/branch-guard.sh" }
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Autonomous Operation
|
|
219
|
+
|
|
220
|
+
```json
|
|
221
|
+
{
|
|
222
|
+
"defaultMode": "dontAsk",
|
|
223
|
+
"permissions": {
|
|
224
|
+
"allow": ["Bash(*)", "Read(*)", "Edit(*)", "Write(*)", "Glob(*)", "Grep(*)"]
|
|
225
|
+
},
|
|
226
|
+
"hooks": {
|
|
227
|
+
"PreToolUse": [
|
|
228
|
+
{
|
|
229
|
+
"matcher": "Bash",
|
|
230
|
+
"hooks": [
|
|
231
|
+
{ "type": "command", "command": "~/.claude/hooks/destructive-guard.sh" },
|
|
232
|
+
{ "type": "command", "command": "~/.claude/hooks/branch-guard.sh" },
|
|
233
|
+
{ "type": "command", "command": "~/.claude/hooks/secret-guard.sh" },
|
|
234
|
+
{ "type": "command", "command": "~/.claude/hooks/compound-command-approver.sh" }
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
],
|
|
238
|
+
"PostToolUse": [
|
|
239
|
+
{
|
|
240
|
+
"matcher": "Edit|Write",
|
|
241
|
+
"hooks": [
|
|
242
|
+
{ "type": "command", "command": "~/.claude/hooks/syntax-check.sh" }
|
|
243
|
+
]
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"matcher": "",
|
|
247
|
+
"hooks": [
|
|
248
|
+
{ "type": "command", "command": "~/.claude/hooks/context-monitor.sh" }
|
|
249
|
+
]
|
|
250
|
+
}
|
|
251
|
+
]
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Generate This Automatically
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
npx cc-safe-setup # Install hooks
|
|
260
|
+
npx cc-safe-setup --audit # Check your score
|
|
261
|
+
npx cc-safe-setup --doctor # Diagnose issues
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Troubleshooting
|
|
265
|
+
|
|
266
|
+
| Problem | Cause | Fix |
|
|
267
|
+
|---------|-------|-----|
|
|
268
|
+
| Hooks don't fire | Not registered in settings.json | `npx cc-safe-setup` |
|
|
269
|
+
| Hooks don't block | Wrong exit code (not 2) | Check `echo $?` after test |
|
|
270
|
+
| "jq: command not found" | jq not installed | `brew install jq` / `apt install jq` |
|
|
271
|
+
| Hook permission denied | Not executable | `chmod +x ~/.claude/hooks/*.sh` |
|
|
272
|
+
| Compound commands prompt | Permission system limitation | Install `compound-command-approver` |
|
|
273
|
+
| "Always Allow" doesn't stick | Saves exact string, not pattern | Use hooks instead |
|
|
274
|
+
|
|
275
|
+
Run `npx cc-safe-setup --doctor` for automated diagnosis.
|
|
276
|
+
|
|
277
|
+
## Resources
|
|
278
|
+
|
|
279
|
+
- [Official Hooks Documentation](https://docs.anthropic.com/en/docs/claude-code/hooks)
|
|
280
|
+
- [COOKBOOK.md](https://github.com/yurukusa/claude-code-hooks/blob/main/COOKBOOK.md) — 20 hook recipes
|
|
281
|
+
- [Migration Guide](MIGRATION.md) — from permissions to hooks
|
|
282
|
+
- [Ecosystem Comparison](https://yurukusa.github.io/cc-safe-setup/ecosystem.html) — all hook projects
|
|
@@ -0,0 +1,187 @@
|
|
|
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</title>
|
|
7
|
+
<style>
|
|
8
|
+
@media print { body { padding: 0; background: #fff; color: #000; font-size: 9px; } .no-print { display: none; } h1 { font-size: 14px; } h2 { font-size: 11px; } pre { font-size: 8px; border: 1px solid #ccc; } .col { break-inside: avoid; } }
|
|
9
|
+
@media screen { body { background: #0d1117; color: #c9d1d9; padding: 1rem; } pre { background: #161b22; border: 1px solid #30363d; } h1 { color: #f0f6fc; } h2 { color: #f0f6fc; } a { color: #58a6ff; } }
|
|
10
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
11
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', monospace; font-size: 11px; line-height: 1.4; }
|
|
12
|
+
.container { max-width: 900px; margin: 0 auto; columns: 2; column-gap: 1.5rem; }
|
|
13
|
+
h1 { font-size: 16px; margin-bottom: 0.3rem; text-align: center; column-span: all; }
|
|
14
|
+
.subtitle { text-align: center; margin-bottom: 0.8rem; font-size: 10px; opacity: 0.7; column-span: all; }
|
|
15
|
+
h2 { font-size: 12px; margin: 0.6rem 0 0.3rem; border-bottom: 1px solid #30363d; padding-bottom: 0.15rem; }
|
|
16
|
+
pre { padding: 0.4rem; border-radius: 4px; overflow-x: auto; font-size: 9.5px; margin: 0.2rem 0 0.4rem; white-space: pre-wrap; word-break: break-all; }
|
|
17
|
+
.col { break-inside: avoid; margin-bottom: 0.3rem; }
|
|
18
|
+
table { width: 100%; border-collapse: collapse; font-size: 9.5px; margin: 0.2rem 0; }
|
|
19
|
+
th, td { text-align: left; padding: 0.15rem 0.3rem; border-bottom: 1px solid #21262d; }
|
|
20
|
+
th { font-weight: 600; }
|
|
21
|
+
code { font-size: 9px; padding: 0.1rem 0.2rem; border-radius: 2px; }
|
|
22
|
+
@media screen { code { background: #161b22; } }
|
|
23
|
+
.footer { text-align: center; font-size: 8px; opacity: 0.5; margin-top: 0.5rem; column-span: all; }
|
|
24
|
+
</style>
|
|
25
|
+
</head>
|
|
26
|
+
<body>
|
|
27
|
+
<h1>Claude Code Hooks Cheat Sheet</h1>
|
|
28
|
+
<p class="subtitle">Quick reference · Print this page (Ctrl+P) · <a href="https://github.com/yurukusa/cc-safe-setup" class="no-print">github.com/yurukusa/cc-safe-setup</a></p>
|
|
29
|
+
|
|
30
|
+
<div class="container">
|
|
31
|
+
|
|
32
|
+
<div class="col">
|
|
33
|
+
<h2>Hook Lifecycle</h2>
|
|
34
|
+
<pre>Prompt → PreToolUse → Tool Executes → PostToolUse → Stop
|
|
35
|
+
↑ block here ↑ check here ↑ log here</pre>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="col">
|
|
39
|
+
<h2>Exit Codes</h2>
|
|
40
|
+
<table>
|
|
41
|
+
<tr><th>Code</th><th>Meaning</th></tr>
|
|
42
|
+
<tr><td><code>0</code></td><td>Allow (or no opinion)</td></tr>
|
|
43
|
+
<tr><td><code>2</code></td><td><strong>Block</strong> — tool call cancelled</td></tr>
|
|
44
|
+
<tr><td>other</td><td>Error (treated as allow)</td></tr>
|
|
45
|
+
</table>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="col">
|
|
49
|
+
<h2>Hook Events</h2>
|
|
50
|
+
<table>
|
|
51
|
+
<tr><th>Event</th><th>Matcher</th><th>Use</th></tr>
|
|
52
|
+
<tr><td>PreToolUse</td><td>Bash</td><td>Block commands</td></tr>
|
|
53
|
+
<tr><td>PostToolUse</td><td>Edit|Write</td><td>Syntax check</td></tr>
|
|
54
|
+
<tr><td>PostToolUse</td><td>(empty)</td><td>All tools</td></tr>
|
|
55
|
+
<tr><td>Stop</td><td>(empty)</td><td>Session end</td></tr>
|
|
56
|
+
<tr><td>UserPromptSubmit</td><td>—</td><td>Validate input</td></tr>
|
|
57
|
+
</table>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="col">
|
|
61
|
+
<h2>settings.json Structure</h2>
|
|
62
|
+
<pre>{
|
|
63
|
+
"hooks": {
|
|
64
|
+
"PreToolUse": [{
|
|
65
|
+
"matcher": "Bash",
|
|
66
|
+
"hooks": [{
|
|
67
|
+
"type": "command",
|
|
68
|
+
"command": "~/.claude/hooks/guard.sh"
|
|
69
|
+
}]
|
|
70
|
+
}]
|
|
71
|
+
}
|
|
72
|
+
}</pre>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="col">
|
|
76
|
+
<h2>Minimal Block Hook</h2>
|
|
77
|
+
<pre>#!/bin/bash
|
|
78
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty')
|
|
79
|
+
[ -z "$COMMAND" ] && exit 0
|
|
80
|
+
if echo "$COMMAND" | grep -qE 'PATTERN'; then
|
|
81
|
+
echo "BLOCKED: reason" >&2
|
|
82
|
+
exit 2
|
|
83
|
+
fi
|
|
84
|
+
exit 0</pre>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div class="col">
|
|
88
|
+
<h2>Auto-Approve Hook</h2>
|
|
89
|
+
<pre>#!/bin/bash
|
|
90
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty')
|
|
91
|
+
[ -z "$COMMAND" ] && exit 0
|
|
92
|
+
if echo "$COMMAND" | grep -qE '^\s*git\s+(status|log|diff)'; then
|
|
93
|
+
jq -n '{
|
|
94
|
+
"hookSpecificOutput": {
|
|
95
|
+
"hookEventName": "PreToolUse",
|
|
96
|
+
"permissionDecision": "allow"
|
|
97
|
+
}
|
|
98
|
+
}'
|
|
99
|
+
fi
|
|
100
|
+
exit 0</pre>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="col">
|
|
104
|
+
<h2>PostToolUse Syntax Check</h2>
|
|
105
|
+
<pre>#!/bin/bash
|
|
106
|
+
FILE=$(cat | jq -r '.tool_input.file_path // empty')
|
|
107
|
+
[ -z "$FILE" ] || [ ! -f "$FILE" ] && exit 0
|
|
108
|
+
case "${FILE##*.}" in
|
|
109
|
+
py) python3 -m py_compile "$FILE" 2>&1 ;;
|
|
110
|
+
sh) bash -n "$FILE" 2>&1 ;;
|
|
111
|
+
json) jq empty "$FILE" 2>&1 ;;
|
|
112
|
+
js) node --check "$FILE" 2>&1 ;;
|
|
113
|
+
esac
|
|
114
|
+
exit 0</pre>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div class="col">
|
|
118
|
+
<h2>Modify Input</h2>
|
|
119
|
+
<pre>#!/bin/bash
|
|
120
|
+
# Strip comments from bash commands
|
|
121
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty')
|
|
122
|
+
CLEAN=$(echo "$COMMAND" | sed '/^#/d; /^$/d')
|
|
123
|
+
[ "$CLEAN" = "$COMMAND" ] && exit 0
|
|
124
|
+
jq -n --arg cmd "$CLEAN" '{
|
|
125
|
+
"hookSpecificOutput": {
|
|
126
|
+
"hookEventName": "PreToolUse",
|
|
127
|
+
"updatedInput": {"command": $cmd}
|
|
128
|
+
}
|
|
129
|
+
}'</pre>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div class="col">
|
|
133
|
+
<h2>stdin JSON Reference</h2>
|
|
134
|
+
<table>
|
|
135
|
+
<tr><th>Event</th><th>Key Fields</th></tr>
|
|
136
|
+
<tr><td>PreToolUse (Bash)</td><td><code>.tool_input.command</code></td></tr>
|
|
137
|
+
<tr><td>PreToolUse (Edit)</td><td><code>.tool_input.file_path</code></td></tr>
|
|
138
|
+
<tr><td>PostToolUse</td><td><code>.tool_input.file_path</code></td></tr>
|
|
139
|
+
<tr><td>Stop</td><td><code>.stop_reason</code></td></tr>
|
|
140
|
+
<tr><td>UserPromptSubmit</td><td><code>.prompt</code></td></tr>
|
|
141
|
+
</table>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div class="col">
|
|
145
|
+
<h2>Test a Hook</h2>
|
|
146
|
+
<pre># Manual test
|
|
147
|
+
echo '{"tool_input":{"command":"rm -rf /"}}' \
|
|
148
|
+
| bash ~/.claude/hooks/guard.sh
|
|
149
|
+
echo $? # 2 = blocked
|
|
150
|
+
|
|
151
|
+
# Auto-test
|
|
152
|
+
npx cc-hook-test ~/.claude/hooks/guard.sh</pre>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div class="col">
|
|
156
|
+
<h2>Quick Setup Commands</h2>
|
|
157
|
+
<table>
|
|
158
|
+
<tr><td><code>npx cc-safe-setup</code></td><td>Install 8 hooks</td></tr>
|
|
159
|
+
<tr><td><code>--create "desc"</code></td><td>Generate hook</td></tr>
|
|
160
|
+
<tr><td><code>--audit</code></td><td>Safety score</td></tr>
|
|
161
|
+
<tr><td><code>--lint</code></td><td>Config analysis</td></tr>
|
|
162
|
+
<tr><td><code>--doctor</code></td><td>Diagnose issues</td></tr>
|
|
163
|
+
<tr><td><code>--watch</code></td><td>Live dashboard</td></tr>
|
|
164
|
+
<tr><td><code>--stats</code></td><td>Block statistics</td></tr>
|
|
165
|
+
<tr><td><code>--verify</code></td><td>Test all hooks</td></tr>
|
|
166
|
+
<tr><td><code>--export</code></td><td>Share with team</td></tr>
|
|
167
|
+
</table>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<div class="col">
|
|
171
|
+
<h2>Common Patterns</h2>
|
|
172
|
+
<table>
|
|
173
|
+
<tr><th>Block</th><th>Pattern</th></tr>
|
|
174
|
+
<tr><td>rm -rf /</td><td><code>rm\s+(-[rf]+\s+)*/</code></td></tr>
|
|
175
|
+
<tr><td>force push</td><td><code>git\s+push.*--force</code></td></tr>
|
|
176
|
+
<tr><td>push main</td><td><code>git\s+push.*main</code></td></tr>
|
|
177
|
+
<tr><td>.env commit</td><td><code>git\s+add.*\.env</code></td></tr>
|
|
178
|
+
<tr><td>git reset</td><td><code>git\s+reset\s+--hard</code></td></tr>
|
|
179
|
+
<tr><td>DB wipe</td><td><code>migrate:fresh|DROP\s+DB</code></td></tr>
|
|
180
|
+
</table>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<p class="footer">cc-safe-setup v3.2.0 · 20 recipes: <a href="https://github.com/yurukusa/claude-code-hooks/blob/main/COOKBOOK.md" class="no-print">COOKBOOK.md</a> · Full ref: <a href="https://github.com/yurukusa/cc-safe-setup/blob/main/SETTINGS_REFERENCE.md" class="no-print">SETTINGS_REFERENCE.md</a></p>
|
|
186
|
+
</body>
|
|
187
|
+
</html>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# tmp-cleanup.sh — Clean up /tmp/claude-*-cwd temp files
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude Code creates /tmp/claude-{hex}-cwd files to track working
|
|
7
|
+
# directory changes but never deletes them. Over time, thousands
|
|
8
|
+
# accumulate.
|
|
9
|
+
#
|
|
10
|
+
# This hook runs on session end and cleans up stale files.
|
|
11
|
+
#
|
|
12
|
+
# GitHub #8856 (67 reactions, 102 comments) — the most reported
|
|
13
|
+
# resource leak in Claude Code.
|
|
14
|
+
#
|
|
15
|
+
# TRIGGER: Stop
|
|
16
|
+
# MATCHER: ""
|
|
17
|
+
#
|
|
18
|
+
# WHAT IT CLEANS:
|
|
19
|
+
# - /tmp/claude-*-cwd (working directory tracking files, ~22 bytes each)
|
|
20
|
+
# - Only files older than 1 hour (to avoid cleaning active sessions)
|
|
21
|
+
#
|
|
22
|
+
# WHAT IT DOES NOT CLEAN:
|
|
23
|
+
# - /tmp/claude-* directories (may be in use by other sessions)
|
|
24
|
+
# - Any non-claude temp files
|
|
25
|
+
# ================================================================
|
|
26
|
+
|
|
27
|
+
# Clean up stale cwd tracking files (older than 60 minutes)
|
|
28
|
+
find /tmp -maxdepth 1 -name 'claude-*-cwd' -type f -mmin +60 -delete 2>/dev/null
|
|
29
|
+
|
|
30
|
+
# Count remaining (for logging)
|
|
31
|
+
REMAINING=$(find /tmp -maxdepth 1 -name 'claude-*-cwd' -type f 2>/dev/null | wc -l)
|
|
32
|
+
if [ "$REMAINING" -gt 100 ]; then
|
|
33
|
+
echo "NOTE: $REMAINING claude-*-cwd files remain in /tmp (active sessions)" >&2
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
exit 0
|
package/index.mjs
CHANGED
|
@@ -79,6 +79,7 @@ const IMPORT_IDX = process.argv.findIndex(a => a === '--import');
|
|
|
79
79
|
const IMPORT_FILE = IMPORT_IDX !== -1 ? process.argv[IMPORT_IDX + 1] : null;
|
|
80
80
|
const STATS = process.argv.includes('--stats');
|
|
81
81
|
const JSON_OUTPUT = process.argv.includes('--json');
|
|
82
|
+
const LINT = process.argv.includes('--lint');
|
|
82
83
|
const CREATE_IDX = process.argv.findIndex(a => a === '--create');
|
|
83
84
|
const CREATE_DESC = CREATE_IDX !== -1 ? process.argv.slice(CREATE_IDX + 1).join(' ') : null;
|
|
84
85
|
|
|
@@ -92,7 +93,7 @@ if (HELP) {
|
|
|
92
93
|
npx cc-safe-setup --verify Test each hook with sample inputs
|
|
93
94
|
npx cc-safe-setup --dry-run Preview without installing
|
|
94
95
|
npx cc-safe-setup --uninstall Remove all installed hooks
|
|
95
|
-
npx cc-safe-setup --examples List
|
|
96
|
+
npx cc-safe-setup --examples List 28 example hooks (5 categories)
|
|
96
97
|
npx cc-safe-setup --install-example <name> Install a specific example
|
|
97
98
|
npx cc-safe-setup --full Complete setup: hooks + scan + audit + badge
|
|
98
99
|
npx cc-safe-setup --audit Safety score (0-100) with fixes
|
|
@@ -100,6 +101,7 @@ if (HELP) {
|
|
|
100
101
|
npx cc-safe-setup --audit --json Machine-readable output for CI/CD
|
|
101
102
|
npx cc-safe-setup --scan Detect tech stack, recommend hooks
|
|
102
103
|
npx cc-safe-setup --learn Learn from your block history
|
|
104
|
+
npx cc-safe-setup --lint Static analysis of hook configuration
|
|
103
105
|
npx cc-safe-setup --doctor Diagnose why hooks aren't working
|
|
104
106
|
npx cc-safe-setup --watch Live dashboard of blocked commands
|
|
105
107
|
npx cc-safe-setup --create "<desc>" Generate a custom hook from description
|
|
@@ -767,6 +769,148 @@ async function fullSetup() {
|
|
|
767
769
|
console.log();
|
|
768
770
|
}
|
|
769
771
|
|
|
772
|
+
async function lint() {
|
|
773
|
+
console.log();
|
|
774
|
+
console.log(c.bold + ' cc-safe-setup --lint' + c.reset);
|
|
775
|
+
console.log(c.dim + ' Static analysis of hook configuration...' + c.reset);
|
|
776
|
+
console.log();
|
|
777
|
+
|
|
778
|
+
if (!existsSync(SETTINGS_PATH)) {
|
|
779
|
+
console.log(c.red + ' No settings.json found.' + c.reset);
|
|
780
|
+
process.exit(1);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
let settings;
|
|
784
|
+
try {
|
|
785
|
+
settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
786
|
+
} catch (e) {
|
|
787
|
+
console.log(c.red + ' settings.json parse error: ' + e.message + c.reset);
|
|
788
|
+
process.exit(1);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
let warnings = 0;
|
|
792
|
+
let errors = 0;
|
|
793
|
+
const warn = (msg) => { console.log(c.yellow + ' WARN: ' + c.reset + msg); warnings++; };
|
|
794
|
+
const error = (msg) => { console.log(c.red + ' ERROR: ' + c.reset + msg); errors++; };
|
|
795
|
+
const info = (msg) => { console.log(c.green + ' OK: ' + c.reset + msg); };
|
|
796
|
+
|
|
797
|
+
const hooks = settings.hooks || {};
|
|
798
|
+
|
|
799
|
+
// 1. Check for duplicate hook commands within same trigger
|
|
800
|
+
for (const [trigger, entries] of Object.entries(hooks)) {
|
|
801
|
+
const commands = [];
|
|
802
|
+
for (const entry of entries) {
|
|
803
|
+
for (const h of (entry.hooks || [])) {
|
|
804
|
+
if (h.command) {
|
|
805
|
+
if (commands.includes(h.command)) {
|
|
806
|
+
warn(trigger + ': duplicate hook "' + h.command.split('/').pop() + '"');
|
|
807
|
+
}
|
|
808
|
+
commands.push(h.command);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
if (commands.length > 0 && new Set(commands).size === commands.length) {
|
|
813
|
+
info(trigger + ': no duplicates (' + commands.length + ' hooks)');
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// 2. Check for empty matcher on PreToolUse (runs on every tool = slow)
|
|
818
|
+
for (const entry of (hooks.PreToolUse || [])) {
|
|
819
|
+
if (!entry.matcher || entry.matcher === '') {
|
|
820
|
+
const hookNames = (entry.hooks || []).map(h => (h.command || '').split('/').pop()).join(', ');
|
|
821
|
+
warn('PreToolUse hook with empty matcher runs on EVERY tool call: ' + hookNames);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// 3. Check for empty matcher on PostToolUse with heavy scripts
|
|
826
|
+
for (const entry of (hooks.PostToolUse || [])) {
|
|
827
|
+
if (!entry.matcher || entry.matcher === '') {
|
|
828
|
+
const hookNames = (entry.hooks || []).map(h => (h.command || '').split('/').pop()).join(', ');
|
|
829
|
+
// Check if any of these scripts are large (>5KB = potentially slow)
|
|
830
|
+
for (const h of (entry.hooks || [])) {
|
|
831
|
+
if (h.command) {
|
|
832
|
+
const resolved = h.command.replace(/^(bash|sh|node)\s+/, '').split(/\s+/)[0].replace(/^~/, HOME);
|
|
833
|
+
try {
|
|
834
|
+
const { statSync } = await import('fs');
|
|
835
|
+
const size = statSync(resolved).size;
|
|
836
|
+
if (size > 5000) {
|
|
837
|
+
warn('PostToolUse empty matcher + large script (' + (size/1024).toFixed(1) + 'KB): ' + resolved.split('/').pop());
|
|
838
|
+
}
|
|
839
|
+
} catch {}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// 4. Check for hooks that exist in settings but script is missing
|
|
846
|
+
for (const [trigger, entries] of Object.entries(hooks)) {
|
|
847
|
+
for (const entry of entries) {
|
|
848
|
+
for (const h of (entry.hooks || [])) {
|
|
849
|
+
if (h.command) {
|
|
850
|
+
let scriptPath = h.command.replace(/^(bash|sh|node|python3?)\s+/, '').split(/\s+/)[0];
|
|
851
|
+
scriptPath = scriptPath.replace(/^~/, HOME);
|
|
852
|
+
if (!existsSync(scriptPath)) {
|
|
853
|
+
error(trigger + ': missing script "' + scriptPath.split('/').pop() + '"');
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// 5. Check for overly broad allow rules combined with no hooks
|
|
861
|
+
const allows = settings.permissions?.allow || [];
|
|
862
|
+
if (allows.includes('Bash(*)') && (hooks.PreToolUse || []).length === 0) {
|
|
863
|
+
error('Bash(*) in allow list with no PreToolUse hooks = no safety net');
|
|
864
|
+
} else if (allows.includes('Bash(*)') && (hooks.PreToolUse || []).length > 0) {
|
|
865
|
+
info('Bash(*) with PreToolUse hooks = hooks provide safety');
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// 6. Check for conflicting allow and deny
|
|
869
|
+
const denies = settings.permissions?.deny || [];
|
|
870
|
+
for (const d of denies) {
|
|
871
|
+
if (allows.includes(d)) {
|
|
872
|
+
warn('Same pattern in both allow and deny: ' + d);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// 7. Check total hook count and warn about performance
|
|
877
|
+
let totalHooks = 0;
|
|
878
|
+
for (const entries of Object.values(hooks)) {
|
|
879
|
+
for (const entry of entries) {
|
|
880
|
+
totalHooks += (entry.hooks || []).length;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
if (totalHooks > 20) {
|
|
884
|
+
warn(totalHooks + ' total hooks registered — may slow down tool calls');
|
|
885
|
+
} else if (totalHooks > 0) {
|
|
886
|
+
info(totalHooks + ' hooks registered');
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// 8. Check for hooks without type: "command"
|
|
890
|
+
for (const [trigger, entries] of Object.entries(hooks)) {
|
|
891
|
+
for (const entry of entries) {
|
|
892
|
+
for (const h of (entry.hooks || [])) {
|
|
893
|
+
if (h.type !== 'command') {
|
|
894
|
+
warn(trigger + ': hook with type "' + h.type + '" (only "command" is supported)');
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Summary
|
|
901
|
+
console.log();
|
|
902
|
+
if (errors === 0 && warnings === 0) {
|
|
903
|
+
console.log(c.bold + c.green + ' Clean. No issues found.' + c.reset);
|
|
904
|
+
} else if (errors === 0) {
|
|
905
|
+
console.log(c.bold + c.yellow + ' ' + warnings + ' warning(s). No errors.' + c.reset);
|
|
906
|
+
} else {
|
|
907
|
+
console.log(c.bold + c.red + ' ' + errors + ' error(s), ' + warnings + ' warning(s).' + c.reset);
|
|
908
|
+
}
|
|
909
|
+
console.log();
|
|
910
|
+
|
|
911
|
+
process.exit(errors > 0 ? 1 : 0);
|
|
912
|
+
}
|
|
913
|
+
|
|
770
914
|
async function createHook(description) {
|
|
771
915
|
console.log();
|
|
772
916
|
console.log(c.bold + ' cc-safe-setup --create' + c.reset);
|
|
@@ -1688,6 +1832,7 @@ async function main() {
|
|
|
1688
1832
|
if (FULL) return fullSetup();
|
|
1689
1833
|
if (DOCTOR) return doctor();
|
|
1690
1834
|
if (WATCH) return watch();
|
|
1835
|
+
if (LINT) return lint();
|
|
1691
1836
|
if (CREATE_DESC) return createHook(CREATE_DESC);
|
|
1692
1837
|
if (STATS) return stats();
|
|
1693
1838
|
if (EXPORT) return exportConfig();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "3.1
|
|
3
|
+
"version": "3.2.1",
|
|
4
4
|
"description": "One command to make Claude Code safe for autonomous operation. 8 built-in hooks + 27 installable examples. Destructive blocker, branch guard, compound command approver, database wipe protection, and more.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"bin": {
|