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