cc-safe-setup 2.7.0 → 2.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/MIGRATION.md +224 -0
- package/README.md +11 -2
- package/index.mjs +257 -0
- package/package.json +1 -1
package/MIGRATION.md
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Migrating from Permissions-Only to Hooks
|
|
2
|
+
|
|
3
|
+
You've been using `permissions.allow` and `permissions.deny` to control Claude Code. It works — until it doesn't. This guide shows how to add hooks for the things permissions can't do.
|
|
4
|
+
|
|
5
|
+
## Why Migrate?
|
|
6
|
+
|
|
7
|
+
Permissions are binary: allow or deny. Hooks are programmable: inspect the command, check context, decide dynamically.
|
|
8
|
+
|
|
9
|
+
| What you want | Permissions | Hooks |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| Allow `git status` | `Bash(git status:*)` | Same, or auto-approve hook |
|
|
12
|
+
| Block `rm -rf /` but allow `rm -rf node_modules` | Can't — it's all or nothing | `destructive-guard.sh` checks the path |
|
|
13
|
+
| Block `git push --force` but allow `git push` | Can't | `branch-guard.sh` checks flags |
|
|
14
|
+
| Block `git add .env` but allow `git add src/` | Can't | `secret-guard.sh` checks the target |
|
|
15
|
+
| Auto-approve `cd /path && git log` | Can't — compound command | `cd-git-allow.sh` parses both parts |
|
|
16
|
+
| Warn when context is running low | Not a permission concept | `context-monitor.sh` tracks usage |
|
|
17
|
+
|
|
18
|
+
## Step 1: Audit Your Current Setup
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx cc-safe-setup --audit
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This scores your current settings (0-100) and shows what's missing.
|
|
25
|
+
|
|
26
|
+
Or paste your `settings.json` into the [web tool](https://yurukusa.github.io/cc-safe-setup/) — no npm required.
|
|
27
|
+
|
|
28
|
+
## Step 2: Keep Your Permissions, Add Hooks
|
|
29
|
+
|
|
30
|
+
**You don't need to remove your existing permissions.** Hooks and permissions work together:
|
|
31
|
+
|
|
32
|
+
1. Permissions run first (allow/deny the tool call)
|
|
33
|
+
2. If allowed, PreToolUse hooks run (can block with exit 2)
|
|
34
|
+
3. Tool executes
|
|
35
|
+
4. PostToolUse hooks run (can warn about issues)
|
|
36
|
+
|
|
37
|
+
This means you can keep your working `allow` rules and layer hooks on top for the edge cases.
|
|
38
|
+
|
|
39
|
+
### Before (permissions only)
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"permissions": {
|
|
44
|
+
"allow": [
|
|
45
|
+
"Bash(git:*)",
|
|
46
|
+
"Bash(npm:*)",
|
|
47
|
+
"Bash(node:*)",
|
|
48
|
+
"Read(*)",
|
|
49
|
+
"Edit(*)",
|
|
50
|
+
"Write(*)"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Problem:** `Bash(git:*)` allows `git push --force origin main`. No way to block it without also blocking `git push origin feature-branch`.
|
|
57
|
+
|
|
58
|
+
### After (permissions + hooks)
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"permissions": {
|
|
63
|
+
"allow": [
|
|
64
|
+
"Bash(git:*)",
|
|
65
|
+
"Bash(npm:*)",
|
|
66
|
+
"Bash(node:*)",
|
|
67
|
+
"Read(*)",
|
|
68
|
+
"Edit(*)",
|
|
69
|
+
"Write(*)"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
"hooks": {
|
|
73
|
+
"PreToolUse": [
|
|
74
|
+
{
|
|
75
|
+
"matcher": "Bash",
|
|
76
|
+
"hooks": [
|
|
77
|
+
{ "type": "command", "command": "~/.claude/hooks/destructive-guard.sh" },
|
|
78
|
+
{ "type": "command", "command": "~/.claude/hooks/branch-guard.sh" },
|
|
79
|
+
{ "type": "command", "command": "~/.claude/hooks/secret-guard.sh" },
|
|
80
|
+
{ "type": "command", "command": "~/.claude/hooks/comment-strip.sh" },
|
|
81
|
+
{ "type": "command", "command": "~/.claude/hooks/cd-git-allow.sh" }
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
"PostToolUse": [
|
|
86
|
+
{
|
|
87
|
+
"matcher": "Edit|Write",
|
|
88
|
+
"hooks": [
|
|
89
|
+
{ "type": "command", "command": "~/.claude/hooks/syntax-check.sh" }
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"matcher": "",
|
|
94
|
+
"hooks": [
|
|
95
|
+
{ "type": "command", "command": "~/.claude/hooks/context-monitor.sh" }
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
"Stop": [
|
|
100
|
+
{
|
|
101
|
+
"matcher": "",
|
|
102
|
+
"hooks": [
|
|
103
|
+
{ "type": "command", "command": "~/.claude/hooks/api-error-alert.sh" }
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Result:** `git push origin feature-branch` still works. `git push --force` and `git push origin main` are blocked. `rm -rf node_modules` works. `rm -rf /` is blocked. All without changing your `allow` rules.
|
|
112
|
+
|
|
113
|
+
## Step 3: Install Everything Automatically
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npx cc-safe-setup
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
This creates the hook scripts and merges the config into your existing settings.json. Your current `permissions` are preserved.
|
|
120
|
+
|
|
121
|
+
## Step 4: Verify
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
npx cc-safe-setup --verify
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Tests each hook with sample inputs. If something fails:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npx cc-safe-setup --doctor
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
This checks jq installation, file permissions, shebang lines, and common misconfigurations.
|
|
134
|
+
|
|
135
|
+
## Common Migration Patterns
|
|
136
|
+
|
|
137
|
+
### "I use `Bash(*)` to auto-approve everything"
|
|
138
|
+
|
|
139
|
+
You're trading speed for safety. Keep `Bash(*)` but add hooks to catch the dangerous commands:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npx cc-safe-setup
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Now `Bash(*)` auto-approves commands, but hooks still run and block dangerous ones. Best of both worlds.
|
|
146
|
+
|
|
147
|
+
### "I use `dontAsk` mode"
|
|
148
|
+
|
|
149
|
+
Same approach. `dontAsk` skips permission prompts but **hooks still fire**. Install hooks and you're protected.
|
|
150
|
+
|
|
151
|
+
### "I use `bypassPermissions`"
|
|
152
|
+
|
|
153
|
+
**Warning:** `bypassPermissions` skips **everything** including hooks. Switch to `dontAsk` instead — same UX (no prompts) but hooks still protect you.
|
|
154
|
+
|
|
155
|
+
### "I have deny rules for specific commands"
|
|
156
|
+
|
|
157
|
+
Deny rules work but are fragile. `deny: ["Bash(rm -rf:*)"]` doesn't catch `rm -r -f` or `sudo rm -rf`. A hook can use regex to catch all variants.
|
|
158
|
+
|
|
159
|
+
You can keep your deny rules as a first line of defense and add hooks as a second layer.
|
|
160
|
+
|
|
161
|
+
## Hook Development Reference
|
|
162
|
+
|
|
163
|
+
### How hooks receive data
|
|
164
|
+
|
|
165
|
+
Hooks read JSON from stdin:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"tool_name": "Bash",
|
|
170
|
+
"tool_input": {
|
|
171
|
+
"command": "git push origin main"
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Exit codes
|
|
177
|
+
|
|
178
|
+
| Code | Meaning |
|
|
179
|
+
|------|---------|
|
|
180
|
+
| 0 | Allow (or no opinion) |
|
|
181
|
+
| 2 | **Block** — command does not execute |
|
|
182
|
+
| Other | Error (treated as allow) |
|
|
183
|
+
|
|
184
|
+
### Returning data
|
|
185
|
+
|
|
186
|
+
Hooks can modify the input or make permission decisions by writing JSON to stdout:
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"hookSpecificOutput": {
|
|
191
|
+
"hookEventName": "PreToolUse",
|
|
192
|
+
"permissionDecision": "allow",
|
|
193
|
+
"permissionDecisionReason": "auto-approved by hook"
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Testing a hook manually
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
echo '{"tool_input":{"command":"rm -rf /"}}' | bash ~/.claude/hooks/destructive-guard.sh
|
|
202
|
+
echo $? # Should be 2 (blocked)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Monitor Your Hooks
|
|
206
|
+
|
|
207
|
+
Watch what's being blocked in real time:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
npx cc-safe-setup --watch
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
After a few sessions, generate custom hooks from your block patterns:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npx cc-safe-setup --learn
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Resources
|
|
220
|
+
|
|
221
|
+
- [Official Hooks Documentation](https://docs.anthropic.com/en/docs/claude-code/hooks)
|
|
222
|
+
- [COOKBOOK.md](https://github.com/yurukusa/claude-code-hooks/blob/main/COOKBOOK.md) — 19 hook recipes
|
|
223
|
+
- [cc-safe-setup](https://github.com/yurukusa/cc-safe-setup) — automated setup
|
|
224
|
+
- [Web Audit Tool](https://yurukusa.github.io/cc-safe-setup/) — browser-based setup generator
|
package/README.md
CHANGED
|
@@ -78,6 +78,10 @@ Safe to run multiple times. Existing settings are preserved. A backup is created
|
|
|
78
78
|
|
|
79
79
|
**Verify hooks work:** `npx cc-safe-setup --verify` — sends test inputs to each hook and confirms they block/allow correctly.
|
|
80
80
|
|
|
81
|
+
**Troubleshoot:** `npx cc-safe-setup --doctor` — diagnoses why hooks aren't working (jq, permissions, paths, shebang).
|
|
82
|
+
|
|
83
|
+
**Live monitor:** `npx cc-safe-setup --watch` — real-time dashboard of blocked commands during autonomous sessions.
|
|
84
|
+
|
|
81
85
|
**Uninstall:** `npx cc-safe-setup --uninstall` — removes all hooks and cleans settings.json.
|
|
82
86
|
|
|
83
87
|
**Requires:** [jq](https://jqlang.github.io/jq/) for JSON parsing (`brew install jq` / `apt install jq`).
|
|
@@ -204,11 +208,16 @@ Or browse all available examples in [`examples/`](examples/):
|
|
|
204
208
|
|
|
205
209
|
**[SAFETY_CHECKLIST.md](SAFETY_CHECKLIST.md)** — Copy-paste checklist for before/during/after autonomous sessions.
|
|
206
210
|
|
|
211
|
+
## Migration Guide
|
|
212
|
+
|
|
213
|
+
**[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.
|
|
214
|
+
|
|
207
215
|
## Learn More
|
|
208
216
|
|
|
209
|
-
- [Official Hooks Reference](https://
|
|
210
|
-
- [Hooks Cookbook](https://github.com/yurukusa/claude-code-hooks/blob/main/COOKBOOK.md) —
|
|
217
|
+
- [Official Hooks Reference](https://docs.anthropic.com/en/docs/claude-code/hooks) — Claude Code hooks documentation
|
|
218
|
+
- [Hooks Cookbook](https://github.com/yurukusa/claude-code-hooks/blob/main/COOKBOOK.md) — 19 ready-to-use recipes from real GitHub Issues
|
|
211
219
|
- [Japanese guide (Qiita)](https://qiita.com/yurukusa/items/a9714b33f5d974e8f1e8) — この記事の日本語解説
|
|
220
|
+
- [Hook Test Runner](https://github.com/yurukusa/cc-hook-test) — `npx cc-hook-test <hook.sh>` to auto-test any hook
|
|
212
221
|
- [The incident that inspired this tool](https://github.com/anthropics/claude-code/issues/36339) — NTFS junction rm -rf
|
|
213
222
|
|
|
214
223
|
## FAQ
|
package/index.mjs
CHANGED
|
@@ -74,6 +74,10 @@ const SCAN = process.argv.includes('--scan');
|
|
|
74
74
|
const FULL = process.argv.includes('--full');
|
|
75
75
|
const DOCTOR = process.argv.includes('--doctor');
|
|
76
76
|
const WATCH = process.argv.includes('--watch');
|
|
77
|
+
const EXPORT = process.argv.includes('--export');
|
|
78
|
+
const IMPORT_IDX = process.argv.findIndex(a => a === '--import');
|
|
79
|
+
const IMPORT_FILE = IMPORT_IDX !== -1 ? process.argv[IMPORT_IDX + 1] : null;
|
|
80
|
+
const STATS = process.argv.includes('--stats');
|
|
77
81
|
|
|
78
82
|
if (HELP) {
|
|
79
83
|
console.log(`
|
|
@@ -94,6 +98,9 @@ if (HELP) {
|
|
|
94
98
|
npx cc-safe-setup --learn Learn from your block history
|
|
95
99
|
npx cc-safe-setup --doctor Diagnose why hooks aren't working
|
|
96
100
|
npx cc-safe-setup --watch Live dashboard of blocked commands
|
|
101
|
+
npx cc-safe-setup --stats Block statistics and patterns report
|
|
102
|
+
npx cc-safe-setup --export Export hooks config for team sharing
|
|
103
|
+
npx cc-safe-setup --import <file> Import hooks from exported config
|
|
97
104
|
npx cc-safe-setup --help Show this help
|
|
98
105
|
|
|
99
106
|
Hooks installed:
|
|
@@ -742,6 +749,253 @@ async function fullSetup() {
|
|
|
742
749
|
console.log();
|
|
743
750
|
}
|
|
744
751
|
|
|
752
|
+
async function stats() {
|
|
753
|
+
const LOG_PATH = join(HOME, '.claude', 'blocked-commands.log');
|
|
754
|
+
|
|
755
|
+
console.log();
|
|
756
|
+
console.log(c.bold + ' cc-safe-setup --stats' + c.reset);
|
|
757
|
+
console.log(c.dim + ' Block statistics from your hook history' + c.reset);
|
|
758
|
+
console.log();
|
|
759
|
+
|
|
760
|
+
if (!existsSync(LOG_PATH)) {
|
|
761
|
+
console.log(c.dim + ' No blocked-commands.log found. Hooks haven\'t blocked anything yet.' + c.reset);
|
|
762
|
+
console.log(c.dim + ' This is normal if you just installed hooks.' + c.reset);
|
|
763
|
+
console.log();
|
|
764
|
+
process.exit(0);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const lines = readFileSync(LOG_PATH, 'utf-8').split('\n').filter(l => l.trim());
|
|
768
|
+
if (lines.length === 0) {
|
|
769
|
+
console.log(c.dim + ' Log is empty. No blocks recorded yet.' + c.reset);
|
|
770
|
+
console.log();
|
|
771
|
+
process.exit(0);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Parse log entries: [timestamp] BLOCKED: reason | cmd: command
|
|
775
|
+
const entries = [];
|
|
776
|
+
const reasonCounts = {};
|
|
777
|
+
const hourCounts = {};
|
|
778
|
+
const dayCounts = {};
|
|
779
|
+
const commandPatterns = {};
|
|
780
|
+
|
|
781
|
+
for (const line of lines) {
|
|
782
|
+
const match = line.match(/^\[([^\]]+)\]\s*BLOCKED:\s*(.+?)\s*\|\s*cmd:\s*(.+)$/);
|
|
783
|
+
if (!match) continue;
|
|
784
|
+
|
|
785
|
+
const [, ts, reason, cmd] = match;
|
|
786
|
+
const date = new Date(ts);
|
|
787
|
+
const day = ts.split('T')[0];
|
|
788
|
+
const hour = date.getHours();
|
|
789
|
+
|
|
790
|
+
entries.push({ ts, reason: reason.trim(), cmd: cmd.trim(), date, day, hour });
|
|
791
|
+
|
|
792
|
+
// Count reasons
|
|
793
|
+
const r = reason.trim();
|
|
794
|
+
reasonCounts[r] = (reasonCounts[r] || 0) + 1;
|
|
795
|
+
|
|
796
|
+
// Count by hour
|
|
797
|
+
hourCounts[hour] = (hourCounts[hour] || 0) + 1;
|
|
798
|
+
|
|
799
|
+
// Count by day
|
|
800
|
+
dayCounts[day] = (dayCounts[day] || 0) + 1;
|
|
801
|
+
|
|
802
|
+
// Categorize commands
|
|
803
|
+
const cmdLower = cmd.toLowerCase();
|
|
804
|
+
let pattern = 'other';
|
|
805
|
+
if (cmdLower.includes('rm ')) pattern = 'rm (delete)';
|
|
806
|
+
else if (cmdLower.includes('git push')) pattern = 'git push';
|
|
807
|
+
else if (cmdLower.includes('git reset')) pattern = 'git reset';
|
|
808
|
+
else if (cmdLower.includes('git clean')) pattern = 'git clean';
|
|
809
|
+
else if (cmdLower.includes('git add')) pattern = 'git add (secrets)';
|
|
810
|
+
else if (cmdLower.includes('remove-item')) pattern = 'PowerShell delete';
|
|
811
|
+
else if (cmdLower.includes('git checkout') || cmdLower.includes('git switch')) pattern = 'git checkout --force';
|
|
812
|
+
commandPatterns[pattern] = (commandPatterns[pattern] || 0) + 1;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (entries.length === 0) {
|
|
816
|
+
console.log(c.dim + ' No parseable entries in log.' + c.reset);
|
|
817
|
+
console.log();
|
|
818
|
+
process.exit(0);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Summary
|
|
822
|
+
const firstDate = entries[0].day;
|
|
823
|
+
const lastDate = entries[entries.length - 1].day;
|
|
824
|
+
const daySpan = Object.keys(dayCounts).length;
|
|
825
|
+
|
|
826
|
+
console.log(c.bold + ' Summary' + c.reset);
|
|
827
|
+
console.log(' Total blocks: ' + c.bold + entries.length + c.reset);
|
|
828
|
+
console.log(' Period: ' + firstDate + ' to ' + lastDate + ' (' + daySpan + ' days)');
|
|
829
|
+
console.log(' Average: ' + (entries.length / Math.max(daySpan, 1)).toFixed(1) + ' blocks/day');
|
|
830
|
+
console.log();
|
|
831
|
+
|
|
832
|
+
// Top reasons
|
|
833
|
+
console.log(c.bold + ' Top Block Reasons' + c.reset);
|
|
834
|
+
const sortedReasons = Object.entries(reasonCounts).sort((a, b) => b[1] - a[1]);
|
|
835
|
+
const maxReasonCount = sortedReasons[0]?.[1] || 1;
|
|
836
|
+
for (const [reason, count] of sortedReasons.slice(0, 8)) {
|
|
837
|
+
const bar = '█'.repeat(Math.ceil(count / maxReasonCount * 20));
|
|
838
|
+
const pct = ((count / entries.length) * 100).toFixed(0);
|
|
839
|
+
console.log(' ' + c.red + bar + c.reset + ' ' + count + ' (' + pct + '%) ' + reason);
|
|
840
|
+
}
|
|
841
|
+
console.log();
|
|
842
|
+
|
|
843
|
+
// Command categories
|
|
844
|
+
console.log(c.bold + ' Command Categories' + c.reset);
|
|
845
|
+
const sortedPatterns = Object.entries(commandPatterns).sort((a, b) => b[1] - a[1]);
|
|
846
|
+
for (const [pattern, count] of sortedPatterns) {
|
|
847
|
+
const pct = ((count / entries.length) * 100).toFixed(0);
|
|
848
|
+
console.log(' ' + c.yellow + count.toString().padStart(4) + c.reset + ' ' + pattern + ' (' + pct + '%)');
|
|
849
|
+
}
|
|
850
|
+
console.log();
|
|
851
|
+
|
|
852
|
+
// Activity by hour
|
|
853
|
+
console.log(c.bold + ' Blocks by Hour' + c.reset);
|
|
854
|
+
const maxHour = Math.max(...Object.values(hourCounts), 1);
|
|
855
|
+
for (let h = 0; h < 24; h++) {
|
|
856
|
+
const count = hourCounts[h] || 0;
|
|
857
|
+
if (count === 0) continue;
|
|
858
|
+
const bar = '▓'.repeat(Math.ceil(count / maxHour * 15));
|
|
859
|
+
console.log(' ' + h.toString().padStart(2) + ':00 ' + c.blue + bar + c.reset + ' ' + count);
|
|
860
|
+
}
|
|
861
|
+
console.log();
|
|
862
|
+
|
|
863
|
+
// Recent blocks (last 5)
|
|
864
|
+
console.log(c.bold + ' Recent Blocks' + c.reset);
|
|
865
|
+
for (const entry of entries.slice(-5)) {
|
|
866
|
+
const time = entry.ts.replace(/T/, ' ').replace(/\+.*/, '');
|
|
867
|
+
console.log(' ' + c.dim + time + c.reset + ' ' + entry.reason);
|
|
868
|
+
console.log(' ' + c.dim + entry.cmd.slice(0, 100) + c.reset);
|
|
869
|
+
}
|
|
870
|
+
console.log();
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
async function exportConfig() {
|
|
874
|
+
console.log();
|
|
875
|
+
console.log(c.bold + ' cc-safe-setup --export' + c.reset);
|
|
876
|
+
console.log(c.dim + ' Exporting hooks config for team sharing...' + c.reset);
|
|
877
|
+
console.log();
|
|
878
|
+
|
|
879
|
+
if (!existsSync(SETTINGS_PATH)) {
|
|
880
|
+
console.log(c.red + ' No settings.json found. Run npx cc-safe-setup first.' + c.reset);
|
|
881
|
+
process.exit(1);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
885
|
+
const hooks = settings.hooks || {};
|
|
886
|
+
|
|
887
|
+
// Collect installed hook scripts
|
|
888
|
+
const exportData = {
|
|
889
|
+
version: '1.0',
|
|
890
|
+
generator: 'cc-safe-setup',
|
|
891
|
+
exported_at: new Date().toISOString(),
|
|
892
|
+
hooks: {},
|
|
893
|
+
scripts: {},
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
// Copy hook configuration
|
|
897
|
+
exportData.hooks = JSON.parse(JSON.stringify(hooks));
|
|
898
|
+
|
|
899
|
+
// Read and embed script contents
|
|
900
|
+
const scriptPaths = new Set();
|
|
901
|
+
for (const trigger of Object.keys(hooks)) {
|
|
902
|
+
for (const entry of hooks[trigger]) {
|
|
903
|
+
for (const h of (entry.hooks || [])) {
|
|
904
|
+
if (h.type === 'command' && h.command) {
|
|
905
|
+
let scriptPath = h.command.replace(/^(bash|sh|node)\s+/, '').split(/\s+/)[0];
|
|
906
|
+
scriptPath = scriptPath.replace(/^~/, HOME);
|
|
907
|
+
if (existsSync(scriptPath)) {
|
|
908
|
+
const relName = scriptPath.replace(HOME, '~');
|
|
909
|
+
exportData.scripts[relName] = readFileSync(scriptPath, 'utf-8');
|
|
910
|
+
scriptPaths.add(relName);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const outputFile = 'cc-safe-setup-export.json';
|
|
918
|
+
writeFileSync(outputFile, JSON.stringify(exportData, null, 2));
|
|
919
|
+
|
|
920
|
+
console.log(c.green + ' ✓ Exported to ' + outputFile + c.reset);
|
|
921
|
+
console.log(c.dim + ' Contains: ' + Object.keys(exportData.hooks).length + ' trigger types, ' + scriptPaths.size + ' hook scripts' + c.reset);
|
|
922
|
+
console.log();
|
|
923
|
+
console.log(c.dim + ' Share this file with your team. They can import with:' + c.reset);
|
|
924
|
+
console.log(c.bold + ' npx cc-safe-setup --import ' + outputFile + c.reset);
|
|
925
|
+
console.log();
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
async function importConfig(file) {
|
|
929
|
+
console.log();
|
|
930
|
+
console.log(c.bold + ' cc-safe-setup --import ' + file + c.reset);
|
|
931
|
+
console.log(c.dim + ' Importing hooks config...' + c.reset);
|
|
932
|
+
console.log();
|
|
933
|
+
|
|
934
|
+
if (!existsSync(file)) {
|
|
935
|
+
console.log(c.red + ' File not found: ' + file + c.reset);
|
|
936
|
+
process.exit(1);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
let exportData;
|
|
940
|
+
try {
|
|
941
|
+
exportData = JSON.parse(readFileSync(file, 'utf-8'));
|
|
942
|
+
} catch (e) {
|
|
943
|
+
console.log(c.red + ' Invalid JSON in ' + file + c.reset);
|
|
944
|
+
process.exit(1);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (!exportData.hooks || !exportData.scripts) {
|
|
948
|
+
console.log(c.red + ' Invalid export file (missing hooks or scripts section)' + c.reset);
|
|
949
|
+
process.exit(1);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Install scripts
|
|
953
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
954
|
+
let installed = 0;
|
|
955
|
+
|
|
956
|
+
for (const [relPath, content] of Object.entries(exportData.scripts)) {
|
|
957
|
+
const absPath = relPath.replace(/^~/, HOME);
|
|
958
|
+
const dir = dirname(absPath);
|
|
959
|
+
mkdirSync(dir, { recursive: true });
|
|
960
|
+
writeFileSync(absPath, content);
|
|
961
|
+
chmodSync(absPath, 0o755);
|
|
962
|
+
console.log(c.green + ' ✓ ' + c.reset + relPath);
|
|
963
|
+
installed++;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Merge hooks into settings.json
|
|
967
|
+
let settings = {};
|
|
968
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
969
|
+
try {
|
|
970
|
+
settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
971
|
+
} catch {}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (!settings.hooks) settings.hooks = {};
|
|
975
|
+
|
|
976
|
+
for (const [trigger, entries] of Object.entries(exportData.hooks)) {
|
|
977
|
+
if (!settings.hooks[trigger]) settings.hooks[trigger] = [];
|
|
978
|
+
// Add entries that don't already exist (by command path)
|
|
979
|
+
const existing = new Set(
|
|
980
|
+
settings.hooks[trigger].flatMap(e => (e.hooks || []).map(h => h.command))
|
|
981
|
+
);
|
|
982
|
+
for (const entry of entries) {
|
|
983
|
+
const newCommands = (entry.hooks || []).filter(h => !existing.has(h.command));
|
|
984
|
+
if (newCommands.length > 0) {
|
|
985
|
+
settings.hooks[trigger].push({ ...entry, hooks: newCommands });
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
|
|
991
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
992
|
+
|
|
993
|
+
console.log();
|
|
994
|
+
console.log(c.bold + c.green + ' ✓ Imported ' + installed + ' hook scripts' + c.reset);
|
|
995
|
+
console.log(c.dim + ' Restart Claude Code to activate.' + c.reset);
|
|
996
|
+
console.log();
|
|
997
|
+
}
|
|
998
|
+
|
|
745
999
|
async function watch() {
|
|
746
1000
|
const { spawn } = await import('child_process');
|
|
747
1001
|
const { createReadStream, watchFile } = await import('fs');
|
|
@@ -1173,6 +1427,9 @@ async function main() {
|
|
|
1173
1427
|
if (FULL) return fullSetup();
|
|
1174
1428
|
if (DOCTOR) return doctor();
|
|
1175
1429
|
if (WATCH) return watch();
|
|
1430
|
+
if (STATS) return stats();
|
|
1431
|
+
if (EXPORT) return exportConfig();
|
|
1432
|
+
if (IMPORT_FILE) return importConfig(IMPORT_FILE);
|
|
1176
1433
|
|
|
1177
1434
|
console.log();
|
|
1178
1435
|
console.log(c.bold + ' cc-safe-setup' + c.reset);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
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.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"bin": {
|