cc-safe-setup 8.7.0 → 8.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/settings-reference.html +317 -0
- package/examples/git-blame-context.sh +35 -0
- package/examples/import-cycle-warn.sh +48 -0
- package/index.mjs +89 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**One command to make Claude Code safe for autonomous operation.** [日本語](docs/README.ja.md)
|
|
8
8
|
|
|
9
|
-
8 built-in +
|
|
9
|
+
8 built-in + 102 examples = **110 hooks**. 34 CLI commands. 457 tests. 4 languages. [Web Tool](https://yurukusa.github.io/cc-safe-setup/) · [Cheat Sheet](https://yurukusa.github.io/cc-safe-setup/hooks-cheatsheet.html) · [Builder](https://yurukusa.github.io/cc-safe-setup/builder.html) · [FAQ](https://yurukusa.github.io/cc-safe-setup/faq.html) · [Examples](https://yurukusa.github.io/cc-safe-setup/by-example.html) · [Migration](https://yurukusa.github.io/cc-safe-setup/migration-guide.html) · [Playground](https://yurukusa.github.io/cc-hook-registry/playground.html)
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npx cc-safe-setup
|
|
@@ -0,0 +1,317 @@
|
|
|
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 settings.json Complete Reference</title>
|
|
7
|
+
<meta name="description" content="Every settings.json field explained with examples. Hooks, permissions, env vars — the definitive reference.">
|
|
8
|
+
<style>
|
|
9
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
10
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0d1117;color:#c9d1d9;padding:1.5rem;line-height:1.6}
|
|
11
|
+
.c{max-width:820px;margin:0 auto}
|
|
12
|
+
h1{color:#f0f6fc;font-size:1.4rem;margin-bottom:.3rem}
|
|
13
|
+
h2{color:#f0f6fc;font-size:1.05rem;margin:1.3rem 0 .4rem;padding-bottom:.3rem;border-bottom:1px solid #21262d}
|
|
14
|
+
h3{color:#c9d1d9;font-size:.88rem;margin:.7rem 0 .2rem}
|
|
15
|
+
.sub{color:#8b949e;font-size:.83rem;margin-bottom:1rem}
|
|
16
|
+
a{color:#58a6ff;text-decoration:none}
|
|
17
|
+
code{background:#161b22;padding:.12rem .25rem;border-radius:3px;font-size:.82rem}
|
|
18
|
+
pre{background:#161b22;border:1px solid #30363d;border-radius:6px;padding:.6rem;font-size:.78rem;color:#e6edf3;overflow-x:auto;margin:.3rem 0;position:relative}
|
|
19
|
+
.copy{position:absolute;top:.3rem;right:.3rem;background:#21262d;border:1px solid #30363d;color:#8b949e;padding:.15rem .4rem;border-radius:3px;cursor:pointer;font-size:.65rem}
|
|
20
|
+
.copy:hover{color:#f0f6fc}
|
|
21
|
+
table{width:100%;border-collapse:collapse;margin:.4rem 0;font-size:.8rem}
|
|
22
|
+
th,td{padding:.35rem .5rem;border:1px solid #21262d;text-align:left}
|
|
23
|
+
th{background:#161b22;color:#f0f6fc}
|
|
24
|
+
.note{background:#161b22;border-left:3px solid #58a6ff;padding:.4rem .7rem;margin:.4rem 0;font-size:.8rem;color:#8b949e}
|
|
25
|
+
.warn{border-left-color:#d29922}
|
|
26
|
+
.field{background:#161b22;border:1px solid #30363d;border-radius:6px;padding:.6rem;margin:.4rem 0}
|
|
27
|
+
.field-name{font-weight:700;color:#f0f6fc;font-family:monospace;font-size:.85rem}
|
|
28
|
+
.field-type{color:#d29922;font-size:.7rem;margin-left:.4rem}
|
|
29
|
+
.field-desc{color:#8b949e;font-size:.8rem;margin:.2rem 0}
|
|
30
|
+
.footer{text-align:center;color:#484f58;font-size:.7rem;margin-top:2rem;padding-top:1rem;border-top:1px solid #21262d}
|
|
31
|
+
.toc{columns:2;font-size:.8rem;margin:.5rem 0}
|
|
32
|
+
.toc a{display:block;padding:.15rem 0}
|
|
33
|
+
</style>
|
|
34
|
+
</head>
|
|
35
|
+
<body>
|
|
36
|
+
<div class="c">
|
|
37
|
+
|
|
38
|
+
<h1>settings.json Reference</h1>
|
|
39
|
+
<p class="sub">Every field, every option, with practical examples. <a href="https://docs.anthropic.com/en/docs/claude-code/settings">Official docs</a></p>
|
|
40
|
+
|
|
41
|
+
<div class="toc">
|
|
42
|
+
<a href="#locations">File Locations</a>
|
|
43
|
+
<a href="#hooks">hooks</a>
|
|
44
|
+
<a href="#permissions">permissions</a>
|
|
45
|
+
<a href="#env">env</a>
|
|
46
|
+
<a href="#model">model</a>
|
|
47
|
+
<a href="#projects">projects</a>
|
|
48
|
+
<a href="#examples">Full Examples</a>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<h2 id="locations">File Locations</h2>
|
|
52
|
+
|
|
53
|
+
<table>
|
|
54
|
+
<tr><th>File</th><th>Scope</th><th>Priority</th></tr>
|
|
55
|
+
<tr><td><code>~/.claude/settings.json</code></td><td>Global (all projects)</td><td>Lowest</td></tr>
|
|
56
|
+
<tr><td><code>.claude/settings.json</code></td><td>Project (in repo)</td><td>Medium</td></tr>
|
|
57
|
+
<tr><td><code>.claude/settings.local.json</code></td><td>Project local (gitignored)</td><td>Highest</td></tr>
|
|
58
|
+
</table>
|
|
59
|
+
|
|
60
|
+
<div class="note">
|
|
61
|
+
Project settings merge with global. Higher priority files override lower ones for the same key. For hooks, they are <strong>combined</strong>, not replaced.
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<h2 id="hooks">hooks</h2>
|
|
65
|
+
|
|
66
|
+
<div class="field">
|
|
67
|
+
<span class="field-name">hooks</span> <span class="field-type">object</span>
|
|
68
|
+
<div class="field-desc">Shell scripts that run at lifecycle points. The core of safety configuration.</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<h3>Structure</h3>
|
|
72
|
+
<pre><code>{
|
|
73
|
+
"hooks": {
|
|
74
|
+
"EVENT": [
|
|
75
|
+
{
|
|
76
|
+
"matcher": "REGEX",
|
|
77
|
+
"hooks": [
|
|
78
|
+
{
|
|
79
|
+
"type": "command",
|
|
80
|
+
"command": "bash path/to/hook.sh"
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
}</code><button class="copy" onclick="cp(this)">Copy</button></pre>
|
|
87
|
+
|
|
88
|
+
<h3>Events</h3>
|
|
89
|
+
<table>
|
|
90
|
+
<tr><th>Event</th><th>When</th><th>stdin JSON</th></tr>
|
|
91
|
+
<tr><td><code>PreToolUse</code></td><td>Before tool executes</td><td><code>tool_name</code>, <code>tool_input</code></td></tr>
|
|
92
|
+
<tr><td><code>PostToolUse</code></td><td>After tool completes</td><td><code>tool_name</code>, <code>tool_input</code>, <code>tool_result</code></td></tr>
|
|
93
|
+
<tr><td><code>Stop</code></td><td>Claude finishes responding</td><td><code>stop_reason</code></td></tr>
|
|
94
|
+
<tr><td><code>SubagentStop</code></td><td>Subagent finishes</td><td>Subagent context</td></tr>
|
|
95
|
+
<tr><td><code>UserPromptSubmit</code></td><td>User sends a message</td><td>Prompt content</td></tr>
|
|
96
|
+
</table>
|
|
97
|
+
|
|
98
|
+
<h3>Matcher</h3>
|
|
99
|
+
<table>
|
|
100
|
+
<tr><th>Value</th><th>Matches</th></tr>
|
|
101
|
+
<tr><td><code>""</code></td><td>All tools</td></tr>
|
|
102
|
+
<tr><td><code>"Bash"</code></td><td>Shell commands only</td></tr>
|
|
103
|
+
<tr><td><code>"Edit"</code></td><td>File edits only</td></tr>
|
|
104
|
+
<tr><td><code>"Write"</code></td><td>File writes only</td></tr>
|
|
105
|
+
<tr><td><code>"Read"</code></td><td>File reads only</td></tr>
|
|
106
|
+
<tr><td><code>"Edit|Write"</code></td><td>Any file modification</td></tr>
|
|
107
|
+
<tr><td><code>"Bash|Edit|Write"</code></td><td>Multiple tools (regex OR)</td></tr>
|
|
108
|
+
</table>
|
|
109
|
+
|
|
110
|
+
<h3>Exit Codes</h3>
|
|
111
|
+
<table>
|
|
112
|
+
<tr><th>Code</th><th>Effect</th></tr>
|
|
113
|
+
<tr><td><code>0</code></td><td>Allow (default)</td></tr>
|
|
114
|
+
<tr><td><code>2</code></td><td>Block the action</td></tr>
|
|
115
|
+
<tr><td><code>1</code></td><td>Hook error (ignored, action proceeds)</td></tr>
|
|
116
|
+
</table>
|
|
117
|
+
|
|
118
|
+
<h3>stdout Override</h3>
|
|
119
|
+
<pre><code>// Auto-approve
|
|
120
|
+
echo '{"decision":"approve","reason":"Safe command"}'
|
|
121
|
+
|
|
122
|
+
// Block with reason
|
|
123
|
+
echo '{"decision":"block","reason":"Too dangerous"}'</code></pre>
|
|
124
|
+
|
|
125
|
+
<h3>Practical Example</h3>
|
|
126
|
+
<pre><code>{
|
|
127
|
+
"hooks": {
|
|
128
|
+
"PreToolUse": [
|
|
129
|
+
{
|
|
130
|
+
"matcher": "Bash",
|
|
131
|
+
"hooks": [
|
|
132
|
+
{"type": "command", "command": "bash ~/.claude/hooks/destructive-guard.sh"},
|
|
133
|
+
{"type": "command", "command": "bash ~/.claude/hooks/branch-guard.sh"},
|
|
134
|
+
{"type": "command", "command": "bash ~/.claude/hooks/secret-guard.sh"}
|
|
135
|
+
]
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"matcher": "Edit|Write",
|
|
139
|
+
"hooks": [
|
|
140
|
+
{"type": "command", "command": "bash ~/.claude/hooks/scope-guard.sh"}
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
"PostToolUse": [
|
|
145
|
+
{
|
|
146
|
+
"matcher": "Edit|Write",
|
|
147
|
+
"hooks": [
|
|
148
|
+
{"type": "command", "command": "bash ~/.claude/hooks/syntax-check.sh"}
|
|
149
|
+
]
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"matcher": "",
|
|
153
|
+
"hooks": [
|
|
154
|
+
{"type": "command", "command": "bash ~/.claude/hooks/context-monitor.sh"}
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
],
|
|
158
|
+
"Stop": [
|
|
159
|
+
{
|
|
160
|
+
"matcher": "",
|
|
161
|
+
"hooks": [
|
|
162
|
+
{"type": "command", "command": "bash ~/.claude/hooks/api-error-alert.sh"}
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
}</code><button class="copy" onclick="cp(this)">Copy</button></pre>
|
|
168
|
+
|
|
169
|
+
<h2 id="permissions">permissions</h2>
|
|
170
|
+
|
|
171
|
+
<div class="field">
|
|
172
|
+
<span class="field-name">permissions</span> <span class="field-type">object</span>
|
|
173
|
+
<div class="field-desc">Allow/deny rules for tool usage without prompting.</div>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<pre><code>{
|
|
177
|
+
"permissions": {
|
|
178
|
+
"allow": [
|
|
179
|
+
"Bash(npm test)",
|
|
180
|
+
"Bash(git status)",
|
|
181
|
+
"Bash(git log *)",
|
|
182
|
+
"Read(*)",
|
|
183
|
+
"Bash(ls *)"
|
|
184
|
+
],
|
|
185
|
+
"deny": [
|
|
186
|
+
"Bash(rm -rf *)",
|
|
187
|
+
"Bash(sudo *)",
|
|
188
|
+
"Bash(git push * --force)"
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
}</code><button class="copy" onclick="cp(this)">Copy</button></pre>
|
|
192
|
+
|
|
193
|
+
<div class="note warn">
|
|
194
|
+
<strong>Gotcha:</strong> Permissions use glob patterns, not regex. <code>*</code> matches anything. Claude adds flags like <code>-C /path</code> that can break exact matches — use hooks for reliable blocking.
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<h2 id="env">env</h2>
|
|
198
|
+
|
|
199
|
+
<div class="field">
|
|
200
|
+
<span class="field-name">env</span> <span class="field-type">object</span>
|
|
201
|
+
<div class="field-desc">Environment variables passed to Claude Code and hooks.</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<pre><code>{
|
|
205
|
+
"env": {
|
|
206
|
+
"CC_TOKEN_BUDGET": "20",
|
|
207
|
+
"CC_MAX_LINE_LENGTH": "120",
|
|
208
|
+
"CC_DISK_WARN_PCT": "90",
|
|
209
|
+
"CC_ALLOWLIST_FILE": "~/.claude/allowlist.txt"
|
|
210
|
+
}
|
|
211
|
+
}</code><button class="copy" onclick="cp(this)">Copy</button></pre>
|
|
212
|
+
|
|
213
|
+
<h2 id="model">model</h2>
|
|
214
|
+
|
|
215
|
+
<div class="field">
|
|
216
|
+
<span class="field-name">model</span> <span class="field-type">string</span>
|
|
217
|
+
<div class="field-desc">Default model for Claude Code sessions.</div>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<pre><code>{
|
|
221
|
+
"model": "claude-sonnet-4-6"
|
|
222
|
+
}</code></pre>
|
|
223
|
+
|
|
224
|
+
<table>
|
|
225
|
+
<tr><th>Model</th><th>Best for</th></tr>
|
|
226
|
+
<tr><td><code>claude-opus-4-6</code></td><td>Complex reasoning, architecture</td></tr>
|
|
227
|
+
<tr><td><code>claude-sonnet-4-6</code></td><td>Fast daily coding (default)</td></tr>
|
|
228
|
+
<tr><td><code>claude-haiku-4-5</code></td><td>Quick tasks, high volume</td></tr>
|
|
229
|
+
</table>
|
|
230
|
+
|
|
231
|
+
<h2 id="projects">Project-Level Settings</h2>
|
|
232
|
+
|
|
233
|
+
<pre><code>// .claude/settings.json (commit to repo)
|
|
234
|
+
{
|
|
235
|
+
"hooks": {
|
|
236
|
+
"PreToolUse": [{
|
|
237
|
+
"matcher": "Bash",
|
|
238
|
+
"hooks": [{
|
|
239
|
+
"type": "command",
|
|
240
|
+
"command": "bash .claude/hooks/guard.sh"
|
|
241
|
+
}]
|
|
242
|
+
}]
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// .claude/settings.local.json (gitignored, personal)
|
|
247
|
+
{
|
|
248
|
+
"permissions": {
|
|
249
|
+
"allow": ["Bash(npm test)"]
|
|
250
|
+
}
|
|
251
|
+
}</code><button class="copy" onclick="cp(this)">Copy</button></pre>
|
|
252
|
+
|
|
253
|
+
<div class="note">
|
|
254
|
+
Use relative paths in project settings (<code>.claude/hooks/guard.sh</code>) so they work for all team members. Use <code>npx cc-safe-setup --team</code> to set this up automatically.
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<h2 id="examples">Full Working Examples</h2>
|
|
258
|
+
|
|
259
|
+
<h3>Minimal Safe Setup</h3>
|
|
260
|
+
<pre><code>{
|
|
261
|
+
"hooks": {
|
|
262
|
+
"PreToolUse": [{
|
|
263
|
+
"matcher": "Bash",
|
|
264
|
+
"hooks": [
|
|
265
|
+
{"type": "command", "command": "bash ~/.claude/hooks/destructive-guard.sh"},
|
|
266
|
+
{"type": "command", "command": "bash ~/.claude/hooks/branch-guard.sh"},
|
|
267
|
+
{"type": "command", "command": "bash ~/.claude/hooks/secret-guard.sh"}
|
|
268
|
+
]
|
|
269
|
+
}]
|
|
270
|
+
}
|
|
271
|
+
}</code><button class="copy" onclick="cp(this)">Copy</button></pre>
|
|
272
|
+
|
|
273
|
+
<h3>Node.js Project</h3>
|
|
274
|
+
<pre><code>{
|
|
275
|
+
"hooks": {
|
|
276
|
+
"PreToolUse": [
|
|
277
|
+
{"matcher": "Bash", "hooks": [
|
|
278
|
+
{"type": "command", "command": "bash ~/.claude/hooks/destructive-guard.sh"},
|
|
279
|
+
{"type": "command", "command": "bash ~/.claude/hooks/branch-guard.sh"},
|
|
280
|
+
{"type": "command", "command": "bash ~/.claude/hooks/secret-guard.sh"},
|
|
281
|
+
{"type": "command", "command": "bash ~/.claude/hooks/auto-approve-build.sh"}
|
|
282
|
+
]},
|
|
283
|
+
{"matcher": "Edit|Write", "hooks": [
|
|
284
|
+
{"type": "command", "command": "bash ~/.claude/hooks/syntax-check.sh"}
|
|
285
|
+
]}
|
|
286
|
+
]
|
|
287
|
+
},
|
|
288
|
+
"permissions": {
|
|
289
|
+
"allow": ["Bash(npm test)", "Bash(npm run lint)", "Read(*)"]
|
|
290
|
+
}
|
|
291
|
+
}</code><button class="copy" onclick="cp(this)">Copy</button></pre>
|
|
292
|
+
|
|
293
|
+
<h3>Maximum Safety (Autonomous)</h3>
|
|
294
|
+
<pre><code>// Generated by: npx cc-safe-setup --profile strict
|
|
295
|
+
// 33 hooks for autonomous/production use
|
|
296
|
+
// Run: npx cc-safe-setup --shield</code></pre>
|
|
297
|
+
|
|
298
|
+
<div class="footer">
|
|
299
|
+
<a href="hooks-cheatsheet.html">Cheat Sheet</a> ·
|
|
300
|
+
<a href="builder.html">Builder</a> ·
|
|
301
|
+
<a href="faq.html">FAQ</a> ·
|
|
302
|
+
<a href="by-example.html">Examples</a> ·
|
|
303
|
+
<a href="migration-guide.html">Migration</a> ·
|
|
304
|
+
<a href="https://github.com/yurukusa/cc-safe-setup">GitHub</a>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
|
|
308
|
+
<script>
|
|
309
|
+
function cp(btn) {
|
|
310
|
+
const code = btn.parentElement.querySelector('code').textContent;
|
|
311
|
+
navigator.clipboard.writeText(code);
|
|
312
|
+
btn.textContent = 'Copied!';
|
|
313
|
+
setTimeout(() => btn.textContent = 'Copy', 1500);
|
|
314
|
+
}
|
|
315
|
+
</script>
|
|
316
|
+
</body>
|
|
317
|
+
</html>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# git-blame-context.sh — Show file ownership before major edits
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Before Claude rewrites a file, show who wrote most of it.
|
|
7
|
+
# Helps prevent accidentally breaking code you don't understand
|
|
8
|
+
# the history of. Especially useful in team repositories.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PreToolUse MATCHER: "Edit|Write"
|
|
11
|
+
# ================================================================
|
|
12
|
+
|
|
13
|
+
INPUT=$(cat)
|
|
14
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
15
|
+
[ -z "$FILE" ] && exit 0
|
|
16
|
+
[ ! -f "$FILE" ] && exit 0
|
|
17
|
+
|
|
18
|
+
# Only warn on substantial edits (old_string > 10 lines)
|
|
19
|
+
OLD=$(echo "$INPUT" | jq -r '.tool_input.old_string // empty' 2>/dev/null)
|
|
20
|
+
[ -z "$OLD" ] && exit 0
|
|
21
|
+
OLD_LINES=$(echo "$OLD" | wc -l)
|
|
22
|
+
[ "$OLD_LINES" -lt 10 ] && exit 0
|
|
23
|
+
|
|
24
|
+
# Get top contributors for this file
|
|
25
|
+
CONTRIBUTORS=$(git log --format='%an' -- "$FILE" 2>/dev/null | sort | uniq -c | sort -rn | head -3)
|
|
26
|
+
if [ -n "$CONTRIBUTORS" ]; then
|
|
27
|
+
TOTAL_COMMITS=$(git log --oneline -- "$FILE" 2>/dev/null | wc -l)
|
|
28
|
+
LAST_AUTHOR=$(git log -1 --format='%an' -- "$FILE" 2>/dev/null)
|
|
29
|
+
echo "NOTE: Editing $OLD_LINES+ lines in $FILE" >&2
|
|
30
|
+
echo " Last edited by: $LAST_AUTHOR" >&2
|
|
31
|
+
echo " Top contributors ($TOTAL_COMMITS commits):" >&2
|
|
32
|
+
echo "$CONTRIBUTORS" | head -3 | sed 's/^/ /' >&2
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
exit 0
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# import-cycle-warn.sh — Detect potential circular imports
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude adds imports without considering dependency cycles.
|
|
7
|
+
# After an edit that adds an import/require, check if the
|
|
8
|
+
# target file imports back from the edited file.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PostToolUse MATCHER: "Edit"
|
|
11
|
+
# ================================================================
|
|
12
|
+
|
|
13
|
+
INPUT=$(cat)
|
|
14
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
15
|
+
[ -z "$FILE" ] && exit 0
|
|
16
|
+
|
|
17
|
+
NEW=$(echo "$INPUT" | jq -r '.tool_input.new_string // empty' 2>/dev/null)
|
|
18
|
+
[ -z "$NEW" ] && exit 0
|
|
19
|
+
|
|
20
|
+
# Extract newly added imports
|
|
21
|
+
BASENAME=$(basename "$FILE" | sed 's/\.[^.]*$//')
|
|
22
|
+
|
|
23
|
+
# JS/TS: import ... from './target' or require('./target')
|
|
24
|
+
IMPORTS=$(echo "$NEW" | grep -oE "(from\s+['\"]\.\/[^'\"]+|require\(['\"]\.\/[^'\"]+)" | grep -oE '\./[^"'"'"']+' | sed 's/^\.\///')
|
|
25
|
+
|
|
26
|
+
# Python: from .target import or import target
|
|
27
|
+
if [ -z "$IMPORTS" ]; then
|
|
28
|
+
IMPORTS=$(echo "$NEW" | grep -oE "from\s+\.\w+" | awk '{print $2}' | sed 's/^\.//')
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
[ -z "$IMPORTS" ] && exit 0
|
|
32
|
+
|
|
33
|
+
DIR=$(dirname "$FILE")
|
|
34
|
+
for imp in $IMPORTS; do
|
|
35
|
+
# Check if target imports back
|
|
36
|
+
for ext in .js .ts .jsx .tsx .py .mjs; do
|
|
37
|
+
TARGET="$DIR/$imp$ext"
|
|
38
|
+
[ -f "$TARGET" ] || continue
|
|
39
|
+
if grep -qE "(from\s+['\"].*${BASENAME}|import\s+.*${BASENAME}|require.*${BASENAME})" "$TARGET" 2>/dev/null; then
|
|
40
|
+
echo "WARNING: Potential circular import detected." >&2
|
|
41
|
+
echo " $FILE imports $imp" >&2
|
|
42
|
+
echo " $TARGET imports back from $BASENAME" >&2
|
|
43
|
+
break
|
|
44
|
+
fi
|
|
45
|
+
done
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
exit 0
|
package/index.mjs
CHANGED
|
@@ -95,6 +95,7 @@ const ANALYZE = process.argv.includes('--analyze');
|
|
|
95
95
|
const TEAM = process.argv.includes('--team');
|
|
96
96
|
const MIGRATE_FROM_IDX = process.argv.findIndex(a => a === '--migrate-from');
|
|
97
97
|
const MIGRATE_FROM = MIGRATE_FROM_IDX !== -1 ? process.argv[MIGRATE_FROM_IDX + 1] : null;
|
|
98
|
+
const HEALTH = process.argv.includes('--health');
|
|
98
99
|
const PROFILE_IDX = process.argv.findIndex(a => a === '--profile');
|
|
99
100
|
const PROFILE = PROFILE_IDX !== -1 ? process.argv[PROFILE_IDX + 1] : null;
|
|
100
101
|
const COMPARE_IDX = process.argv.findIndex(a => a === '--compare');
|
|
@@ -132,6 +133,7 @@ if (HELP) {
|
|
|
132
133
|
npx cc-safe-setup --doctor Diagnose why hooks aren't working
|
|
133
134
|
npx cc-safe-setup --watch Live dashboard of blocked commands
|
|
134
135
|
npx cc-safe-setup --create "<desc>" Generate a custom hook from description
|
|
136
|
+
npx cc-safe-setup --health Hook health dashboard (size, permissions, age)
|
|
135
137
|
npx cc-safe-setup --migrate-from <tool> Migrate from safety-net/hooks-mastery/etc.
|
|
136
138
|
npx cc-safe-setup --team Set up project-level hooks (commit to repo for team)
|
|
137
139
|
npx cc-safe-setup --profile <level> Switch safety profile (strict/standard/minimal)
|
|
@@ -843,6 +845,92 @@ async function fullSetup() {
|
|
|
843
845
|
console.log();
|
|
844
846
|
}
|
|
845
847
|
|
|
848
|
+
async function health() {
|
|
849
|
+
const { readdirSync, statSync } = await import('fs');
|
|
850
|
+
console.log();
|
|
851
|
+
console.log(c.bold + ' Hook Health Dashboard' + c.reset);
|
|
852
|
+
console.log();
|
|
853
|
+
|
|
854
|
+
if (!existsSync(HOOKS_DIR)) {
|
|
855
|
+
console.log(c.red + ' No hooks directory found.' + c.reset);
|
|
856
|
+
console.log(c.dim + ' Run: npx cc-safe-setup --shield' + c.reset);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const hooks = readdirSync(HOOKS_DIR).filter(f => f.endsWith('.sh') || f.endsWith('.py'));
|
|
861
|
+
if (hooks.length === 0) {
|
|
862
|
+
console.log(c.yellow + ' No hooks installed.' + c.reset);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Table header
|
|
867
|
+
const pad = (s, n) => s.substring(0, n).padEnd(n);
|
|
868
|
+
console.log(c.dim + ' ' + pad('Hook', 30) + pad('Size', 8) + pad('Perms', 7) + pad('Age', 12) + 'Shebang' + c.reset);
|
|
869
|
+
console.log(c.dim + ' ' + '─'.repeat(70) + c.reset);
|
|
870
|
+
|
|
871
|
+
let healthy = 0, issues = 0;
|
|
872
|
+
const now = Date.now();
|
|
873
|
+
|
|
874
|
+
for (const h of hooks.sort()) {
|
|
875
|
+
const fullPath = join(HOOKS_DIR, h);
|
|
876
|
+
const st = statSync(fullPath);
|
|
877
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
878
|
+
const firstLine = content.split('\n')[0];
|
|
879
|
+
|
|
880
|
+
// Size
|
|
881
|
+
const size = st.size < 1024 ? st.size + 'B' : Math.round(st.size / 1024) + 'KB';
|
|
882
|
+
|
|
883
|
+
// Permissions
|
|
884
|
+
const isExec = !!(st.mode & 0o111);
|
|
885
|
+
const perms = isExec ? c.green + '✓ exec' + c.reset : c.red + '✗ exec' + c.reset;
|
|
886
|
+
|
|
887
|
+
// Age
|
|
888
|
+
const ageDays = Math.floor((now - st.mtimeMs) / 86400000);
|
|
889
|
+
const age = ageDays === 0 ? 'today' : ageDays === 1 ? '1 day' : ageDays + ' days';
|
|
890
|
+
|
|
891
|
+
// Shebang
|
|
892
|
+
const hasShebang = firstLine.startsWith('#!');
|
|
893
|
+
const shebang = hasShebang ? c.green + '✓' + c.reset : c.red + '✗' + c.reset;
|
|
894
|
+
|
|
895
|
+
// Status icon
|
|
896
|
+
const ok = isExec && hasShebang;
|
|
897
|
+
const icon = ok ? c.green + '●' + c.reset : c.red + '●' + c.reset;
|
|
898
|
+
if (ok) healthy++; else issues++;
|
|
899
|
+
|
|
900
|
+
console.log(' ' + icon + ' ' + pad(h, 29) + pad(size, 8) + pad(isExec ? '✓ exec' : '✗ exec', 7) + pad(age, 12) + (hasShebang ? '✓' : '✗'));
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
console.log(c.dim + ' ' + '─'.repeat(70) + c.reset);
|
|
904
|
+
console.log(` ${c.green}${healthy} healthy${c.reset} · ${issues > 0 ? c.red + issues + ' issues' + c.reset : c.dim + '0 issues' + c.reset} · ${hooks.length} total`);
|
|
905
|
+
|
|
906
|
+
if (issues > 0) {
|
|
907
|
+
console.log();
|
|
908
|
+
console.log(c.yellow + ' Fix issues: npx cc-safe-setup --quickfix' + c.reset);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Settings.json hook count
|
|
912
|
+
console.log();
|
|
913
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
914
|
+
try {
|
|
915
|
+
const s = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
916
|
+
let configured = 0;
|
|
917
|
+
for (const groups of Object.values(s.hooks || {})) {
|
|
918
|
+
for (const g of groups) {
|
|
919
|
+
configured += (g.hooks || []).length;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
console.log(c.dim + ` ${configured} hooks configured in settings.json` + c.reset);
|
|
923
|
+
if (configured < hooks.length) {
|
|
924
|
+
console.log(c.yellow + ` ${hooks.length - configured} hooks installed but not configured.` + c.reset);
|
|
925
|
+
console.log(c.dim + ' Run --shield to auto-configure all hooks.' + c.reset);
|
|
926
|
+
}
|
|
927
|
+
} catch {
|
|
928
|
+
console.log(c.red + ' settings.json has syntax errors.' + c.reset);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
console.log();
|
|
932
|
+
}
|
|
933
|
+
|
|
846
934
|
async function migrateFrom(tool) {
|
|
847
935
|
console.log();
|
|
848
936
|
console.log(c.bold + ' cc-safe-setup --migrate-from ' + (tool || '?') + c.reset);
|
|
@@ -3519,6 +3607,7 @@ async function main() {
|
|
|
3519
3607
|
if (FULL) return fullSetup();
|
|
3520
3608
|
if (DOCTOR) return doctor();
|
|
3521
3609
|
if (WATCH) return watch();
|
|
3610
|
+
if (HEALTH) return health();
|
|
3522
3611
|
if (MIGRATE_FROM_IDX !== -1) return migrateFrom(MIGRATE_FROM);
|
|
3523
3612
|
if (TEAM) return team();
|
|
3524
3613
|
if (PROFILE_IDX !== -1) return profile(PROFILE);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.9.0",
|
|
4
4
|
"description": "One command to make Claude Code safe. 59 hooks (8 built-in + 51 examples). 26 CLI commands: dashboard, create, audit, lint, diff, migrate, compare, generate-ci. 284 tests.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"bin": {
|