cc-safe-setup 2.1.1 โ†’ 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.
@@ -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(`
@@ -569,6 +570,118 @@ async function audit() {
569
570
  console.log();
570
571
  }
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);
682
+ console.log();
683
+ }
684
+
572
685
  async function main() {
573
686
  if (UNINSTALL) return uninstall();
574
687
  if (VERIFY) return verify();
@@ -576,6 +689,7 @@ async function main() {
576
689
  if (EXAMPLES) return examples();
577
690
  if (INSTALL_EXAMPLE) return installExample(INSTALL_EXAMPLE);
578
691
  if (AUDIT) return audit();
692
+ if (LEARN) return learn();
579
693
 
580
694
  console.log();
581
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.1",
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": {