cc-safe-setup 3.6.1 → 3.8.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/CONTRIBUTING.md +73 -0
- package/README.md +3 -2
- package/audit-web/index.html +67 -0
- package/docs/README.ja.md +64 -0
- package/docs/index.html +67 -0
- package/examples/dependency-audit.sh +70 -0
- package/examples/read-before-edit.sh +44 -0
- package/index.mjs +87 -0
- package/package.json +2 -2
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Contributing to cc-safe-setup
|
|
2
|
+
|
|
3
|
+
Thanks for considering a contribution. Here's how to add a new hook.
|
|
4
|
+
|
|
5
|
+
## Adding an Example Hook
|
|
6
|
+
|
|
7
|
+
1. Create `examples/your-hook-name.sh`
|
|
8
|
+
2. Add the standard header comment:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
#!/bin/bash
|
|
12
|
+
# ================================================================
|
|
13
|
+
# your-hook-name.sh — Short description
|
|
14
|
+
# ================================================================
|
|
15
|
+
# PURPOSE: What it does and why
|
|
16
|
+
# TRIGGER: PreToolUse | PostToolUse | Stop
|
|
17
|
+
# MATCHER: "Bash" | "Edit|Write" | ""
|
|
18
|
+
# ================================================================
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
3. Handle empty input:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
INPUT=$(cat)
|
|
25
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
26
|
+
[ -z "$COMMAND" ] && exit 0
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
4. Use exit codes correctly:
|
|
30
|
+
- `exit 0` = allow
|
|
31
|
+
- `exit 2` = block
|
|
32
|
+
- Never use `exit 1` for blocking
|
|
33
|
+
|
|
34
|
+
5. Add to `index.mjs` categories (search for `CATEGORIES`)
|
|
35
|
+
6. Add to `README.md` examples list
|
|
36
|
+
7. Run tests: `bash test.sh`
|
|
37
|
+
|
|
38
|
+
## Testing Your Hook
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Manual test
|
|
42
|
+
echo '{"tool_input":{"command":"your test command"}}' | bash examples/your-hook.sh
|
|
43
|
+
echo $?
|
|
44
|
+
|
|
45
|
+
# Auto-test
|
|
46
|
+
npx cc-hook-test examples/your-hook.sh
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Hook Quality Checklist
|
|
50
|
+
|
|
51
|
+
- [ ] Handles empty input (exits 0)
|
|
52
|
+
- [ ] Uses `exit 2` for blocking (not `exit 1`)
|
|
53
|
+
- [ ] Has descriptive stderr messages on block
|
|
54
|
+
- [ ] Passes `bash -n` syntax check
|
|
55
|
+
- [ ] Linked to a GitHub Issue if applicable
|
|
56
|
+
- [ ] Added to README.md and index.mjs categories
|
|
57
|
+
|
|
58
|
+
## Pull Request Process
|
|
59
|
+
|
|
60
|
+
1. Fork and create a branch
|
|
61
|
+
2. Add your hook + update README + update categories
|
|
62
|
+
3. Run `bash test.sh` (all must pass)
|
|
63
|
+
4. Submit PR with:
|
|
64
|
+
- What the hook does
|
|
65
|
+
- Which GitHub Issue inspired it (if any)
|
|
66
|
+
- Test evidence (manual or cc-hook-test output)
|
|
67
|
+
|
|
68
|
+
## Code Style
|
|
69
|
+
|
|
70
|
+
- Bash hooks (not Python/Node) for zero dependencies
|
|
71
|
+
- `jq` for JSON parsing
|
|
72
|
+
- Short variable names are fine (`CMD`, `FILE`, `INPUT`)
|
|
73
|
+
- Comments explain *why*, not *what*
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/cc-safe-setup)
|
|
5
5
|
[](https://github.com/yurukusa/cc-safe-setup/actions/workflows/test.yml)
|
|
6
6
|
|
|
7
|
-
**One command to make Claude Code safe for autonomous operation.**
|
|
7
|
+
**One command to make Claude Code safe for autonomous operation.** [日本語](docs/README.ja.md)
|
|
8
8
|
|
|
9
9
|
8 built-in hooks + 32 installable examples. Audit, create, lint, diff, watch, and learn. [Cheat Sheet](https://yurukusa.github.io/cc-safe-setup/cheatsheet.html) · [Web Tool](https://yurukusa.github.io/cc-safe-setup/) · [Troubleshooting](TROUBLESHOOTING.md)
|
|
10
10
|
|
|
@@ -223,6 +223,7 @@ Or browse all available examples in [`examples/`](examples/):
|
|
|
223
223
|
- **commit-quality-gate.sh** — Warn on vague commit messages ("update code"), long subjects, mega-commits
|
|
224
224
|
- **session-handoff.sh** — Auto-save git state and session info to `~/.claude/session-handoff.md` on session end
|
|
225
225
|
- **diff-size-guard.sh** — Warn/block when committing too many files at once (default: warn at 10, block at 50)
|
|
226
|
+
- **dependency-audit.sh** — Warn when installing packages not in manifest (npm/pip/cargo supply chain awareness)
|
|
226
227
|
|
|
227
228
|
## Safety Checklist
|
|
228
229
|
|
|
@@ -243,7 +244,7 @@ Or browse all available examples in [`examples/`](examples/):
|
|
|
243
244
|
## Learn More
|
|
244
245
|
|
|
245
246
|
- [Official Hooks Reference](https://docs.anthropic.com/en/docs/claude-code/hooks) — Claude Code hooks documentation
|
|
246
|
-
- [Hooks Cookbook](https://github.com/yurukusa/claude-code-hooks/blob/main/COOKBOOK.md) —
|
|
247
|
+
- [Hooks Cookbook](https://github.com/yurukusa/claude-code-hooks/blob/main/COOKBOOK.md) — 25 recipes from real GitHub Issues ([interactive version](https://yurukusa.github.io/claude-code-hooks/))
|
|
247
248
|
- [Japanese guide (Qiita)](https://qiita.com/yurukusa/items/a9714b33f5d974e8f1e8) — この記事の日本語解説
|
|
248
249
|
- [Hook Test Runner](https://github.com/yurukusa/cc-hook-test) — `npx cc-hook-test <hook.sh>` to auto-test any hook
|
|
249
250
|
- [Hooks Cheat Sheet](https://yurukusa.github.io/cc-safe-setup/cheatsheet.html) — printable A4 quick reference
|
package/audit-web/index.html
CHANGED
|
@@ -88,6 +88,31 @@ 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
|
+
<h2 style="margin-top:2rem">Hook Builder</h2>
|
|
92
|
+
<p class="subtitle">Build a custom hook without writing code.</p>
|
|
93
|
+
|
|
94
|
+
<div style="display:flex;gap:1rem;flex-wrap:wrap;margin:.5rem 0">
|
|
95
|
+
<div style="flex:1;min-width:200px">
|
|
96
|
+
<label style="color:#8b949e;font-size:.8rem">What should the hook do?</label>
|
|
97
|
+
<select id="hb-action" style="width:100%;padding:.5rem;background:#161b22;border:1px solid #30363d;border-radius:4px;color:#c9d1d9;margin-top:.25rem">
|
|
98
|
+
<option value="block">Block a command</option>
|
|
99
|
+
<option value="warn">Warn about a command</option>
|
|
100
|
+
<option value="approve">Auto-approve a command</option>
|
|
101
|
+
</select>
|
|
102
|
+
</div>
|
|
103
|
+
<div style="flex:2;min-width:300px">
|
|
104
|
+
<label style="color:#8b949e;font-size:.8rem">Command pattern (regex)</label>
|
|
105
|
+
<input id="hb-pattern" placeholder="e.g. rm\s+-rf, git push --force, npm publish" style="width:100%;padding:.5rem;background:#161b22;border:1px solid #30363d;border-radius:4px;color:#c9d1d9;margin-top:.25rem;font-family:monospace;font-size:.85rem">
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
<div style="margin:.5rem 0">
|
|
109
|
+
<label style="color:#8b949e;font-size:.8rem">Message shown to Claude</label>
|
|
110
|
+
<input id="hb-message" placeholder="e.g. Run tests before publishing" style="width:100%;padding:.5rem;background:#161b22;border:1px solid #30363d;border-radius:4px;color:#c9d1d9;margin-top:.25rem">
|
|
111
|
+
</div>
|
|
112
|
+
<button class="btn" onclick="buildHook()">Generate Hook</button>
|
|
113
|
+
|
|
114
|
+
<div id="hb-result" style="margin-top:1rem"></div>
|
|
115
|
+
|
|
91
116
|
<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
117
|
</div>
|
|
93
118
|
|
|
@@ -599,6 +624,48 @@ exit 0`
|
|
|
599
624
|
return scripts[id] || '#!/bin/bash\nexit 0';
|
|
600
625
|
}
|
|
601
626
|
|
|
627
|
+
function buildHook() {
|
|
628
|
+
const action = document.getElementById('hb-action').value;
|
|
629
|
+
const pattern = document.getElementById('hb-pattern').value.trim();
|
|
630
|
+
const message = document.getElementById('hb-message').value.trim() || 'Blocked by hook';
|
|
631
|
+
const el = document.getElementById('hb-result');
|
|
632
|
+
|
|
633
|
+
if (!pattern) { el.innerHTML = '<p style="color:#f85149">Enter a command pattern.</p>'; return; }
|
|
634
|
+
|
|
635
|
+
let script = '#!/bin/bash\n';
|
|
636
|
+
script += 'INPUT=$(cat)\n';
|
|
637
|
+
script += 'COMMAND=$(echo "$INPUT" | jq -r \'.tool_input.command // empty\' 2>/dev/null)\n';
|
|
638
|
+
script += '[ -z "$COMMAND" ] && exit 0\n\n';
|
|
639
|
+
|
|
640
|
+
if (action === 'block') {
|
|
641
|
+
script += 'if echo "$COMMAND" | grep -qE \'' + pattern.replace(/'/g, "'\\''") + '\'; then\n';
|
|
642
|
+
script += ' echo "BLOCKED: ' + message.replace(/"/g, '\\"') + '" >&2\n';
|
|
643
|
+
script += ' echo "Command: $COMMAND" >&2\n';
|
|
644
|
+
script += ' exit 2\n';
|
|
645
|
+
script += 'fi\n';
|
|
646
|
+
} else if (action === 'warn') {
|
|
647
|
+
script += 'if echo "$COMMAND" | grep -qE \'' + pattern.replace(/'/g, "'\\''") + '\'; then\n';
|
|
648
|
+
script += ' echo "WARNING: ' + message.replace(/"/g, '\\"') + '" >&2\n';
|
|
649
|
+
script += ' echo "Command: $COMMAND" >&2\n';
|
|
650
|
+
script += 'fi\n';
|
|
651
|
+
} else if (action === 'approve') {
|
|
652
|
+
script += 'if echo "$COMMAND" | grep -qE \'' + pattern.replace(/'/g, "'\\''") + '\'; then\n';
|
|
653
|
+
script += ' jq -n \'{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"' + message.replace(/"/g, '\\"') + '"}}\'\n';
|
|
654
|
+
script += 'fi\n';
|
|
655
|
+
}
|
|
656
|
+
script += 'exit 0';
|
|
657
|
+
|
|
658
|
+
const settings = {
|
|
659
|
+
hooks: { PreToolUse: [{ matcher: 'Bash', hooks: [{ type: 'command', command: '~/.claude/hooks/custom-hook.sh' }] }] }
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
el.innerHTML = '<h3>Hook Script</h3>' +
|
|
663
|
+
'<div class="code-block"><button class="copy-btn" onclick="copyText(\'hb-script\')">Copy</button><pre id="hb-script">' + escHtml(script) + '</pre></div>' +
|
|
664
|
+
'<h3>settings.json entry</h3>' +
|
|
665
|
+
'<div class="code-block"><button class="copy-btn" onclick="copyText(\'hb-settings\')">Copy</button><pre id="hb-settings">' + escHtml(JSON.stringify(settings, null, 2)) + '</pre></div>' +
|
|
666
|
+
'<p style="color:#8b949e;font-size:.8rem;margin-top:.5rem">Save the script as <code>~/.claude/hooks/custom-hook.sh</code>, run <code>chmod +x</code>, and merge the settings entry.</p>';
|
|
667
|
+
}
|
|
668
|
+
|
|
602
669
|
// Auto-load from URL parameter: ?config=base64encodedJSON
|
|
603
670
|
(function() {
|
|
604
671
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# cc-safe-setup
|
|
2
|
+
|
|
3
|
+
**Claude Codeを安全にするワンコマンドツール。**
|
|
4
|
+
|
|
5
|
+
8個の安全フック + 36個のインストール可能なexample。destructive guard、branch protection、secret leak prevention、syntax check、context monitor、その他。
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx cc-safe-setup
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 何ができるか
|
|
12
|
+
|
|
13
|
+
| コマンド | 機能 |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `npx cc-safe-setup` | 8個の安全フックをインストール |
|
|
16
|
+
| `--create "説明"` | 自然言語でカスタムフック生成 |
|
|
17
|
+
| `--audit` | 安全スコア(0-100) |
|
|
18
|
+
| `--lint` | 設定の静的解析 |
|
|
19
|
+
| `--diff <file>` | 設定を比較 |
|
|
20
|
+
| `--benchmark` | フック実行速度を計測 |
|
|
21
|
+
| `--doctor` | 動かない原因を診断 |
|
|
22
|
+
| `--watch` | ブロックされたコマンドをリアルタイム表示 |
|
|
23
|
+
| `--stats` | ブロック統計レポート |
|
|
24
|
+
| `--share` | 共有URLを生成 |
|
|
25
|
+
| `--export / --import` | チームで設定を共有 |
|
|
26
|
+
| `--verify` | 各フックの動作確認 |
|
|
27
|
+
| `--install-example <name>` | 36個のexampleフック |
|
|
28
|
+
|
|
29
|
+
## インストール
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx cc-safe-setup
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Claude Codeを再起動。完了。
|
|
36
|
+
|
|
37
|
+
## 何がブロックされるか
|
|
38
|
+
|
|
39
|
+
| 操作 | Before | After |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `rm -rf /` | 実行される | ブロック |
|
|
42
|
+
| `git push --force` | 実行される | ブロック |
|
|
43
|
+
| `git push origin main` | 実行される | ブロック |
|
|
44
|
+
| `git add .env` | 実行される | ブロック |
|
|
45
|
+
| Python構文エラー | 気づかない | 自動検出 |
|
|
46
|
+
| コンテキスト枯渇 | 突然死 | 段階的警告 |
|
|
47
|
+
|
|
48
|
+
## ドキュメント
|
|
49
|
+
|
|
50
|
+
- [settings.jsonリファレンス](../SETTINGS_REFERENCE.md) — 全設定の解説
|
|
51
|
+
- [移行ガイド](../MIGRATION.md) — permissionsからhooksへ
|
|
52
|
+
- [トラブルシューティング](../TROUBLESHOOTING.md) — 動かない時の対処法
|
|
53
|
+
- [チートシート](https://yurukusa.github.io/cc-safe-setup/cheatsheet.html) — 印刷用A4
|
|
54
|
+
- [エコシステム比較](https://yurukusa.github.io/cc-safe-setup/ecosystem.html) — 全hookプロジェクト比較
|
|
55
|
+
- [Web版](https://yurukusa.github.io/cc-safe-setup/) — ブラウザで診断+セットアップ生成
|
|
56
|
+
|
|
57
|
+
## 必要なもの
|
|
58
|
+
|
|
59
|
+
- `jq`: `brew install jq` / `apt install jq`
|
|
60
|
+
- Claude Code 2.1以上
|
|
61
|
+
|
|
62
|
+
## ライセンス
|
|
63
|
+
|
|
64
|
+
MIT
|
package/docs/index.html
CHANGED
|
@@ -88,6 +88,31 @@ 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
|
+
<h2 style="margin-top:2rem">Hook Builder</h2>
|
|
92
|
+
<p class="subtitle">Build a custom hook without writing code.</p>
|
|
93
|
+
|
|
94
|
+
<div style="display:flex;gap:1rem;flex-wrap:wrap;margin:.5rem 0">
|
|
95
|
+
<div style="flex:1;min-width:200px">
|
|
96
|
+
<label style="color:#8b949e;font-size:.8rem">What should the hook do?</label>
|
|
97
|
+
<select id="hb-action" style="width:100%;padding:.5rem;background:#161b22;border:1px solid #30363d;border-radius:4px;color:#c9d1d9;margin-top:.25rem">
|
|
98
|
+
<option value="block">Block a command</option>
|
|
99
|
+
<option value="warn">Warn about a command</option>
|
|
100
|
+
<option value="approve">Auto-approve a command</option>
|
|
101
|
+
</select>
|
|
102
|
+
</div>
|
|
103
|
+
<div style="flex:2;min-width:300px">
|
|
104
|
+
<label style="color:#8b949e;font-size:.8rem">Command pattern (regex)</label>
|
|
105
|
+
<input id="hb-pattern" placeholder="e.g. rm\s+-rf, git push --force, npm publish" style="width:100%;padding:.5rem;background:#161b22;border:1px solid #30363d;border-radius:4px;color:#c9d1d9;margin-top:.25rem;font-family:monospace;font-size:.85rem">
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
<div style="margin:.5rem 0">
|
|
109
|
+
<label style="color:#8b949e;font-size:.8rem">Message shown to Claude</label>
|
|
110
|
+
<input id="hb-message" placeholder="e.g. Run tests before publishing" style="width:100%;padding:.5rem;background:#161b22;border:1px solid #30363d;border-radius:4px;color:#c9d1d9;margin-top:.25rem">
|
|
111
|
+
</div>
|
|
112
|
+
<button class="btn" onclick="buildHook()">Generate Hook</button>
|
|
113
|
+
|
|
114
|
+
<div id="hb-result" style="margin-top:1rem"></div>
|
|
115
|
+
|
|
91
116
|
<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
117
|
</div>
|
|
93
118
|
|
|
@@ -599,6 +624,48 @@ exit 0`
|
|
|
599
624
|
return scripts[id] || '#!/bin/bash\nexit 0';
|
|
600
625
|
}
|
|
601
626
|
|
|
627
|
+
function buildHook() {
|
|
628
|
+
const action = document.getElementById('hb-action').value;
|
|
629
|
+
const pattern = document.getElementById('hb-pattern').value.trim();
|
|
630
|
+
const message = document.getElementById('hb-message').value.trim() || 'Blocked by hook';
|
|
631
|
+
const el = document.getElementById('hb-result');
|
|
632
|
+
|
|
633
|
+
if (!pattern) { el.innerHTML = '<p style="color:#f85149">Enter a command pattern.</p>'; return; }
|
|
634
|
+
|
|
635
|
+
let script = '#!/bin/bash\n';
|
|
636
|
+
script += 'INPUT=$(cat)\n';
|
|
637
|
+
script += 'COMMAND=$(echo "$INPUT" | jq -r \'.tool_input.command // empty\' 2>/dev/null)\n';
|
|
638
|
+
script += '[ -z "$COMMAND" ] && exit 0\n\n';
|
|
639
|
+
|
|
640
|
+
if (action === 'block') {
|
|
641
|
+
script += 'if echo "$COMMAND" | grep -qE \'' + pattern.replace(/'/g, "'\\''") + '\'; then\n';
|
|
642
|
+
script += ' echo "BLOCKED: ' + message.replace(/"/g, '\\"') + '" >&2\n';
|
|
643
|
+
script += ' echo "Command: $COMMAND" >&2\n';
|
|
644
|
+
script += ' exit 2\n';
|
|
645
|
+
script += 'fi\n';
|
|
646
|
+
} else if (action === 'warn') {
|
|
647
|
+
script += 'if echo "$COMMAND" | grep -qE \'' + pattern.replace(/'/g, "'\\''") + '\'; then\n';
|
|
648
|
+
script += ' echo "WARNING: ' + message.replace(/"/g, '\\"') + '" >&2\n';
|
|
649
|
+
script += ' echo "Command: $COMMAND" >&2\n';
|
|
650
|
+
script += 'fi\n';
|
|
651
|
+
} else if (action === 'approve') {
|
|
652
|
+
script += 'if echo "$COMMAND" | grep -qE \'' + pattern.replace(/'/g, "'\\''") + '\'; then\n';
|
|
653
|
+
script += ' jq -n \'{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"' + message.replace(/"/g, '\\"') + '"}}\'\n';
|
|
654
|
+
script += 'fi\n';
|
|
655
|
+
}
|
|
656
|
+
script += 'exit 0';
|
|
657
|
+
|
|
658
|
+
const settings = {
|
|
659
|
+
hooks: { PreToolUse: [{ matcher: 'Bash', hooks: [{ type: 'command', command: '~/.claude/hooks/custom-hook.sh' }] }] }
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
el.innerHTML = '<h3>Hook Script</h3>' +
|
|
663
|
+
'<div class="code-block"><button class="copy-btn" onclick="copyText(\'hb-script\')">Copy</button><pre id="hb-script">' + escHtml(script) + '</pre></div>' +
|
|
664
|
+
'<h3>settings.json entry</h3>' +
|
|
665
|
+
'<div class="code-block"><button class="copy-btn" onclick="copyText(\'hb-settings\')">Copy</button><pre id="hb-settings">' + escHtml(JSON.stringify(settings, null, 2)) + '</pre></div>' +
|
|
666
|
+
'<p style="color:#8b949e;font-size:.8rem;margin-top:.5rem">Save the script as <code>~/.claude/hooks/custom-hook.sh</code>, run <code>chmod +x</code>, and merge the settings entry.</p>';
|
|
667
|
+
}
|
|
668
|
+
|
|
602
669
|
// Auto-load from URL parameter: ?config=base64encodedJSON
|
|
603
670
|
(function() {
|
|
604
671
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# dependency-audit.sh — Warn before installing unknown packages
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude Code may install packages you've never heard of.
|
|
7
|
+
# This hook warns when npm/pip/cargo installs a new dependency,
|
|
8
|
+
# giving you a chance to review before it executes.
|
|
9
|
+
#
|
|
10
|
+
# Doesn't block devDependencies or packages already in
|
|
11
|
+
# package.json/requirements.txt.
|
|
12
|
+
#
|
|
13
|
+
# TRIGGER: PreToolUse
|
|
14
|
+
# MATCHER: "Bash"
|
|
15
|
+
#
|
|
16
|
+
# WHAT IT WARNS ON:
|
|
17
|
+
# - npm install <pkg> (not in package.json)
|
|
18
|
+
# - pip install <pkg> (not in requirements.txt)
|
|
19
|
+
# - cargo add <pkg> (not in Cargo.toml)
|
|
20
|
+
#
|
|
21
|
+
# WHAT IT ALLOWS:
|
|
22
|
+
# - npm install (no args = install from package.json)
|
|
23
|
+
# - pip install -r requirements.txt
|
|
24
|
+
# - Packages already in manifest files
|
|
25
|
+
# ================================================================
|
|
26
|
+
|
|
27
|
+
INPUT=$(cat)
|
|
28
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
29
|
+
|
|
30
|
+
if [[ -z "$COMMAND" ]]; then
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# npm install <package>
|
|
35
|
+
if echo "$COMMAND" | grep -qE '^\s*npm\s+install\s+\S'; then
|
|
36
|
+
# Skip if no specific package (just `npm install`)
|
|
37
|
+
PKG=$(echo "$COMMAND" | grep -oP 'npm\s+install\s+(-[DSg]\s+)*\K[^-\s]\S*' | head -1)
|
|
38
|
+
if [[ -n "$PKG" ]] && [[ -f "package.json" ]]; then
|
|
39
|
+
if ! grep -q "\"$PKG\"" package.json 2>/dev/null; then
|
|
40
|
+
echo "NOTE: Installing new npm package: $PKG" >&2
|
|
41
|
+
echo "Not found in package.json. Review before proceeding." >&2
|
|
42
|
+
fi
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# pip install <package>
|
|
47
|
+
if echo "$COMMAND" | grep -qE '^\s*(pip3?|python3?\s+-m\s+pip)\s+install\s+\S'; then
|
|
48
|
+
# Skip -r requirements.txt
|
|
49
|
+
if ! echo "$COMMAND" | grep -qE '\-r\s+'; then
|
|
50
|
+
PKG=$(echo "$COMMAND" | grep -oP '(pip3?|python3?\s+-m\s+pip)\s+install\s+(-[^\s]+\s+)*\K[^-\s]\S*' | head -1)
|
|
51
|
+
if [[ -n "$PKG" ]] && [[ -f "requirements.txt" ]]; then
|
|
52
|
+
if ! grep -qi "$PKG" requirements.txt 2>/dev/null; then
|
|
53
|
+
echo "NOTE: Installing new pip package: $PKG" >&2
|
|
54
|
+
echo "Not found in requirements.txt." >&2
|
|
55
|
+
fi
|
|
56
|
+
fi
|
|
57
|
+
fi
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# cargo add <package>
|
|
61
|
+
if echo "$COMMAND" | grep -qE '^\s*cargo\s+add\s+\S'; then
|
|
62
|
+
PKG=$(echo "$COMMAND" | grep -oP 'cargo\s+add\s+\K\S+' | head -1)
|
|
63
|
+
if [[ -n "$PKG" ]] && [[ -f "Cargo.toml" ]]; then
|
|
64
|
+
if ! grep -q "$PKG" Cargo.toml 2>/dev/null; then
|
|
65
|
+
echo "NOTE: Adding new cargo dependency: $PKG" >&2
|
|
66
|
+
fi
|
|
67
|
+
fi
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
exit 0
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# read-before-edit.sh — Warn when editing files not recently read
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude Code sometimes tries to Edit files it hasn't Read,
|
|
7
|
+
# leading to old_string mismatches. This hook tracks which
|
|
8
|
+
# files were recently Read and warns when Edit targets an
|
|
9
|
+
# unread file.
|
|
10
|
+
#
|
|
11
|
+
# TRIGGER: PreToolUse
|
|
12
|
+
# MATCHER: "Edit"
|
|
13
|
+
#
|
|
14
|
+
# HOW IT WORKS:
|
|
15
|
+
# - PostToolUse Read hook records file paths to /tmp/cc-read-files
|
|
16
|
+
# - This PreToolUse Edit hook checks if the target was read
|
|
17
|
+
# - Warns (doesn't block) if file wasn't read recently
|
|
18
|
+
#
|
|
19
|
+
# NOTE: Requires companion PostToolUse hook to record Read events.
|
|
20
|
+
# Or just install this and accept the warning.
|
|
21
|
+
# ================================================================
|
|
22
|
+
|
|
23
|
+
INPUT=$(cat)
|
|
24
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
25
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
26
|
+
|
|
27
|
+
# Only check Edit tool
|
|
28
|
+
if [[ "$TOOL" != "Edit" ]] || [[ -z "$FILE" ]]; then
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
READ_LOG="/tmp/cc-read-files"
|
|
33
|
+
|
|
34
|
+
# Check if file was recently read
|
|
35
|
+
if [ -f "$READ_LOG" ]; then
|
|
36
|
+
if grep -qF "$FILE" "$READ_LOG" 2>/dev/null; then
|
|
37
|
+
exit 0 # File was read, safe to edit
|
|
38
|
+
fi
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
echo "NOTE: Editing $FILE without reading it first." >&2
|
|
42
|
+
echo "Consider using Read before Edit to avoid old_string mismatches." >&2
|
|
43
|
+
|
|
44
|
+
exit 0
|
package/index.mjs
CHANGED
|
@@ -83,6 +83,7 @@ const LINT = process.argv.includes('--lint');
|
|
|
83
83
|
const DIFF_IDX = process.argv.findIndex(a => a === '--diff');
|
|
84
84
|
const DIFF_FILE = DIFF_IDX !== -1 ? process.argv[DIFF_IDX + 1] : null;
|
|
85
85
|
const SHARE = process.argv.includes('--share');
|
|
86
|
+
const BENCHMARK = process.argv.includes('--benchmark');
|
|
86
87
|
const CREATE_IDX = process.argv.findIndex(a => a === '--create');
|
|
87
88
|
const CREATE_DESC = CREATE_IDX !== -1 ? process.argv.slice(CREATE_IDX + 1).join(' ') : null;
|
|
88
89
|
|
|
@@ -104,6 +105,7 @@ if (HELP) {
|
|
|
104
105
|
npx cc-safe-setup --audit --json Machine-readable output for CI/CD
|
|
105
106
|
npx cc-safe-setup --scan Detect tech stack, recommend hooks
|
|
106
107
|
npx cc-safe-setup --learn Learn from your block history
|
|
108
|
+
npx cc-safe-setup --benchmark Measure hook execution time
|
|
107
109
|
npx cc-safe-setup --share Generate shareable URL for your setup
|
|
108
110
|
npx cc-safe-setup --diff <file> Compare your settings with another file
|
|
109
111
|
npx cc-safe-setup --lint Static analysis of hook configuration
|
|
@@ -353,6 +355,7 @@ function examples() {
|
|
|
353
355
|
'session-handoff.sh': 'Auto-save session state for next session resume',
|
|
354
356
|
'commit-quality-gate.sh': 'Warn on vague or too-long commit messages',
|
|
355
357
|
'diff-size-guard.sh': 'Warn/block on large diffs (10+ files warn, 50+ block)',
|
|
358
|
+
'dependency-audit.sh': 'Warn on new package installs not in manifest',
|
|
356
359
|
},
|
|
357
360
|
};
|
|
358
361
|
|
|
@@ -783,6 +786,89 @@ async function fullSetup() {
|
|
|
783
786
|
console.log();
|
|
784
787
|
}
|
|
785
788
|
|
|
789
|
+
async function benchmark() {
|
|
790
|
+
const { spawnSync } = await import('child_process');
|
|
791
|
+
|
|
792
|
+
console.log();
|
|
793
|
+
console.log(c.bold + ' cc-safe-setup --benchmark' + c.reset);
|
|
794
|
+
console.log(c.dim + ' Measuring hook execution time (10 runs each)...' + c.reset);
|
|
795
|
+
console.log();
|
|
796
|
+
|
|
797
|
+
if (!existsSync(SETTINGS_PATH)) {
|
|
798
|
+
console.log(c.red + ' No settings.json found.' + c.reset);
|
|
799
|
+
process.exit(1);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
803
|
+
const hooks = settings.hooks || {};
|
|
804
|
+
const results = [];
|
|
805
|
+
const testInput = JSON.stringify({ tool_input: { command: 'echo hello' } });
|
|
806
|
+
const RUNS = 10;
|
|
807
|
+
|
|
808
|
+
for (const [trigger, entries] of Object.entries(hooks)) {
|
|
809
|
+
for (const entry of entries) {
|
|
810
|
+
for (const h of (entry.hooks || [])) {
|
|
811
|
+
if (h.type !== 'command') continue;
|
|
812
|
+
let scriptPath = (h.command || '').replace(/^(bash|sh|node)\s+/, '').split(/\s+/)[0];
|
|
813
|
+
scriptPath = scriptPath.replace(/^~/, HOME);
|
|
814
|
+
if (!existsSync(scriptPath)) continue;
|
|
815
|
+
|
|
816
|
+
const name = scriptPath.split('/').pop();
|
|
817
|
+
const times = [];
|
|
818
|
+
|
|
819
|
+
for (let i = 0; i < RUNS; i++) {
|
|
820
|
+
const start = process.hrtime.bigint();
|
|
821
|
+
spawnSync('bash', [scriptPath], {
|
|
822
|
+
input: testInput,
|
|
823
|
+
timeout: 5000,
|
|
824
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
825
|
+
});
|
|
826
|
+
const end = process.hrtime.bigint();
|
|
827
|
+
times.push(Number(end - start) / 1_000_000); // ms
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
|
831
|
+
const max = Math.max(...times);
|
|
832
|
+
const min = Math.min(...times);
|
|
833
|
+
|
|
834
|
+
results.push({ name, trigger, avg, max, min, matcher: entry.matcher || '(all)' });
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Sort by avg time descending
|
|
840
|
+
results.sort((a, b) => b.avg - a.avg);
|
|
841
|
+
|
|
842
|
+
// Display
|
|
843
|
+
const maxAvg = results[0]?.avg || 1;
|
|
844
|
+
console.log(c.bold + ' Hook Avg Min Max Trigger' + c.reset);
|
|
845
|
+
console.log(' ' + '-'.repeat(75));
|
|
846
|
+
|
|
847
|
+
for (const r of results) {
|
|
848
|
+
const bar = '█'.repeat(Math.ceil(r.avg / maxAvg * 15));
|
|
849
|
+
const avgColor = r.avg > 100 ? c.red : r.avg > 50 ? c.yellow : c.green;
|
|
850
|
+
console.log(
|
|
851
|
+
' ' + r.name.padEnd(30) +
|
|
852
|
+
avgColor + r.avg.toFixed(1).padStart(6) + 'ms' + c.reset +
|
|
853
|
+
r.min.toFixed(1).padStart(6) + 'ms' +
|
|
854
|
+
r.max.toFixed(1).padStart(6) + 'ms' +
|
|
855
|
+
' ' + c.dim + r.trigger + c.reset
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
console.log();
|
|
860
|
+
const totalAvg = results.reduce((s, r) => s + r.avg, 0);
|
|
861
|
+
const slow = results.filter(r => r.avg > 50);
|
|
862
|
+
|
|
863
|
+
console.log(c.dim + ' Total avg per tool call: ' + totalAvg.toFixed(1) + 'ms (sum of all hooks on that trigger)' + c.reset);
|
|
864
|
+
if (slow.length > 0) {
|
|
865
|
+
console.log(c.yellow + ' ' + slow.length + ' hook(s) over 50ms — consider optimizing' + c.reset);
|
|
866
|
+
} else {
|
|
867
|
+
console.log(c.green + ' All hooks under 50ms — good performance' + c.reset);
|
|
868
|
+
}
|
|
869
|
+
console.log();
|
|
870
|
+
}
|
|
871
|
+
|
|
786
872
|
function share() {
|
|
787
873
|
console.log();
|
|
788
874
|
console.log(c.bold + ' cc-safe-setup --share' + c.reset);
|
|
@@ -1987,6 +2073,7 @@ async function main() {
|
|
|
1987
2073
|
if (FULL) return fullSetup();
|
|
1988
2074
|
if (DOCTOR) return doctor();
|
|
1989
2075
|
if (WATCH) return watch();
|
|
2076
|
+
if (BENCHMARK) return benchmark();
|
|
1990
2077
|
if (SHARE) return share();
|
|
1991
2078
|
if (DIFF_FILE) return diff(DIFF_FILE);
|
|
1992
2079
|
if (LINT) return lint();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "One command to make Claude Code safe for autonomous operation. 8 built-in
|
|
3
|
+
"version": "3.8.0",
|
|
4
|
+
"description": "One command to make Claude Code safe for autonomous operation. 8 built-in + 36 examples. 21 commands: create, audit, lint, diff, share, benchmark, watch, learn. 2,500+ daily npm downloads.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cc-safe-setup": "index.mjs"
|