cc-safe-setup 2.7.0 → 2.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/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,10 +208,14 @@ 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://code.claude.com/docs/en/hooks) — Claude Code hooks documentation
210
- - [Hooks Cookbook](https://github.com/yurukusa/claude-code-hooks/blob/main/COOKBOOK.md) — 18 ready-to-use recipes from real GitHub Issues
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) — この記事の日本語解説
212
220
  - [The incident that inspired this tool](https://github.com/anthropics/claude-code/issues/36339) — NTFS junction rm -rf
213
221
 
package/index.mjs CHANGED
@@ -74,6 +74,9 @@ 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;
77
80
 
78
81
  if (HELP) {
79
82
  console.log(`
@@ -94,6 +97,8 @@ if (HELP) {
94
97
  npx cc-safe-setup --learn Learn from your block history
95
98
  npx cc-safe-setup --doctor Diagnose why hooks aren't working
96
99
  npx cc-safe-setup --watch Live dashboard of blocked commands
100
+ npx cc-safe-setup --export Export hooks config for team sharing
101
+ npx cc-safe-setup --import <file> Import hooks from exported config
97
102
  npx cc-safe-setup --help Show this help
98
103
 
99
104
  Hooks installed:
@@ -742,6 +747,132 @@ async function fullSetup() {
742
747
  console.log();
743
748
  }
744
749
 
750
+ async function exportConfig() {
751
+ console.log();
752
+ console.log(c.bold + ' cc-safe-setup --export' + c.reset);
753
+ console.log(c.dim + ' Exporting hooks config for team sharing...' + c.reset);
754
+ console.log();
755
+
756
+ if (!existsSync(SETTINGS_PATH)) {
757
+ console.log(c.red + ' No settings.json found. Run npx cc-safe-setup first.' + c.reset);
758
+ process.exit(1);
759
+ }
760
+
761
+ const settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
762
+ const hooks = settings.hooks || {};
763
+
764
+ // Collect installed hook scripts
765
+ const exportData = {
766
+ version: '1.0',
767
+ generator: 'cc-safe-setup',
768
+ exported_at: new Date().toISOString(),
769
+ hooks: {},
770
+ scripts: {},
771
+ };
772
+
773
+ // Copy hook configuration
774
+ exportData.hooks = JSON.parse(JSON.stringify(hooks));
775
+
776
+ // Read and embed script contents
777
+ const scriptPaths = new Set();
778
+ for (const trigger of Object.keys(hooks)) {
779
+ for (const entry of hooks[trigger]) {
780
+ for (const h of (entry.hooks || [])) {
781
+ if (h.type === 'command' && h.command) {
782
+ let scriptPath = h.command.replace(/^(bash|sh|node)\s+/, '').split(/\s+/)[0];
783
+ scriptPath = scriptPath.replace(/^~/, HOME);
784
+ if (existsSync(scriptPath)) {
785
+ const relName = scriptPath.replace(HOME, '~');
786
+ exportData.scripts[relName] = readFileSync(scriptPath, 'utf-8');
787
+ scriptPaths.add(relName);
788
+ }
789
+ }
790
+ }
791
+ }
792
+ }
793
+
794
+ const outputFile = 'cc-safe-setup-export.json';
795
+ writeFileSync(outputFile, JSON.stringify(exportData, null, 2));
796
+
797
+ console.log(c.green + ' ✓ Exported to ' + outputFile + c.reset);
798
+ console.log(c.dim + ' Contains: ' + Object.keys(exportData.hooks).length + ' trigger types, ' + scriptPaths.size + ' hook scripts' + c.reset);
799
+ console.log();
800
+ console.log(c.dim + ' Share this file with your team. They can import with:' + c.reset);
801
+ console.log(c.bold + ' npx cc-safe-setup --import ' + outputFile + c.reset);
802
+ console.log();
803
+ }
804
+
805
+ async function importConfig(file) {
806
+ console.log();
807
+ console.log(c.bold + ' cc-safe-setup --import ' + file + c.reset);
808
+ console.log(c.dim + ' Importing hooks config...' + c.reset);
809
+ console.log();
810
+
811
+ if (!existsSync(file)) {
812
+ console.log(c.red + ' File not found: ' + file + c.reset);
813
+ process.exit(1);
814
+ }
815
+
816
+ let exportData;
817
+ try {
818
+ exportData = JSON.parse(readFileSync(file, 'utf-8'));
819
+ } catch (e) {
820
+ console.log(c.red + ' Invalid JSON in ' + file + c.reset);
821
+ process.exit(1);
822
+ }
823
+
824
+ if (!exportData.hooks || !exportData.scripts) {
825
+ console.log(c.red + ' Invalid export file (missing hooks or scripts section)' + c.reset);
826
+ process.exit(1);
827
+ }
828
+
829
+ // Install scripts
830
+ mkdirSync(HOOKS_DIR, { recursive: true });
831
+ let installed = 0;
832
+
833
+ for (const [relPath, content] of Object.entries(exportData.scripts)) {
834
+ const absPath = relPath.replace(/^~/, HOME);
835
+ const dir = dirname(absPath);
836
+ mkdirSync(dir, { recursive: true });
837
+ writeFileSync(absPath, content);
838
+ chmodSync(absPath, 0o755);
839
+ console.log(c.green + ' ✓ ' + c.reset + relPath);
840
+ installed++;
841
+ }
842
+
843
+ // Merge hooks into settings.json
844
+ let settings = {};
845
+ if (existsSync(SETTINGS_PATH)) {
846
+ try {
847
+ settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
848
+ } catch {}
849
+ }
850
+
851
+ if (!settings.hooks) settings.hooks = {};
852
+
853
+ for (const [trigger, entries] of Object.entries(exportData.hooks)) {
854
+ if (!settings.hooks[trigger]) settings.hooks[trigger] = [];
855
+ // Add entries that don't already exist (by command path)
856
+ const existing = new Set(
857
+ settings.hooks[trigger].flatMap(e => (e.hooks || []).map(h => h.command))
858
+ );
859
+ for (const entry of entries) {
860
+ const newCommands = (entry.hooks || []).filter(h => !existing.has(h.command));
861
+ if (newCommands.length > 0) {
862
+ settings.hooks[trigger].push({ ...entry, hooks: newCommands });
863
+ }
864
+ }
865
+ }
866
+
867
+ mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
868
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
869
+
870
+ console.log();
871
+ console.log(c.bold + c.green + ' ✓ Imported ' + installed + ' hook scripts' + c.reset);
872
+ console.log(c.dim + ' Restart Claude Code to activate.' + c.reset);
873
+ console.log();
874
+ }
875
+
745
876
  async function watch() {
746
877
  const { spawn } = await import('child_process');
747
878
  const { createReadStream, watchFile } = await import('fs');
@@ -1173,6 +1304,8 @@ async function main() {
1173
1304
  if (FULL) return fullSetup();
1174
1305
  if (DOCTOR) return doctor();
1175
1306
  if (WATCH) return watch();
1307
+ if (EXPORT) return exportConfig();
1308
+ if (IMPORT_FILE) return importConfig(IMPORT_FILE);
1176
1309
 
1177
1310
  console.log();
1178
1311
  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.7.0",
3
+ "version": "2.8.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": {