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 +12 -0
- package/audit-web/index.html +147 -0
- package/index.mjs +138 -1
- package/package.json +1 -1
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.
|
|
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": {
|