cc-safe-setup 2.8.0 → 2.9.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 +124 -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,7 @@ 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');
80
81
 
81
82
  if (HELP) {
82
83
  console.log(`
@@ -97,6 +98,7 @@ if (HELP) {
97
98
  npx cc-safe-setup --learn Learn from your block history
98
99
  npx cc-safe-setup --doctor Diagnose why hooks aren't working
99
100
  npx cc-safe-setup --watch Live dashboard of blocked commands
101
+ npx cc-safe-setup --stats Block statistics and patterns report
100
102
  npx cc-safe-setup --export Export hooks config for team sharing
101
103
  npx cc-safe-setup --import <file> Import hooks from exported config
102
104
  npx cc-safe-setup --help Show this help
@@ -747,6 +749,127 @@ async function fullSetup() {
747
749
  console.log();
748
750
  }
749
751
 
752
+ async function stats() {
753
+ const LOG_PATH = join(HOME, '.claude', 'blocked-commands.log');
754
+
755
+ console.log();
756
+ console.log(c.bold + ' cc-safe-setup --stats' + c.reset);
757
+ console.log(c.dim + ' Block statistics from your hook history' + c.reset);
758
+ console.log();
759
+
760
+ if (!existsSync(LOG_PATH)) {
761
+ console.log(c.dim + ' No blocked-commands.log found. Hooks haven\'t blocked anything yet.' + c.reset);
762
+ console.log(c.dim + ' This is normal if you just installed hooks.' + c.reset);
763
+ console.log();
764
+ process.exit(0);
765
+ }
766
+
767
+ const lines = readFileSync(LOG_PATH, 'utf-8').split('\n').filter(l => l.trim());
768
+ if (lines.length === 0) {
769
+ console.log(c.dim + ' Log is empty. No blocks recorded yet.' + c.reset);
770
+ console.log();
771
+ process.exit(0);
772
+ }
773
+
774
+ // Parse log entries: [timestamp] BLOCKED: reason | cmd: command
775
+ const entries = [];
776
+ const reasonCounts = {};
777
+ const hourCounts = {};
778
+ const dayCounts = {};
779
+ const commandPatterns = {};
780
+
781
+ for (const line of lines) {
782
+ const match = line.match(/^\[([^\]]+)\]\s*BLOCKED:\s*(.+?)\s*\|\s*cmd:\s*(.+)$/);
783
+ if (!match) continue;
784
+
785
+ const [, ts, reason, cmd] = match;
786
+ const date = new Date(ts);
787
+ const day = ts.split('T')[0];
788
+ const hour = date.getHours();
789
+
790
+ entries.push({ ts, reason: reason.trim(), cmd: cmd.trim(), date, day, hour });
791
+
792
+ // Count reasons
793
+ const r = reason.trim();
794
+ reasonCounts[r] = (reasonCounts[r] || 0) + 1;
795
+
796
+ // Count by hour
797
+ hourCounts[hour] = (hourCounts[hour] || 0) + 1;
798
+
799
+ // Count by day
800
+ dayCounts[day] = (dayCounts[day] || 0) + 1;
801
+
802
+ // Categorize commands
803
+ const cmdLower = cmd.toLowerCase();
804
+ let pattern = 'other';
805
+ if (cmdLower.includes('rm ')) pattern = 'rm (delete)';
806
+ else if (cmdLower.includes('git push')) pattern = 'git push';
807
+ else if (cmdLower.includes('git reset')) pattern = 'git reset';
808
+ else if (cmdLower.includes('git clean')) pattern = 'git clean';
809
+ else if (cmdLower.includes('git add')) pattern = 'git add (secrets)';
810
+ else if (cmdLower.includes('remove-item')) pattern = 'PowerShell delete';
811
+ else if (cmdLower.includes('git checkout') || cmdLower.includes('git switch')) pattern = 'git checkout --force';
812
+ commandPatterns[pattern] = (commandPatterns[pattern] || 0) + 1;
813
+ }
814
+
815
+ if (entries.length === 0) {
816
+ console.log(c.dim + ' No parseable entries in log.' + c.reset);
817
+ console.log();
818
+ process.exit(0);
819
+ }
820
+
821
+ // Summary
822
+ const firstDate = entries[0].day;
823
+ const lastDate = entries[entries.length - 1].day;
824
+ const daySpan = Object.keys(dayCounts).length;
825
+
826
+ console.log(c.bold + ' Summary' + c.reset);
827
+ console.log(' Total blocks: ' + c.bold + entries.length + c.reset);
828
+ console.log(' Period: ' + firstDate + ' to ' + lastDate + ' (' + daySpan + ' days)');
829
+ console.log(' Average: ' + (entries.length / Math.max(daySpan, 1)).toFixed(1) + ' blocks/day');
830
+ console.log();
831
+
832
+ // Top reasons
833
+ console.log(c.bold + ' Top Block Reasons' + c.reset);
834
+ const sortedReasons = Object.entries(reasonCounts).sort((a, b) => b[1] - a[1]);
835
+ const maxReasonCount = sortedReasons[0]?.[1] || 1;
836
+ for (const [reason, count] of sortedReasons.slice(0, 8)) {
837
+ const bar = '█'.repeat(Math.ceil(count / maxReasonCount * 20));
838
+ const pct = ((count / entries.length) * 100).toFixed(0);
839
+ console.log(' ' + c.red + bar + c.reset + ' ' + count + ' (' + pct + '%) ' + reason);
840
+ }
841
+ console.log();
842
+
843
+ // Command categories
844
+ console.log(c.bold + ' Command Categories' + c.reset);
845
+ const sortedPatterns = Object.entries(commandPatterns).sort((a, b) => b[1] - a[1]);
846
+ for (const [pattern, count] of sortedPatterns) {
847
+ const pct = ((count / entries.length) * 100).toFixed(0);
848
+ console.log(' ' + c.yellow + count.toString().padStart(4) + c.reset + ' ' + pattern + ' (' + pct + '%)');
849
+ }
850
+ console.log();
851
+
852
+ // Activity by hour
853
+ console.log(c.bold + ' Blocks by Hour' + c.reset);
854
+ const maxHour = Math.max(...Object.values(hourCounts), 1);
855
+ for (let h = 0; h < 24; h++) {
856
+ const count = hourCounts[h] || 0;
857
+ if (count === 0) continue;
858
+ const bar = '▓'.repeat(Math.ceil(count / maxHour * 15));
859
+ console.log(' ' + h.toString().padStart(2) + ':00 ' + c.blue + bar + c.reset + ' ' + count);
860
+ }
861
+ console.log();
862
+
863
+ // Recent blocks (last 5)
864
+ console.log(c.bold + ' Recent Blocks' + c.reset);
865
+ for (const entry of entries.slice(-5)) {
866
+ const time = entry.ts.replace(/T/, ' ').replace(/\+.*/, '');
867
+ console.log(' ' + c.dim + time + c.reset + ' ' + entry.reason);
868
+ console.log(' ' + c.dim + entry.cmd.slice(0, 100) + c.reset);
869
+ }
870
+ console.log();
871
+ }
872
+
750
873
  async function exportConfig() {
751
874
  console.log();
752
875
  console.log(c.bold + ' cc-safe-setup --export' + c.reset);
@@ -1304,6 +1427,7 @@ async function main() {
1304
1427
  if (FULL) return fullSetup();
1305
1428
  if (DOCTOR) return doctor();
1306
1429
  if (WATCH) return watch();
1430
+ if (STATS) return stats();
1307
1431
  if (EXPORT) return exportConfig();
1308
1432
  if (IMPORT_FILE) return importConfig(IMPORT_FILE);
1309
1433
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-safe-setup",
3
- "version": "2.8.0",
3
+ "version": "2.9.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": {