cc-safe-setup 3.0.1 → 3.2.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
@@ -214,11 +214,16 @@ Or browse all available examples in [`examples/`](examples/):
214
214
  - **todo-check.sh** — Warn when committing files with TODO/FIXME/HACK markers
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
+ - **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))
217
218
 
218
219
  ## Safety Checklist
219
220
 
220
221
  **[SAFETY_CHECKLIST.md](SAFETY_CHECKLIST.md)** — Copy-paste checklist for before/during/after autonomous sessions.
221
222
 
223
+ ## settings.json Reference
224
+
225
+ **[SETTINGS_REFERENCE.md](SETTINGS_REFERENCE.md)** — Complete reference for permissions, hooks, modes, and common configurations. Includes known limitations and workarounds.
226
+
222
227
  ## Migration Guide
223
228
 
224
229
  **[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.
@@ -229,6 +234,7 @@ Or browse all available examples in [`examples/`](examples/):
229
234
  - [Hooks Cookbook](https://github.com/yurukusa/claude-code-hooks/blob/main/COOKBOOK.md) — 19 ready-to-use recipes from real GitHub Issues
230
235
  - [Japanese guide (Qiita)](https://qiita.com/yurukusa/items/a9714b33f5d974e8f1e8) — この記事の日本語解説
231
236
  - [Hook Test Runner](https://github.com/yurukusa/cc-hook-test) — `npx cc-hook-test <hook.sh>` to auto-test any hook
237
+ - [Ecosystem Comparison](https://yurukusa.github.io/cc-safe-setup/ecosystem.html) — all Claude Code hook projects compared
232
238
  - [The incident that inspired this tool](https://github.com/anthropics/claude-code/issues/36339) — NTFS junction rm -rf
233
239
 
234
240
  ## FAQ
@@ -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
@@ -88,7 +88,7 @@ a { color: #58a6ff; text-decoration: none; }
88
88
  <div class="tab-content" id="tab-fix"></div>
89
89
  <div class="tab-content" id="tab-manual"></div>
90
90
 
91
- <p class="privacy">100% client-side. Your settings never leave this page. <a href="https://github.com/yurukusa/cc-safe-setup">Source</a> · <a href="https://www.npmjs.com/package/cc-safe-setup">npm</a></p>
91
+ <p class="privacy">100% client-side. Your settings never leave this page. <a href="https://github.com/yurukusa/cc-safe-setup">Source</a> · <a href="https://www.npmjs.com/package/cc-safe-setup">npm</a> · <a href="ecosystem.html">Compare all hook projects</a></p>
92
92
  </div>
93
93
 
94
94
  <script>
@@ -0,0 +1,223 @@
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 Ecosystem</title>
7
+ <meta name="description" content="Compare all Claude Code hook projects — features, language, stars, and installation methods.">
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: 2rem; }
11
+ .container { max-width: 900px; margin: 0 auto; }
12
+ h1 { font-size: 1.5rem; margin-bottom: 0.5rem; color: #f0f6fc; }
13
+ h2 { font-size: 1.2rem; margin: 2rem 0 1rem; color: #f0f6fc; }
14
+ .subtitle { color: #8b949e; margin-bottom: 2rem; }
15
+ a { color: #58a6ff; text-decoration: none; }
16
+ a:hover { text-decoration: underline; }
17
+ table { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: 0.85rem; }
18
+ th { text-align: left; padding: 0.6rem 0.5rem; border-bottom: 2px solid #30363d; color: #f0f6fc; font-weight: 600; }
19
+ td { padding: 0.5rem; border-bottom: 1px solid #21262d; vertical-align: top; }
20
+ tr:hover td { background: #161b22; }
21
+ .badge { display: inline-block; padding: 0.15rem 0.4rem; border-radius: 3px; font-size: 0.7rem; font-weight: bold; margin-right: 0.3rem; }
22
+ .badge-bash { background: #1f6feb22; color: #58a6ff; border: 1px solid #1f6feb44; }
23
+ .badge-js { background: #f0e68c22; color: #f0e68c; border: 1px solid #f0e68c44; }
24
+ .badge-py { background: #3fb95022; color: #3fb950; border: 1px solid #3fb95044; }
25
+ .badge-ts { background: #388bfd22; color: #388bfd; border: 1px solid #388bfd44; }
26
+ .check { color: #3fb950; }
27
+ .cross { color: #484f58; }
28
+ .note { color: #8b949e; font-size: 0.8rem; margin-top: 2rem; }
29
+ .footer { color: #484f58; font-size: 0.75rem; margin-top: 3rem; text-align: center; }
30
+ </style>
31
+ </head>
32
+ <body>
33
+ <div class="container">
34
+ <h1>Claude Code Hooks Ecosystem</h1>
35
+ <p class="subtitle">A neutral comparison of all major hook projects. Pick the one that fits your stack.</p>
36
+
37
+ <h2>Projects at a Glance</h2>
38
+ <table>
39
+ <tr>
40
+ <th>Project</th>
41
+ <th>Language</th>
42
+ <th>Hooks</th>
43
+ <th>Install</th>
44
+ <th>Focus</th>
45
+ </tr>
46
+ <tr>
47
+ <td><a href="https://github.com/kenryu42/claude-code-safety-net">safety-net</a></td>
48
+ <td><span class="badge badge-ts">TS</span></td>
49
+ <td>5 rules</td>
50
+ <td>npx</td>
51
+ <td>Destructive command blocking with configurable severity</td>
52
+ </tr>
53
+ <tr>
54
+ <td><a href="https://github.com/yurukusa/cc-safe-setup">cc-safe-setup</a></td>
55
+ <td><span class="badge badge-bash">Bash</span></td>
56
+ <td>8 + 26 examples</td>
57
+ <td>npx</td>
58
+ <td>Full safety suite: block, audit, create, learn, watch, doctor</td>
59
+ </tr>
60
+ <tr>
61
+ <td><a href="https://github.com/karanb192/claude-code-hooks">karanb192/hooks</a></td>
62
+ <td><span class="badge badge-js">JS</span></td>
63
+ <td>5+</td>
64
+ <td>copy files</td>
65
+ <td>JS hooks with configurable safety levels and logging</td>
66
+ </tr>
67
+ <tr>
68
+ <td><a href="https://github.com/disler/claude-code-hooks-mastery">hooks-mastery</a></td>
69
+ <td><span class="badge badge-py">Python</span></td>
70
+ <td>12</td>
71
+ <td>copy files</td>
72
+ <td>Python hooks covering all hook events + LLM integration</td>
73
+ </tr>
74
+ <tr>
75
+ <td><a href="https://github.com/lasso-security/claude-hooks">lasso-security</a></td>
76
+ <td><span class="badge badge-py">Python</span></td>
77
+ <td>1 skill</td>
78
+ <td>install.sh</td>
79
+ <td>Prompt injection defense using YAML pattern matching</td>
80
+ </tr>
81
+ <tr>
82
+ <td><a href="https://github.com/johnlindquist/claude-hooks">johnlindquist</a></td>
83
+ <td><span class="badge badge-ts">TS</span></td>
84
+ <td>8</td>
85
+ <td>Bun</td>
86
+ <td>TypeScript hooks with Bun runtime, notification focus</td>
87
+ </tr>
88
+ <tr>
89
+ <td><a href="https://github.com/yurukusa/claude-code-hooks">claude-code-hooks</a></td>
90
+ <td><span class="badge badge-bash">Bash</span></td>
91
+ <td>16 + 5 templates</td>
92
+ <td>install.sh</td>
93
+ <td>Production hooks from 1000+ hours autonomous operation</td>
94
+ </tr>
95
+ </table>
96
+
97
+ <h2>Feature Comparison</h2>
98
+ <table>
99
+ <tr>
100
+ <th>Feature</th>
101
+ <th>safety-net</th>
102
+ <th>cc-safe-setup</th>
103
+ <th>karanb192</th>
104
+ <th>mastery</th>
105
+ <th>lasso</th>
106
+ </tr>
107
+ <tr>
108
+ <td>rm -rf blocker</td>
109
+ <td class="check">&#10003;</td>
110
+ <td class="check">&#10003;</td>
111
+ <td class="check">&#10003;</td>
112
+ <td class="check">&#10003;</td>
113
+ <td class="cross">-</td>
114
+ </tr>
115
+ <tr>
116
+ <td>Branch push guard</td>
117
+ <td class="check">&#10003;</td>
118
+ <td class="check">&#10003;</td>
119
+ <td class="cross">-</td>
120
+ <td class="cross">-</td>
121
+ <td class="cross">-</td>
122
+ </tr>
123
+ <tr>
124
+ <td>Secret leak prevention</td>
125
+ <td class="cross">-</td>
126
+ <td class="check">&#10003;</td>
127
+ <td class="check">&#10003;</td>
128
+ <td class="cross">-</td>
129
+ <td class="cross">-</td>
130
+ </tr>
131
+ <tr>
132
+ <td>Syntax validation</td>
133
+ <td class="cross">-</td>
134
+ <td class="check">&#10003;</td>
135
+ <td class="cross">-</td>
136
+ <td class="cross">-</td>
137
+ <td class="cross">-</td>
138
+ </tr>
139
+ <tr>
140
+ <td>Context monitor</td>
141
+ <td class="cross">-</td>
142
+ <td class="check">&#10003;</td>
143
+ <td class="cross">-</td>
144
+ <td class="cross">-</td>
145
+ <td class="cross">-</td>
146
+ </tr>
147
+ <tr>
148
+ <td>Prompt injection defense</td>
149
+ <td class="cross">-</td>
150
+ <td class="cross">-</td>
151
+ <td class="cross">-</td>
152
+ <td class="cross">-</td>
153
+ <td class="check">&#10003;</td>
154
+ </tr>
155
+ <tr>
156
+ <td>Auto-approve safe cmds</td>
157
+ <td class="cross">-</td>
158
+ <td class="check">&#10003;</td>
159
+ <td class="cross">-</td>
160
+ <td class="cross">-</td>
161
+ <td class="cross">-</td>
162
+ </tr>
163
+ <tr>
164
+ <td>Database wipe guard</td>
165
+ <td class="cross">-</td>
166
+ <td class="check">&#10003;</td>
167
+ <td class="cross">-</td>
168
+ <td class="check">&#10003;</td>
169
+ <td class="cross">-</td>
170
+ </tr>
171
+ <tr>
172
+ <td>Safety audit/score</td>
173
+ <td class="cross">-</td>
174
+ <td class="check">&#10003;</td>
175
+ <td class="cross">-</td>
176
+ <td class="cross">-</td>
177
+ <td class="cross">-</td>
178
+ </tr>
179
+ <tr>
180
+ <td>Hook generator (NL)</td>
181
+ <td class="cross">-</td>
182
+ <td class="check">&#10003;</td>
183
+ <td class="cross">-</td>
184
+ <td class="cross">-</td>
185
+ <td class="cross">-</td>
186
+ </tr>
187
+ <tr>
188
+ <td>Zero dependencies</td>
189
+ <td class="cross">TS</td>
190
+ <td class="check">jq only</td>
191
+ <td class="check">Node</td>
192
+ <td class="cross">uv/pip</td>
193
+ <td class="cross">uv</td>
194
+ </tr>
195
+ <tr>
196
+ <td>GitHub Action</td>
197
+ <td class="cross">-</td>
198
+ <td class="check">&#10003;</td>
199
+ <td class="cross">-</td>
200
+ <td class="cross">-</td>
201
+ <td class="cross">-</td>
202
+ </tr>
203
+ </table>
204
+
205
+ <p class="note">This page is maintained by the cc-safe-setup team but aims to be neutral. PRs welcome for corrections: <a href="https://github.com/yurukusa/cc-safe-setup">GitHub</a></p>
206
+
207
+ <h2>Which Should I Use?</h2>
208
+ <table>
209
+ <tr><th>If you want...</th><th>Use this</th></tr>
210
+ <tr><td>Quickest setup, bash-based</td><td><code>npx cc-safe-setup</code></td></tr>
211
+ <tr><td>TypeScript hooks, configurable severity</td><td>safety-net</td></tr>
212
+ <tr><td>Python hooks, all event types</td><td>hooks-mastery</td></tr>
213
+ <tr><td>Prompt injection protection</td><td>lasso-security</td></tr>
214
+ <tr><td>JS hooks with logging</td><td>karanb192</td></tr>
215
+ <tr><td>Complete ops toolkit (hooks + templates)</td><td>claude-code-hooks</td></tr>
216
+ </table>
217
+
218
+ <div class="footer">
219
+ Last updated: March 2026 · <a href="https://yurukusa.github.io/cc-safe-setup/">Safety Audit Tool</a> · <a href="https://github.com/yurukusa/cc-safe-setup">Source</a>
220
+ </div>
221
+ </div>
222
+ </body>
223
+ </html>
package/docs/index.html CHANGED
@@ -88,7 +88,7 @@ a { color: #58a6ff; text-decoration: none; }
88
88
  <div class="tab-content" id="tab-fix"></div>
89
89
  <div class="tab-content" id="tab-manual"></div>
90
90
 
91
- <p class="privacy">100% client-side. Your settings never leave this page. <a href="https://github.com/yurukusa/cc-safe-setup">Source</a> · <a href="https://www.npmjs.com/package/cc-safe-setup">npm</a></p>
91
+ <p class="privacy">100% client-side. Your settings never leave this page. <a href="https://github.com/yurukusa/cc-safe-setup">Source</a> · <a href="https://www.npmjs.com/package/cc-safe-setup">npm</a> · <a href="ecosystem.html">Compare all hook projects</a></p>
92
92
  </div>
93
93
 
94
94
  <script>
@@ -0,0 +1,117 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # compound-command-approver.sh — Auto-approve safe compound commands
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # Claude Code's permission system doesn't match compound commands.
7
+ # `Bash(git:*)` doesn't match `cd /path && git log`.
8
+ # `Bash(npm:*)` doesn't match `cd project && npm test`.
9
+ #
10
+ # This hook parses compound commands (&&, ||, ;) and auto-approves
11
+ # when ALL components are in the safe list.
12
+ #
13
+ # Solves the #1 most-reacted permission issue:
14
+ # GitHub #30519 (53 reactions) — "Permissions matching is broken"
15
+ # GitHub #16561 (101 reactions) — "Parse compound Bash commands"
16
+ #
17
+ # TRIGGER: PreToolUse
18
+ # MATCHER: "Bash"
19
+ #
20
+ # HOW IT WORKS:
21
+ # 1. Splits command on &&, ||, ;
22
+ # 2. Checks each component against safe patterns
23
+ # 3. If ALL components are safe → auto-approve
24
+ # 4. If ANY component is unknown → pass through (no opinion)
25
+ #
26
+ # SAFE PATTERNS (configurable via CC_SAFE_COMMANDS):
27
+ # - cd, ls, pwd, echo, cat, head, tail, wc, sort, uniq, grep
28
+ # - git (read-only: status, log, diff, branch, show, rev-parse, tag)
29
+ # - npm/yarn/pnpm (read-only: test, run, list, outdated, audit)
30
+ # - python/python3 (test: pytest, -m pytest, -m py_compile)
31
+ # - cargo test, go test, make test
32
+ #
33
+ # WHAT IT DOES NOT APPROVE:
34
+ # - git push, git reset, git clean (handled by other guards)
35
+ # - rm, sudo, chmod (handled by destructive-guard)
36
+ # - Any command not in the safe list
37
+ # ================================================================
38
+
39
+ INPUT=$(cat)
40
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
41
+
42
+ if [[ -z "$COMMAND" ]]; then
43
+ exit 0
44
+ fi
45
+
46
+ # Only handle compound commands
47
+ if ! echo "$COMMAND" | grep -qE '&&|\|\||;'; then
48
+ exit 0
49
+ fi
50
+
51
+ # Split on &&, ||, ; and trim each component
52
+ IFS=$'\n' read -r -d '' -a PARTS < <(echo "$COMMAND" | sed 's/&&/\n/g; s/||/\n/g; s/;/\n/g' && printf '\0')
53
+
54
+ # Safe command patterns
55
+ SAFE_PATTERNS=(
56
+ # Navigation/info
57
+ '^\s*cd\s'
58
+ '^\s*ls(\s|$)'
59
+ '^\s*pwd\s*$'
60
+ '^\s*echo\s'
61
+ '^\s*cat\s'
62
+ '^\s*head\s'
63
+ '^\s*tail\s'
64
+ '^\s*wc\s'
65
+ '^\s*sort(\s|$)'
66
+ '^\s*uniq(\s|$)'
67
+ '^\s*grep\s'
68
+ '^\s*find\s.*-name'
69
+ '^\s*test\s'
70
+ '^\s*\[\s'
71
+ '^\s*true\s*$'
72
+ '^\s*false\s*$'
73
+ '^\s*mkdir\s+-p\s'
74
+ # Git read-only
75
+ '^\s*git\s+(status|log|diff|branch|show|rev-parse|tag|remote|stash\s+list|describe|name-rev|ls-files|ls-tree|shortlog|blame|reflog)(\s|$)'
76
+ '^\s*git\s+-C\s+\S+\s+(status|log|diff|branch|show|rev-parse)(\s|$)'
77
+ '^\s*git\s+add\s'
78
+ '^\s*git\s+commit\s'
79
+ # npm/yarn/pnpm read + test
80
+ '^\s*(npm|yarn|pnpm)\s+(test|run|list|outdated|audit|info|view|pack|version)(\s|$)'
81
+ '^\s*npx\s'
82
+ # Python
83
+ '^\s*(python3?|pytest)\s'
84
+ # Build/test tools
85
+ '^\s*(cargo|go|make|gradle|mvn)\s+(test|build|check|verify|compile)(\s|$)'
86
+ '^\s*(ruff|mypy|flake8|pylint|black|isort)\s'
87
+ '^\s*(eslint|prettier|tsc)\s'
88
+ # Docker read-only
89
+ '^\s*docker\s+(ps|images|logs|inspect|stats|version)(\s|$)'
90
+ )
91
+
92
+ ALL_SAFE=1
93
+ for part in "${PARTS[@]}"; do
94
+ part=$(echo "$part" | sed 's/^\s*//; s/\s*$//')
95
+ [[ -z "$part" ]] && continue
96
+
97
+ PART_SAFE=0
98
+ for pattern in "${SAFE_PATTERNS[@]}"; do
99
+ if echo "$part" | grep -qE "$pattern"; then
100
+ PART_SAFE=1
101
+ break
102
+ fi
103
+ done
104
+
105
+ if (( PART_SAFE == 0 )); then
106
+ ALL_SAFE=0
107
+ break
108
+ fi
109
+ done
110
+
111
+ if (( ALL_SAFE == 1 )); then
112
+ jq -n '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"compound command auto-approved (all components safe)"}}'
113
+ exit 0
114
+ fi
115
+
116
+ # Unknown component — let normal permission flow handle it
117
+ 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 25 example hooks (5 categories)
96
+ npx cc-safe-setup --examples List 27 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,7 +1,7 @@
1
1
  {
2
2
  "name": "cc-safe-setup",
3
- "version": "3.0.1",
4
- "description": "One command to make Claude Code safe for autonomous operation. 8 built-in hooks + 26 installable examples. Destructive blocker, branch guard, database wipe protection, case-insensitive FS guard, and more.",
3
+ "version": "3.2.0",
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": {
7
7
  "cc-safe-setup": "index.mjs"