cc-safe-setup 2.1.0 โ†’ 2.2.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/README.md CHANGED
@@ -128,6 +128,18 @@ Or start with the free hooks: [claude-code-hooks](https://github.com/yurukusa/cl
128
128
 
129
129
  ## Examples
130
130
 
131
+ ## Safety Audit
132
+
133
+ Check what's missing in your setup:
134
+
135
+ ```bash
136
+ npx cc-safe-setup --audit
137
+ ```
138
+
139
+ Analyzes 9 safety dimensions and gives you a score (0-100) with one-command fixes for each risk.
140
+
141
+ ## Examples
142
+
131
143
  Need custom hooks beyond the 8 built-in ones? Install any example with one command:
132
144
 
133
145
  ```bash
@@ -0,0 +1,147 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Claude Code Safety Audit</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0d1117; color: #c9d1d9; min-height: 100vh; padding: 2rem; }
10
+ .container { max-width: 720px; margin: 0 auto; }
11
+ h1 { font-size: 1.5rem; margin-bottom: 0.5rem; color: #f0f6fc; }
12
+ .subtitle { color: #8b949e; margin-bottom: 2rem; }
13
+ textarea { width: 100%; height: 200px; background: #161b22; border: 1px solid #30363d; border-radius: 6px; color: #c9d1d9; font-family: monospace; font-size: 13px; padding: 1rem; resize: vertical; }
14
+ textarea::placeholder { color: #484f58; }
15
+ button { background: #238636; color: #fff; border: none; padding: 0.75rem 1.5rem; border-radius: 6px; font-size: 1rem; cursor: pointer; margin-top: 1rem; }
16
+ button:hover { background: #2ea043; }
17
+ .results { margin-top: 2rem; }
18
+ .score { font-size: 2rem; font-weight: bold; margin: 1rem 0; }
19
+ .score.good { color: #3fb950; }
20
+ .score.mid { color: #d29922; }
21
+ .score.bad { color: #f85149; }
22
+ .risk { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 1rem; margin: 0.5rem 0; }
23
+ .risk .severity { font-weight: bold; margin-right: 0.5rem; }
24
+ .risk .severity.critical, .risk .severity.high { color: #f85149; }
25
+ .risk .severity.medium { color: #d29922; }
26
+ .risk .severity.low { color: #8b949e; }
27
+ .risk .fix { color: #8b949e; font-family: monospace; font-size: 12px; margin-top: 0.5rem; }
28
+ .good-item { color: #3fb950; margin: 0.25rem 0; }
29
+ .privacy { color: #484f58; font-size: 12px; margin-top: 2rem; text-align: center; }
30
+ a { color: #58a6ff; text-decoration: none; }
31
+ </style>
32
+ </head>
33
+ <body>
34
+ <div class="container">
35
+ <h1>Claude Code Safety Audit</h1>
36
+ <p class="subtitle">Paste your <code>~/.claude/settings.json</code> below. Nothing leaves your browser.</p>
37
+
38
+ <textarea id="settings" placeholder='{
39
+ "permissions": { "allow": ["Bash(git:*)"] },
40
+ "hooks": {
41
+ "PreToolUse": [{ "matcher": "Bash", "hooks": [{"type":"command","command":"..."}] }]
42
+ }
43
+ }'></textarea>
44
+ <br>
45
+ <button onclick="runAudit()">Run Audit</button>
46
+
47
+ <div class="results" id="results"></div>
48
+
49
+ <p class="privacy">๐Ÿ”’ 100% client-side. Your settings never leave this page. <a href="https://github.com/yurukusa/cc-safe-setup">Source</a></p>
50
+ </div>
51
+
52
+ <script>
53
+ function runAudit() {
54
+ const raw = document.getElementById('settings').value.trim();
55
+ const el = document.getElementById('results');
56
+
57
+ let settings;
58
+ try {
59
+ settings = JSON.parse(raw);
60
+ } catch(e) {
61
+ el.innerHTML = '<p style="color:#f85149">Invalid JSON. Paste your settings.json content.</p>';
62
+ return;
63
+ }
64
+
65
+ const risks = [];
66
+ const good = [];
67
+
68
+ const preHooks = settings.hooks?.PreToolUse || [];
69
+ const postHooks = settings.hooks?.PostToolUse || [];
70
+ const allCmds = JSON.stringify(preHooks).toLowerCase();
71
+
72
+ // 1. PreToolUse hooks
73
+ if (preHooks.length === 0) {
74
+ risks.push({ severity: 'CRITICAL', issue: 'No PreToolUse hooks โ€” rm -rf, git reset --hard run unchecked', fix: 'npx cc-safe-setup' });
75
+ } else {
76
+ good.push('PreToolUse hooks (' + preHooks.length + ')');
77
+ }
78
+
79
+ // 2. Destructive guard
80
+ if (!allCmds.match(/destructive|guard|block|rm.*rf|reset.*hard/)) {
81
+ risks.push({ severity: 'HIGH', issue: 'No destructive command blocker', fix: 'npx cc-safe-setup' });
82
+ } else good.push('Destructive command protection');
83
+
84
+ // 3. Branch guard
85
+ if (!allCmds.match(/branch|push|main|master/)) {
86
+ risks.push({ severity: 'HIGH', issue: 'No branch push protection', fix: 'npx cc-safe-setup' });
87
+ } else good.push('Branch push protection');
88
+
89
+ // 4. Secret guard
90
+ if (!allCmds.match(/secret|env|credential/)) {
91
+ risks.push({ severity: 'HIGH', issue: 'No secret leak protection (.env commits)', fix: 'npx cc-safe-setup' });
92
+ } else good.push('Secret leak protection');
93
+
94
+ // 5. Database wipe
95
+ if (!allCmds.match(/database|wipe|migrate|prisma/)) {
96
+ risks.push({ severity: 'MEDIUM', issue: 'No database wipe protection', fix: 'npx cc-safe-setup --install-example block-database-wipe' });
97
+ } else good.push('Database wipe protection');
98
+
99
+ // 6. PostToolUse
100
+ if (postHooks.length === 0) {
101
+ risks.push({ severity: 'MEDIUM', issue: 'No PostToolUse hooks (no syntax checking)', fix: 'npx cc-safe-setup' });
102
+ } else good.push('PostToolUse hooks (' + postHooks.length + ')');
103
+
104
+ // 7. Allow rules check
105
+ const allows = settings.permissions?.allow || [];
106
+ if (allows.includes('Bash(*)')) {
107
+ risks.push({ severity: 'MEDIUM', issue: 'Bash(*) in allow list โ€” all commands auto-approved without checks', fix: 'Use specific patterns like Bash(git:*) instead' });
108
+ }
109
+
110
+ // 8. Deny rules
111
+ const denies = settings.permissions?.deny || [];
112
+ if (denies.length > 0) good.push('Deny rules configured (' + denies.length + ')');
113
+
114
+ // Score
115
+ const score = Math.max(0, 100 - risks.reduce((s, r) => {
116
+ if (r.severity === 'CRITICAL') return s + 30;
117
+ if (r.severity === 'HIGH') return s + 20;
118
+ if (r.severity === 'MEDIUM') return s + 10;
119
+ return s + 5;
120
+ }, 0));
121
+
122
+ const scoreClass = score >= 80 ? 'good' : score >= 50 ? 'mid' : 'bad';
123
+
124
+ let html = '<div class="score ' + scoreClass + '">Safety Score: ' + score + '/100</div>';
125
+
126
+ if (good.length > 0) {
127
+ html += '<h3 style="color:#3fb950;margin:1rem 0 0.5rem">โœ“ Working</h3>';
128
+ html += good.map(g => '<div class="good-item">โœ“ ' + g + '</div>').join('');
129
+ }
130
+
131
+ if (risks.length > 0) {
132
+ html += '<h3 style="margin:1rem 0 0.5rem">โš  Risks (' + risks.length + ')</h3>';
133
+ html += risks.map(r =>
134
+ '<div class="risk"><span class="severity ' + r.severity.toLowerCase() + '">[' + r.severity + ']</span>' + r.issue +
135
+ '<div class="fix">Fix: ' + r.fix + '</div></div>'
136
+ ).join('');
137
+ }
138
+
139
+ if (risks.length === 0) {
140
+ html += '<p style="color:#3fb950;margin-top:1rem">No risks detected. Your setup looks solid.</p>';
141
+ }
142
+
143
+ el.innerHTML = html;
144
+ }
145
+ </script>
146
+ </body>
147
+ </html>
package/index.mjs CHANGED
@@ -69,6 +69,7 @@ const EXAMPLES = process.argv.includes('--examples') || process.argv.includes('-
69
69
  const INSTALL_EXAMPLE_IDX = process.argv.findIndex(a => a === '--install-example');
70
70
  const INSTALL_EXAMPLE = INSTALL_EXAMPLE_IDX !== -1 ? process.argv[INSTALL_EXAMPLE_IDX + 1] : null;
71
71
  const AUDIT = process.argv.includes('--audit');
72
+ const LEARN = process.argv.includes('--learn');
72
73
 
73
74
  if (HELP) {
74
75
  console.log(`
@@ -398,7 +399,7 @@ async function installExample(name) {
398
399
  console.log();
399
400
  }
400
401
 
401
- function audit() {
402
+ async function audit() {
402
403
  console.log();
403
404
  console.log(c.bold + ' cc-safe-setup --audit' + c.reset);
404
405
  console.log(c.dim + ' Analyzing your Claude Code safety setup...' + c.reset);
@@ -543,6 +544,141 @@ function audit() {
543
544
  return sum + 5;
544
545
  }, 0));
545
546
  console.log(c.bold + ' Safety Score: ' + (score >= 80 ? c.green : score >= 50 ? c.yellow : c.red) + score + '/100' + c.reset);
547
+
548
+ // --audit --fix: auto-fix what we can
549
+ if (process.argv.includes('--fix') && risks.length > 0) {
550
+ console.log();
551
+ console.log(c.bold + ' Applying fixes...' + c.reset);
552
+ const { execSync } = await import('child_process');
553
+ for (const r of risks) {
554
+ if (r.fix.startsWith('npx cc-safe-setup')) {
555
+ try {
556
+ const cmd = r.fix.replace('npx cc-safe-setup', 'node ' + process.argv[1]);
557
+ console.log(' ' + c.dim + 'โ†’ ' + r.fix + c.reset);
558
+ execSync(cmd, { stdio: 'inherit' });
559
+ } catch(e) {
560
+ console.log(' ' + c.red + ' Failed: ' + e.message + c.reset);
561
+ }
562
+ }
563
+ }
564
+ console.log();
565
+ console.log(c.green + ' Re-run --audit to verify fixes.' + c.reset);
566
+ } else if (risks.length > 0) {
567
+ console.log();
568
+ console.log(c.dim + ' Run with --fix to auto-apply: npx cc-safe-setup --audit --fix' + c.reset);
569
+ }
570
+ console.log();
571
+ }
572
+
573
+ function learn() {
574
+ console.log();
575
+ console.log(c.bold + ' cc-safe-setup --learn' + c.reset);
576
+ console.log(c.dim + ' Analyzing your blocked command history to generate custom protections...' + c.reset);
577
+ console.log();
578
+
579
+ const logPath = join(HOME, '.claude', 'blocked-commands.log');
580
+ if (!existsSync(logPath)) {
581
+ console.log(c.yellow + ' No blocked-commands.log found.' + c.reset);
582
+ console.log(c.dim + ' Install cc-safe-setup first, then use Claude Code normally.' + c.reset);
583
+ console.log(c.dim + ' Blocked commands are logged automatically. Re-run --learn after a few sessions.' + c.reset);
584
+ console.log();
585
+ return;
586
+ }
587
+
588
+ const log = readFileSync(logPath, 'utf-8');
589
+ const lines = log.split('\n').filter(l => l.trim());
590
+
591
+ if (lines.length === 0) {
592
+ console.log(c.green + ' No blocked commands in history. Your setup is catching nothing (or everything is safe).' + c.reset);
593
+ console.log();
594
+ return;
595
+ }
596
+
597
+ // Extract command patterns from blocked log
598
+ const patterns = {};
599
+ for (const line of lines) {
600
+ // Extract the command from log lines like "[2026-03-23 12:00:00] BLOCKED: rm -rf / (destructive-guard)"
601
+ const cmdMatch = line.match(/BLOCKED:\s*(.+?)(?:\s*\(|$)/);
602
+ if (cmdMatch) {
603
+ const cmd = cmdMatch[1].trim();
604
+ // Extract the base command (first word)
605
+ const base = cmd.split(/\s+/)[0];
606
+ if (!patterns[base]) patterns[base] = [];
607
+ patterns[base].push(cmd);
608
+ }
609
+ }
610
+
611
+ const uniqueBases = Object.keys(patterns);
612
+ if (uniqueBases.length === 0) {
613
+ console.log(c.dim + ' Could not parse patterns from log. Format may differ.' + c.reset);
614
+ console.log();
615
+ return;
616
+ }
617
+
618
+ console.log(c.bold + ' Patterns found (' + lines.length + ' blocked commands):' + c.reset);
619
+ console.log();
620
+
621
+ const recommendations = [];
622
+
623
+ for (const [base, cmds] of Object.entries(patterns)) {
624
+ const count = cmds.length;
625
+ const unique = [...new Set(cmds)];
626
+
627
+ if (count >= 3) {
628
+ console.log(' ' + c.red + 'โš ' + c.reset + ' ' + c.bold + base + c.reset + ' blocked ' + count + ' times');
629
+ for (const u of unique.slice(0, 3)) {
630
+ console.log(' ' + c.dim + u + c.reset);
631
+ }
632
+ if (unique.length > 3) console.log(' ' + c.dim + '... and ' + (unique.length - 3) + ' more' + c.reset);
633
+
634
+ recommendations.push({
635
+ command: base,
636
+ count,
637
+ examples: unique.slice(0, 5),
638
+ });
639
+ } else {
640
+ console.log(' ' + c.yellow + 'ยท' + c.reset + ' ' + base + ' (' + count + 'x)');
641
+ }
642
+ }
643
+
644
+ if (recommendations.length > 0) {
645
+ console.log();
646
+ console.log(c.bold + ' Recommendations:' + c.reset);
647
+ console.log();
648
+ for (const r of recommendations) {
649
+ console.log(' ' + c.green + 'โ†’' + c.reset + ' ' + r.command + ' is frequently blocked (' + r.count + 'x).');
650
+ console.log(' Consider adding a specific hook or adjusting your allow rules.');
651
+
652
+ // Generate a custom hook suggestion
653
+ const hookCode = `#!/bin/bash
654
+ # Auto-generated: block ${r.command} patterns (seen ${r.count} times)
655
+ CMD=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
656
+ [[ -z "$CMD" ]] && exit 0
657
+ if echo "$CMD" | grep -qE '^\\s*${r.command}\\b'; then
658
+ echo "BLOCKED: ${r.command} requires manual approval" >&2
659
+ exit 2
660
+ fi
661
+ exit 0`;
662
+
663
+ const hookPath = join(HOOKS_DIR, 'learned-block-' + r.command + '.sh');
664
+ console.log(' ' + c.dim + 'Suggested hook: ' + hookPath + c.reset);
665
+
666
+ if (process.argv.includes('--apply')) {
667
+ mkdirSync(HOOKS_DIR, { recursive: true });
668
+ writeFileSync(hookPath, hookCode);
669
+ chmodSync(hookPath, 0o755);
670
+ console.log(' ' + c.green + 'โœ“ Hook created' + c.reset);
671
+ }
672
+ }
673
+
674
+ if (!process.argv.includes('--apply')) {
675
+ console.log();
676
+ console.log(c.dim + ' Run with --apply to auto-create hooks: npx cc-safe-setup --learn --apply' + c.reset);
677
+ }
678
+ }
679
+
680
+ console.log();
681
+ console.log(c.bold + ' Summary: ' + lines.length + ' blocked commands, ' + uniqueBases.length + ' unique patterns, ' + recommendations.length + ' recommendations.' + c.reset);
546
682
  console.log();
547
683
  }
548
684
 
@@ -553,6 +689,7 @@ async function main() {
553
689
  if (EXAMPLES) return examples();
554
690
  if (INSTALL_EXAMPLE) return installExample(INSTALL_EXAMPLE);
555
691
  if (AUDIT) return audit();
692
+ if (LEARN) return learn();
556
693
 
557
694
  console.log();
558
695
  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.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "One command to make Claude Code safe for autonomous operation. 8 built-in hooks + 25 installable examples. Destructive blocker, branch guard, database wipe protection, dotfile guard, and more.",
5
5
  "main": "index.mjs",
6
6
  "bin": {