cc-safe-setup 2.1.1 → 2.3.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 +256 -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,8 @@ 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');
|
|
73
|
+
const SCAN = process.argv.includes('--scan');
|
|
72
74
|
|
|
73
75
|
if (HELP) {
|
|
74
76
|
console.log(`
|
|
@@ -569,6 +571,258 @@ async function audit() {
|
|
|
569
571
|
console.log();
|
|
570
572
|
}
|
|
571
573
|
|
|
574
|
+
function learn() {
|
|
575
|
+
console.log();
|
|
576
|
+
console.log(c.bold + ' cc-safe-setup --learn' + c.reset);
|
|
577
|
+
console.log(c.dim + ' Analyzing your blocked command history to generate custom protections...' + c.reset);
|
|
578
|
+
console.log();
|
|
579
|
+
|
|
580
|
+
const logPath = join(HOME, '.claude', 'blocked-commands.log');
|
|
581
|
+
if (!existsSync(logPath)) {
|
|
582
|
+
console.log(c.yellow + ' No blocked-commands.log found.' + c.reset);
|
|
583
|
+
console.log(c.dim + ' Install cc-safe-setup first, then use Claude Code normally.' + c.reset);
|
|
584
|
+
console.log(c.dim + ' Blocked commands are logged automatically. Re-run --learn after a few sessions.' + c.reset);
|
|
585
|
+
console.log();
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const log = readFileSync(logPath, 'utf-8');
|
|
590
|
+
const lines = log.split('\n').filter(l => l.trim());
|
|
591
|
+
|
|
592
|
+
if (lines.length === 0) {
|
|
593
|
+
console.log(c.green + ' No blocked commands in history. Your setup is catching nothing (or everything is safe).' + c.reset);
|
|
594
|
+
console.log();
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Extract command patterns from blocked log
|
|
599
|
+
const patterns = {};
|
|
600
|
+
for (const line of lines) {
|
|
601
|
+
// Extract the command from log lines like "[2026-03-23 12:00:00] BLOCKED: rm -rf / (destructive-guard)"
|
|
602
|
+
const cmdMatch = line.match(/BLOCKED:\s*(.+?)(?:\s*\(|$)/);
|
|
603
|
+
if (cmdMatch) {
|
|
604
|
+
const cmd = cmdMatch[1].trim();
|
|
605
|
+
// Extract the base command (first word)
|
|
606
|
+
const base = cmd.split(/\s+/)[0];
|
|
607
|
+
if (!patterns[base]) patterns[base] = [];
|
|
608
|
+
patterns[base].push(cmd);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const uniqueBases = Object.keys(patterns);
|
|
613
|
+
if (uniqueBases.length === 0) {
|
|
614
|
+
console.log(c.dim + ' Could not parse patterns from log. Format may differ.' + c.reset);
|
|
615
|
+
console.log();
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
console.log(c.bold + ' Patterns found (' + lines.length + ' blocked commands):' + c.reset);
|
|
620
|
+
console.log();
|
|
621
|
+
|
|
622
|
+
const recommendations = [];
|
|
623
|
+
|
|
624
|
+
for (const [base, cmds] of Object.entries(patterns)) {
|
|
625
|
+
const count = cmds.length;
|
|
626
|
+
const unique = [...new Set(cmds)];
|
|
627
|
+
|
|
628
|
+
if (count >= 3) {
|
|
629
|
+
console.log(' ' + c.red + '⚠' + c.reset + ' ' + c.bold + base + c.reset + ' blocked ' + count + ' times');
|
|
630
|
+
for (const u of unique.slice(0, 3)) {
|
|
631
|
+
console.log(' ' + c.dim + u + c.reset);
|
|
632
|
+
}
|
|
633
|
+
if (unique.length > 3) console.log(' ' + c.dim + '... and ' + (unique.length - 3) + ' more' + c.reset);
|
|
634
|
+
|
|
635
|
+
recommendations.push({
|
|
636
|
+
command: base,
|
|
637
|
+
count,
|
|
638
|
+
examples: unique.slice(0, 5),
|
|
639
|
+
});
|
|
640
|
+
} else {
|
|
641
|
+
console.log(' ' + c.yellow + '·' + c.reset + ' ' + base + ' (' + count + 'x)');
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (recommendations.length > 0) {
|
|
646
|
+
console.log();
|
|
647
|
+
console.log(c.bold + ' Recommendations:' + c.reset);
|
|
648
|
+
console.log();
|
|
649
|
+
for (const r of recommendations) {
|
|
650
|
+
console.log(' ' + c.green + '→' + c.reset + ' ' + r.command + ' is frequently blocked (' + r.count + 'x).');
|
|
651
|
+
console.log(' Consider adding a specific hook or adjusting your allow rules.');
|
|
652
|
+
|
|
653
|
+
// Generate a custom hook suggestion
|
|
654
|
+
const hookCode = `#!/bin/bash
|
|
655
|
+
# Auto-generated: block ${r.command} patterns (seen ${r.count} times)
|
|
656
|
+
CMD=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
657
|
+
[[ -z "$CMD" ]] && exit 0
|
|
658
|
+
if echo "$CMD" | grep -qE '^\\s*${r.command}\\b'; then
|
|
659
|
+
echo "BLOCKED: ${r.command} requires manual approval" >&2
|
|
660
|
+
exit 2
|
|
661
|
+
fi
|
|
662
|
+
exit 0`;
|
|
663
|
+
|
|
664
|
+
const hookPath = join(HOOKS_DIR, 'learned-block-' + r.command + '.sh');
|
|
665
|
+
console.log(' ' + c.dim + 'Suggested hook: ' + hookPath + c.reset);
|
|
666
|
+
|
|
667
|
+
if (process.argv.includes('--apply')) {
|
|
668
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
669
|
+
writeFileSync(hookPath, hookCode);
|
|
670
|
+
chmodSync(hookPath, 0o755);
|
|
671
|
+
console.log(' ' + c.green + '✓ Hook created' + c.reset);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (!process.argv.includes('--apply')) {
|
|
676
|
+
console.log();
|
|
677
|
+
console.log(c.dim + ' Run with --apply to auto-create hooks: npx cc-safe-setup --learn --apply' + c.reset);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
console.log();
|
|
682
|
+
console.log(c.bold + ' Summary: ' + lines.length + ' blocked commands, ' + uniqueBases.length + ' unique patterns, ' + recommendations.length + ' recommendations.' + c.reset);
|
|
683
|
+
console.log();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function scan() {
|
|
687
|
+
console.log();
|
|
688
|
+
console.log(c.bold + ' cc-safe-setup --scan' + c.reset);
|
|
689
|
+
console.log(c.dim + ' Scanning project to generate safety config...' + c.reset);
|
|
690
|
+
console.log();
|
|
691
|
+
|
|
692
|
+
const cwd = process.cwd();
|
|
693
|
+
const detected = { languages: [], frameworks: [], hasDb: false, hasDocker: false, isMonorepo: false };
|
|
694
|
+
const hooks = ['destructive-guard', 'branch-guard', 'secret-guard', 'syntax-check'];
|
|
695
|
+
const claudeMdRules = ['# Project Safety Rules\n', '## Generated by cc-safe-setup --scan\n'];
|
|
696
|
+
|
|
697
|
+
// Detect languages & frameworks
|
|
698
|
+
if (existsSync(join(cwd, 'package.json'))) {
|
|
699
|
+
detected.languages.push('JavaScript/TypeScript');
|
|
700
|
+
try {
|
|
701
|
+
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8'));
|
|
702
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
703
|
+
if (allDeps.next) detected.frameworks.push('Next.js');
|
|
704
|
+
if (allDeps.react) detected.frameworks.push('React');
|
|
705
|
+
if (allDeps.express) detected.frameworks.push('Express');
|
|
706
|
+
if (allDeps.prisma || allDeps['@prisma/client']) { detected.frameworks.push('Prisma'); detected.hasDb = true; }
|
|
707
|
+
if (allDeps.sequelize) { detected.frameworks.push('Sequelize'); detected.hasDb = true; }
|
|
708
|
+
if (allDeps.typeorm) { detected.frameworks.push('TypeORM'); detected.hasDb = true; }
|
|
709
|
+
} catch(e) {}
|
|
710
|
+
}
|
|
711
|
+
if (existsSync(join(cwd, 'requirements.txt')) || existsSync(join(cwd, 'pyproject.toml'))) {
|
|
712
|
+
detected.languages.push('Python');
|
|
713
|
+
if (existsSync(join(cwd, 'manage.py'))) { detected.frameworks.push('Django'); detected.hasDb = true; }
|
|
714
|
+
if (existsSync(join(cwd, 'app.py')) || existsSync(join(cwd, 'wsgi.py'))) detected.frameworks.push('Flask');
|
|
715
|
+
}
|
|
716
|
+
if (existsSync(join(cwd, 'Gemfile'))) {
|
|
717
|
+
detected.languages.push('Ruby');
|
|
718
|
+
detected.frameworks.push('Rails');
|
|
719
|
+
detected.hasDb = true;
|
|
720
|
+
}
|
|
721
|
+
if (existsSync(join(cwd, 'composer.json'))) {
|
|
722
|
+
detected.languages.push('PHP');
|
|
723
|
+
try {
|
|
724
|
+
const composer = JSON.parse(readFileSync(join(cwd, 'composer.json'), 'utf-8'));
|
|
725
|
+
if (composer.require?.['laravel/framework']) { detected.frameworks.push('Laravel'); detected.hasDb = true; }
|
|
726
|
+
if (composer.require?.['symfony/framework-bundle']) { detected.frameworks.push('Symfony'); detected.hasDb = true; }
|
|
727
|
+
} catch(e) {}
|
|
728
|
+
}
|
|
729
|
+
if (existsSync(join(cwd, 'go.mod'))) detected.languages.push('Go');
|
|
730
|
+
if (existsSync(join(cwd, 'Cargo.toml'))) detected.languages.push('Rust');
|
|
731
|
+
|
|
732
|
+
// Docker
|
|
733
|
+
if (existsSync(join(cwd, 'Dockerfile')) || existsSync(join(cwd, 'docker-compose.yml')) || existsSync(join(cwd, 'compose.yaml'))) {
|
|
734
|
+
detected.hasDocker = true;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Monorepo
|
|
738
|
+
if (existsSync(join(cwd, 'pnpm-workspace.yaml')) || existsSync(join(cwd, 'lerna.json')) || existsSync(join(cwd, 'nx.json'))) {
|
|
739
|
+
detected.isMonorepo = true;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// .env files
|
|
743
|
+
const hasEnv = existsSync(join(cwd, '.env')) || existsSync(join(cwd, '.env.local'));
|
|
744
|
+
|
|
745
|
+
// Display detection results
|
|
746
|
+
console.log(c.bold + ' Detected:' + c.reset);
|
|
747
|
+
if (detected.languages.length) console.log(' ' + c.green + '✓' + c.reset + ' Languages: ' + detected.languages.join(', '));
|
|
748
|
+
if (detected.frameworks.length) console.log(' ' + c.green + '✓' + c.reset + ' Frameworks: ' + detected.frameworks.join(', '));
|
|
749
|
+
if (detected.hasDb) console.log(' ' + c.green + '✓' + c.reset + ' Database detected');
|
|
750
|
+
if (detected.hasDocker) console.log(' ' + c.green + '✓' + c.reset + ' Docker detected');
|
|
751
|
+
if (detected.isMonorepo) console.log(' ' + c.green + '✓' + c.reset + ' Monorepo detected');
|
|
752
|
+
if (hasEnv) console.log(' ' + c.yellow + '⚠' + c.reset + ' .env file found (secret leak risk)');
|
|
753
|
+
console.log();
|
|
754
|
+
|
|
755
|
+
// Generate recommendations
|
|
756
|
+
const examples = [];
|
|
757
|
+
|
|
758
|
+
if (detected.hasDb) {
|
|
759
|
+
examples.push('block-database-wipe');
|
|
760
|
+
claudeMdRules.push('- Never run destructive database commands (migrate:fresh, DROP DATABASE, prisma migrate reset)');
|
|
761
|
+
claudeMdRules.push('- Always backup database before schema changes');
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (detected.hasDocker) {
|
|
765
|
+
examples.push('auto-approve-docker');
|
|
766
|
+
claudeMdRules.push('- Docker commands are auto-approved for build/compose/ps/logs');
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (hasEnv) {
|
|
770
|
+
claudeMdRules.push('- Never commit .env files. Use .env.example for templates');
|
|
771
|
+
claudeMdRules.push('- Never hardcode API keys or secrets in source files');
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (detected.languages.includes('Python')) {
|
|
775
|
+
examples.push('auto-approve-python');
|
|
776
|
+
claudeMdRules.push('- Run pytest after every code change');
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (detected.languages.includes('JavaScript/TypeScript')) {
|
|
780
|
+
examples.push('auto-approve-build');
|
|
781
|
+
claudeMdRules.push('- Run npm test after every code change');
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Always recommend
|
|
785
|
+
examples.push('scope-guard');
|
|
786
|
+
examples.push('protect-dotfiles');
|
|
787
|
+
claudeMdRules.push('- Always run tests before committing');
|
|
788
|
+
claudeMdRules.push('- Never force-push to main/master');
|
|
789
|
+
claudeMdRules.push('- Create a backup branch before large refactors');
|
|
790
|
+
|
|
791
|
+
// Display recommendations
|
|
792
|
+
console.log(c.bold + ' Recommended hooks:' + c.reset);
|
|
793
|
+
console.log(' ' + c.dim + 'npx cc-safe-setup' + c.reset + ' (8 built-in hooks)');
|
|
794
|
+
for (const ex of examples) {
|
|
795
|
+
console.log(' ' + c.dim + 'npx cc-safe-setup --install-example ' + ex + c.reset);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Generate CLAUDE.md
|
|
799
|
+
console.log();
|
|
800
|
+
const claudeMdPath = join(cwd, 'CLAUDE.md');
|
|
801
|
+
if (existsSync(claudeMdPath)) {
|
|
802
|
+
console.log(c.yellow + ' CLAUDE.md already exists. Suggested rules to add:' + c.reset);
|
|
803
|
+
console.log();
|
|
804
|
+
for (const rule of claudeMdRules.slice(2)) {
|
|
805
|
+
console.log(' ' + c.dim + rule + c.reset);
|
|
806
|
+
}
|
|
807
|
+
} else {
|
|
808
|
+
const content = claudeMdRules.join('\n') + '\n';
|
|
809
|
+
if (process.argv.includes('--apply')) {
|
|
810
|
+
writeFileSync(claudeMdPath, content);
|
|
811
|
+
console.log(c.green + ' ✓ CLAUDE.md created with ' + (claudeMdRules.length - 2) + ' project-specific rules.' + c.reset);
|
|
812
|
+
} else {
|
|
813
|
+
console.log(c.bold + ' Suggested CLAUDE.md:' + c.reset);
|
|
814
|
+
console.log();
|
|
815
|
+
for (const rule of claudeMdRules) {
|
|
816
|
+
console.log(' ' + c.dim + rule + c.reset);
|
|
817
|
+
}
|
|
818
|
+
console.log();
|
|
819
|
+
console.log(c.dim + ' Run with --apply to create: npx cc-safe-setup --scan --apply' + c.reset);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
console.log();
|
|
824
|
+
}
|
|
825
|
+
|
|
572
826
|
async function main() {
|
|
573
827
|
if (UNINSTALL) return uninstall();
|
|
574
828
|
if (VERIFY) return verify();
|
|
@@ -576,6 +830,8 @@ async function main() {
|
|
|
576
830
|
if (EXAMPLES) return examples();
|
|
577
831
|
if (INSTALL_EXAMPLE) return installExample(INSTALL_EXAMPLE);
|
|
578
832
|
if (AUDIT) return audit();
|
|
833
|
+
if (LEARN) return learn();
|
|
834
|
+
if (SCAN) return scan();
|
|
579
835
|
|
|
580
836
|
console.log();
|
|
581
837
|
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.3.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": {
|