cc-safe-setup 2.8.0 → 2.10.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 +1 -0
- package/index.mjs +139 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -217,6 +217,7 @@ Or browse all available examples in [`examples/`](examples/):
|
|
|
217
217
|
- [Official Hooks Reference](https://docs.anthropic.com/en/docs/claude-code/hooks) — Claude Code hooks documentation
|
|
218
218
|
- [Hooks Cookbook](https://github.com/yurukusa/claude-code-hooks/blob/main/COOKBOOK.md) — 19 ready-to-use recipes from real GitHub Issues
|
|
219
219
|
- [Japanese guide (Qiita)](https://qiita.com/yurukusa/items/a9714b33f5d974e8f1e8) — この記事の日本語解説
|
|
220
|
+
- [Hook Test Runner](https://github.com/yurukusa/cc-hook-test) — `npx cc-hook-test <hook.sh>` to auto-test any hook
|
|
220
221
|
- [The incident that inspired this tool](https://github.com/anthropics/claude-code/issues/36339) — NTFS junction rm -rf
|
|
221
222
|
|
|
222
223
|
## FAQ
|
package/index.mjs
CHANGED
|
@@ -77,6 +77,8 @@ const WATCH = process.argv.includes('--watch');
|
|
|
77
77
|
const EXPORT = process.argv.includes('--export');
|
|
78
78
|
const IMPORT_IDX = process.argv.findIndex(a => a === '--import');
|
|
79
79
|
const IMPORT_FILE = IMPORT_IDX !== -1 ? process.argv[IMPORT_IDX + 1] : null;
|
|
80
|
+
const STATS = process.argv.includes('--stats');
|
|
81
|
+
const JSON_OUTPUT = process.argv.includes('--json');
|
|
80
82
|
|
|
81
83
|
if (HELP) {
|
|
82
84
|
console.log(`
|
|
@@ -93,10 +95,12 @@ if (HELP) {
|
|
|
93
95
|
npx cc-safe-setup --full Complete setup: hooks + scan + audit + badge
|
|
94
96
|
npx cc-safe-setup --audit Safety score (0-100) with fixes
|
|
95
97
|
npx cc-safe-setup --audit --fix Auto-fix missing protections
|
|
98
|
+
npx cc-safe-setup --audit --json Machine-readable output for CI/CD
|
|
96
99
|
npx cc-safe-setup --scan Detect tech stack, recommend hooks
|
|
97
100
|
npx cc-safe-setup --learn Learn from your block history
|
|
98
101
|
npx cc-safe-setup --doctor Diagnose why hooks aren't working
|
|
99
102
|
npx cc-safe-setup --watch Live dashboard of blocked commands
|
|
103
|
+
npx cc-safe-setup --stats Block statistics and patterns report
|
|
100
104
|
npx cc-safe-setup --export Export hooks config for team sharing
|
|
101
105
|
npx cc-safe-setup --import <file> Import hooks from exported config
|
|
102
106
|
npx cc-safe-setup --help Show this help
|
|
@@ -594,7 +598,20 @@ async function audit() {
|
|
|
594
598
|
console.log(c.dim + ' Paste this into your README.md' + c.reset);
|
|
595
599
|
}
|
|
596
600
|
|
|
601
|
+
// JSON output (for CI/CD integration)
|
|
602
|
+
if (JSON_OUTPUT) {
|
|
603
|
+
const output = {
|
|
604
|
+
score,
|
|
605
|
+
grade: score >= 80 ? 'A' : score >= 60 ? 'B' : score >= 40 ? 'C' : 'F',
|
|
606
|
+
risks: risks.map(r => ({ severity: r.severity, issue: r.issue, fix: r.fix })),
|
|
607
|
+
passing: good,
|
|
608
|
+
timestamp: new Date().toISOString(),
|
|
609
|
+
};
|
|
610
|
+
console.log(JSON.stringify(output, null, 2));
|
|
611
|
+
}
|
|
612
|
+
|
|
597
613
|
console.log();
|
|
614
|
+
process.exit(score < (parseInt(process.env.CC_AUDIT_THRESHOLD) || 0) ? 1 : 0);
|
|
598
615
|
}
|
|
599
616
|
|
|
600
617
|
function learn() {
|
|
@@ -747,6 +764,127 @@ async function fullSetup() {
|
|
|
747
764
|
console.log();
|
|
748
765
|
}
|
|
749
766
|
|
|
767
|
+
async function stats() {
|
|
768
|
+
const LOG_PATH = join(HOME, '.claude', 'blocked-commands.log');
|
|
769
|
+
|
|
770
|
+
console.log();
|
|
771
|
+
console.log(c.bold + ' cc-safe-setup --stats' + c.reset);
|
|
772
|
+
console.log(c.dim + ' Block statistics from your hook history' + c.reset);
|
|
773
|
+
console.log();
|
|
774
|
+
|
|
775
|
+
if (!existsSync(LOG_PATH)) {
|
|
776
|
+
console.log(c.dim + ' No blocked-commands.log found. Hooks haven\'t blocked anything yet.' + c.reset);
|
|
777
|
+
console.log(c.dim + ' This is normal if you just installed hooks.' + c.reset);
|
|
778
|
+
console.log();
|
|
779
|
+
process.exit(0);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const lines = readFileSync(LOG_PATH, 'utf-8').split('\n').filter(l => l.trim());
|
|
783
|
+
if (lines.length === 0) {
|
|
784
|
+
console.log(c.dim + ' Log is empty. No blocks recorded yet.' + c.reset);
|
|
785
|
+
console.log();
|
|
786
|
+
process.exit(0);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Parse log entries: [timestamp] BLOCKED: reason | cmd: command
|
|
790
|
+
const entries = [];
|
|
791
|
+
const reasonCounts = {};
|
|
792
|
+
const hourCounts = {};
|
|
793
|
+
const dayCounts = {};
|
|
794
|
+
const commandPatterns = {};
|
|
795
|
+
|
|
796
|
+
for (const line of lines) {
|
|
797
|
+
const match = line.match(/^\[([^\]]+)\]\s*BLOCKED:\s*(.+?)\s*\|\s*cmd:\s*(.+)$/);
|
|
798
|
+
if (!match) continue;
|
|
799
|
+
|
|
800
|
+
const [, ts, reason, cmd] = match;
|
|
801
|
+
const date = new Date(ts);
|
|
802
|
+
const day = ts.split('T')[0];
|
|
803
|
+
const hour = date.getHours();
|
|
804
|
+
|
|
805
|
+
entries.push({ ts, reason: reason.trim(), cmd: cmd.trim(), date, day, hour });
|
|
806
|
+
|
|
807
|
+
// Count reasons
|
|
808
|
+
const r = reason.trim();
|
|
809
|
+
reasonCounts[r] = (reasonCounts[r] || 0) + 1;
|
|
810
|
+
|
|
811
|
+
// Count by hour
|
|
812
|
+
hourCounts[hour] = (hourCounts[hour] || 0) + 1;
|
|
813
|
+
|
|
814
|
+
// Count by day
|
|
815
|
+
dayCounts[day] = (dayCounts[day] || 0) + 1;
|
|
816
|
+
|
|
817
|
+
// Categorize commands
|
|
818
|
+
const cmdLower = cmd.toLowerCase();
|
|
819
|
+
let pattern = 'other';
|
|
820
|
+
if (cmdLower.includes('rm ')) pattern = 'rm (delete)';
|
|
821
|
+
else if (cmdLower.includes('git push')) pattern = 'git push';
|
|
822
|
+
else if (cmdLower.includes('git reset')) pattern = 'git reset';
|
|
823
|
+
else if (cmdLower.includes('git clean')) pattern = 'git clean';
|
|
824
|
+
else if (cmdLower.includes('git add')) pattern = 'git add (secrets)';
|
|
825
|
+
else if (cmdLower.includes('remove-item')) pattern = 'PowerShell delete';
|
|
826
|
+
else if (cmdLower.includes('git checkout') || cmdLower.includes('git switch')) pattern = 'git checkout --force';
|
|
827
|
+
commandPatterns[pattern] = (commandPatterns[pattern] || 0) + 1;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (entries.length === 0) {
|
|
831
|
+
console.log(c.dim + ' No parseable entries in log.' + c.reset);
|
|
832
|
+
console.log();
|
|
833
|
+
process.exit(0);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Summary
|
|
837
|
+
const firstDate = entries[0].day;
|
|
838
|
+
const lastDate = entries[entries.length - 1].day;
|
|
839
|
+
const daySpan = Object.keys(dayCounts).length;
|
|
840
|
+
|
|
841
|
+
console.log(c.bold + ' Summary' + c.reset);
|
|
842
|
+
console.log(' Total blocks: ' + c.bold + entries.length + c.reset);
|
|
843
|
+
console.log(' Period: ' + firstDate + ' to ' + lastDate + ' (' + daySpan + ' days)');
|
|
844
|
+
console.log(' Average: ' + (entries.length / Math.max(daySpan, 1)).toFixed(1) + ' blocks/day');
|
|
845
|
+
console.log();
|
|
846
|
+
|
|
847
|
+
// Top reasons
|
|
848
|
+
console.log(c.bold + ' Top Block Reasons' + c.reset);
|
|
849
|
+
const sortedReasons = Object.entries(reasonCounts).sort((a, b) => b[1] - a[1]);
|
|
850
|
+
const maxReasonCount = sortedReasons[0]?.[1] || 1;
|
|
851
|
+
for (const [reason, count] of sortedReasons.slice(0, 8)) {
|
|
852
|
+
const bar = '█'.repeat(Math.ceil(count / maxReasonCount * 20));
|
|
853
|
+
const pct = ((count / entries.length) * 100).toFixed(0);
|
|
854
|
+
console.log(' ' + c.red + bar + c.reset + ' ' + count + ' (' + pct + '%) ' + reason);
|
|
855
|
+
}
|
|
856
|
+
console.log();
|
|
857
|
+
|
|
858
|
+
// Command categories
|
|
859
|
+
console.log(c.bold + ' Command Categories' + c.reset);
|
|
860
|
+
const sortedPatterns = Object.entries(commandPatterns).sort((a, b) => b[1] - a[1]);
|
|
861
|
+
for (const [pattern, count] of sortedPatterns) {
|
|
862
|
+
const pct = ((count / entries.length) * 100).toFixed(0);
|
|
863
|
+
console.log(' ' + c.yellow + count.toString().padStart(4) + c.reset + ' ' + pattern + ' (' + pct + '%)');
|
|
864
|
+
}
|
|
865
|
+
console.log();
|
|
866
|
+
|
|
867
|
+
// Activity by hour
|
|
868
|
+
console.log(c.bold + ' Blocks by Hour' + c.reset);
|
|
869
|
+
const maxHour = Math.max(...Object.values(hourCounts), 1);
|
|
870
|
+
for (let h = 0; h < 24; h++) {
|
|
871
|
+
const count = hourCounts[h] || 0;
|
|
872
|
+
if (count === 0) continue;
|
|
873
|
+
const bar = '▓'.repeat(Math.ceil(count / maxHour * 15));
|
|
874
|
+
console.log(' ' + h.toString().padStart(2) + ':00 ' + c.blue + bar + c.reset + ' ' + count);
|
|
875
|
+
}
|
|
876
|
+
console.log();
|
|
877
|
+
|
|
878
|
+
// Recent blocks (last 5)
|
|
879
|
+
console.log(c.bold + ' Recent Blocks' + c.reset);
|
|
880
|
+
for (const entry of entries.slice(-5)) {
|
|
881
|
+
const time = entry.ts.replace(/T/, ' ').replace(/\+.*/, '');
|
|
882
|
+
console.log(' ' + c.dim + time + c.reset + ' ' + entry.reason);
|
|
883
|
+
console.log(' ' + c.dim + entry.cmd.slice(0, 100) + c.reset);
|
|
884
|
+
}
|
|
885
|
+
console.log();
|
|
886
|
+
}
|
|
887
|
+
|
|
750
888
|
async function exportConfig() {
|
|
751
889
|
console.log();
|
|
752
890
|
console.log(c.bold + ' cc-safe-setup --export' + c.reset);
|
|
@@ -1304,6 +1442,7 @@ async function main() {
|
|
|
1304
1442
|
if (FULL) return fullSetup();
|
|
1305
1443
|
if (DOCTOR) return doctor();
|
|
1306
1444
|
if (WATCH) return watch();
|
|
1445
|
+
if (STATS) return stats();
|
|
1307
1446
|
if (EXPORT) return exportConfig();
|
|
1308
1447
|
if (IMPORT_FILE) return importConfig(IMPORT_FILE);
|
|
1309
1448
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "One command to make Claude Code safe for autonomous operation. 8 built-in hooks + 26 installable examples. Destructive blocker, branch guard, database wipe protection, case-insensitive FS guard, and more.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"bin": {
|