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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/index.mjs +139 -0
  3. 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.8.0",
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": {