cc-safe-setup 2.6.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 +224 -0
- package/README.md +10 -2
- package/index.mjs +247 -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,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://
|
|
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) — この記事の日本語解説
|
|
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
|
@@ -73,6 +73,10 @@ const LEARN = process.argv.includes('--learn');
|
|
|
73
73
|
const SCAN = process.argv.includes('--scan');
|
|
74
74
|
const FULL = process.argv.includes('--full');
|
|
75
75
|
const DOCTOR = process.argv.includes('--doctor');
|
|
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;
|
|
76
80
|
|
|
77
81
|
if (HELP) {
|
|
78
82
|
console.log(`
|
|
@@ -92,6 +96,9 @@ if (HELP) {
|
|
|
92
96
|
npx cc-safe-setup --scan Detect tech stack, recommend hooks
|
|
93
97
|
npx cc-safe-setup --learn Learn from your block history
|
|
94
98
|
npx cc-safe-setup --doctor Diagnose why hooks aren't working
|
|
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
|
|
95
102
|
npx cc-safe-setup --help Show this help
|
|
96
103
|
|
|
97
104
|
Hooks installed:
|
|
@@ -740,6 +747,243 @@ async function fullSetup() {
|
|
|
740
747
|
console.log();
|
|
741
748
|
}
|
|
742
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
|
+
|
|
876
|
+
async function watch() {
|
|
877
|
+
const { spawn } = await import('child_process');
|
|
878
|
+
const { createReadStream, watchFile } = await import('fs');
|
|
879
|
+
const { createInterface: createRL } = await import('readline');
|
|
880
|
+
|
|
881
|
+
const LOG_PATH = join(HOME, '.claude', 'blocked-commands.log');
|
|
882
|
+
const ERROR_LOG = join(HOME, '.claude', 'session-errors.log');
|
|
883
|
+
|
|
884
|
+
console.log();
|
|
885
|
+
console.log(c.bold + ' cc-safe-setup --watch' + c.reset);
|
|
886
|
+
console.log(c.dim + ' Live safety dashboard — watching blocked commands' + c.reset);
|
|
887
|
+
console.log(c.dim + ' Log: ' + LOG_PATH + c.reset);
|
|
888
|
+
console.log();
|
|
889
|
+
|
|
890
|
+
let blockCount = 0;
|
|
891
|
+
let lastPrint = 0;
|
|
892
|
+
|
|
893
|
+
function formatLine(line) {
|
|
894
|
+
// Format: [2026-03-24T01:30:00+09:00] BLOCKED: reason | cmd: actual command
|
|
895
|
+
const match = line.match(/^\[([^\]]+)\]\s*BLOCKED:\s*(.+?)\s*\|\s*cmd:\s*(.+)$/);
|
|
896
|
+
if (!match) return c.dim + ' ' + line + c.reset;
|
|
897
|
+
|
|
898
|
+
const [, ts, reason, cmd] = match;
|
|
899
|
+
const time = ts.replace(/T/, ' ').replace(/\+.*/, '');
|
|
900
|
+
blockCount++;
|
|
901
|
+
|
|
902
|
+
let severity = c.yellow;
|
|
903
|
+
if (reason.match(/rm|reset|clean|Remove-Item|drop/i)) severity = c.red;
|
|
904
|
+
if (reason.match(/push|force/i)) severity = c.red;
|
|
905
|
+
if (reason.match(/env|secret|credential/i)) severity = c.red;
|
|
906
|
+
|
|
907
|
+
return severity + ' BLOCKED' + c.reset + ' ' + c.dim + time + c.reset + '\n' +
|
|
908
|
+
' ' + c.bold + reason.trim() + c.reset + '\n' +
|
|
909
|
+
' ' + c.dim + cmd.trim().slice(0, 120) + c.reset;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function printStats() {
|
|
913
|
+
const now = Date.now();
|
|
914
|
+
if (now - lastPrint < 30000) return;
|
|
915
|
+
lastPrint = now;
|
|
916
|
+
console.log(c.dim + ' --- ' + blockCount + ' blocks total | ' + new Date().toLocaleTimeString() + ' ---' + c.reset);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Print existing log entries
|
|
920
|
+
if (existsSync(LOG_PATH)) {
|
|
921
|
+
const rl = createRL({ input: createReadStream(LOG_PATH) });
|
|
922
|
+
for await (const line of rl) {
|
|
923
|
+
if (line.trim()) console.log(formatLine(line));
|
|
924
|
+
}
|
|
925
|
+
if (blockCount > 0) {
|
|
926
|
+
console.log();
|
|
927
|
+
console.log(c.dim + ' === History: ' + blockCount + ' blocks ===' + c.reset);
|
|
928
|
+
console.log(c.dim + ' Watching for new blocks... (Ctrl+C to stop)' + c.reset);
|
|
929
|
+
console.log();
|
|
930
|
+
}
|
|
931
|
+
} else {
|
|
932
|
+
console.log(c.dim + ' No blocked-commands.log yet. Hooks will create it on first block.' + c.reset);
|
|
933
|
+
console.log(c.dim + ' Watching... (Ctrl+C to stop)' + c.reset);
|
|
934
|
+
console.log();
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Watch for new entries using tail -f
|
|
938
|
+
let tailProcess;
|
|
939
|
+
try {
|
|
940
|
+
// Ensure log file exists for tail
|
|
941
|
+
if (!existsSync(LOG_PATH)) {
|
|
942
|
+
const { mkdirSync: mkDir, writeFileSync: writeFile } = await import('fs');
|
|
943
|
+
mkDir(dirname(LOG_PATH), { recursive: true });
|
|
944
|
+
writeFile(LOG_PATH, '', 'utf-8');
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
tailProcess = spawn('tail', ['-f', '-n', '0', LOG_PATH], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
948
|
+
|
|
949
|
+
const tailRL = createRL({ input: tailProcess.stdout });
|
|
950
|
+
for await (const line of tailRL) {
|
|
951
|
+
if (line.trim()) {
|
|
952
|
+
console.log(formatLine(line));
|
|
953
|
+
printStats();
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
} catch (e) {
|
|
957
|
+
// tail not available — fall back to polling
|
|
958
|
+
let lastSize = 0;
|
|
959
|
+
try {
|
|
960
|
+
const { statSync } = await import('fs');
|
|
961
|
+
lastSize = statSync(LOG_PATH).size;
|
|
962
|
+
} catch {}
|
|
963
|
+
|
|
964
|
+
console.log(c.dim + ' (tail not available, using polling)' + c.reset);
|
|
965
|
+
|
|
966
|
+
setInterval(async () => {
|
|
967
|
+
try {
|
|
968
|
+
const { statSync, readFileSync: readFile } = await import('fs');
|
|
969
|
+
const stat = statSync(LOG_PATH);
|
|
970
|
+
if (stat.size > lastSize) {
|
|
971
|
+
const content = readFile(LOG_PATH, 'utf-8');
|
|
972
|
+
const lines = content.split('\n').slice(-10);
|
|
973
|
+
for (const line of lines) {
|
|
974
|
+
if (line.trim()) console.log(formatLine(line));
|
|
975
|
+
}
|
|
976
|
+
lastSize = stat.size;
|
|
977
|
+
printStats();
|
|
978
|
+
}
|
|
979
|
+
} catch {}
|
|
980
|
+
}, 2000);
|
|
981
|
+
|
|
982
|
+
// Keep process alive
|
|
983
|
+
await new Promise(() => {});
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
743
987
|
async function doctor() {
|
|
744
988
|
const { execSync, spawnSync } = await import('child_process');
|
|
745
989
|
const { statSync, readdirSync } = await import('fs');
|
|
@@ -1059,6 +1303,9 @@ async function main() {
|
|
|
1059
1303
|
if (SCAN) return scan();
|
|
1060
1304
|
if (FULL) return fullSetup();
|
|
1061
1305
|
if (DOCTOR) return doctor();
|
|
1306
|
+
if (WATCH) return watch();
|
|
1307
|
+
if (EXPORT) return exportConfig();
|
|
1308
|
+
if (IMPORT_FILE) return importConfig(IMPORT_FILE);
|
|
1062
1309
|
|
|
1063
1310
|
console.log();
|
|
1064
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.
|
|
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": {
|