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 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://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) — この記事の日本語解説
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.7.0",
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": {