cc-safe-setup 2.5.0 → 2.7.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 (2) hide show
  1. package/index.mjs +285 -0
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -72,6 +72,8 @@ const AUDIT = process.argv.includes('--audit');
72
72
  const LEARN = process.argv.includes('--learn');
73
73
  const SCAN = process.argv.includes('--scan');
74
74
  const FULL = process.argv.includes('--full');
75
+ const DOCTOR = process.argv.includes('--doctor');
76
+ const WATCH = process.argv.includes('--watch');
75
77
 
76
78
  if (HELP) {
77
79
  console.log(`
@@ -90,6 +92,8 @@ if (HELP) {
90
92
  npx cc-safe-setup --audit --fix Auto-fix missing protections
91
93
  npx cc-safe-setup --scan Detect tech stack, recommend hooks
92
94
  npx cc-safe-setup --learn Learn from your block history
95
+ npx cc-safe-setup --doctor Diagnose why hooks aren't working
96
+ npx cc-safe-setup --watch Live dashboard of blocked commands
93
97
  npx cc-safe-setup --help Show this help
94
98
 
95
99
  Hooks installed:
@@ -738,6 +742,285 @@ async function fullSetup() {
738
742
  console.log();
739
743
  }
740
744
 
745
+ async function watch() {
746
+ const { spawn } = await import('child_process');
747
+ const { createReadStream, watchFile } = await import('fs');
748
+ const { createInterface: createRL } = await import('readline');
749
+
750
+ const LOG_PATH = join(HOME, '.claude', 'blocked-commands.log');
751
+ const ERROR_LOG = join(HOME, '.claude', 'session-errors.log');
752
+
753
+ console.log();
754
+ console.log(c.bold + ' cc-safe-setup --watch' + c.reset);
755
+ console.log(c.dim + ' Live safety dashboard — watching blocked commands' + c.reset);
756
+ console.log(c.dim + ' Log: ' + LOG_PATH + c.reset);
757
+ console.log();
758
+
759
+ let blockCount = 0;
760
+ let lastPrint = 0;
761
+
762
+ function formatLine(line) {
763
+ // Format: [2026-03-24T01:30:00+09:00] BLOCKED: reason | cmd: actual command
764
+ const match = line.match(/^\[([^\]]+)\]\s*BLOCKED:\s*(.+?)\s*\|\s*cmd:\s*(.+)$/);
765
+ if (!match) return c.dim + ' ' + line + c.reset;
766
+
767
+ const [, ts, reason, cmd] = match;
768
+ const time = ts.replace(/T/, ' ').replace(/\+.*/, '');
769
+ blockCount++;
770
+
771
+ let severity = c.yellow;
772
+ if (reason.match(/rm|reset|clean|Remove-Item|drop/i)) severity = c.red;
773
+ if (reason.match(/push|force/i)) severity = c.red;
774
+ if (reason.match(/env|secret|credential/i)) severity = c.red;
775
+
776
+ return severity + ' BLOCKED' + c.reset + ' ' + c.dim + time + c.reset + '\n' +
777
+ ' ' + c.bold + reason.trim() + c.reset + '\n' +
778
+ ' ' + c.dim + cmd.trim().slice(0, 120) + c.reset;
779
+ }
780
+
781
+ function printStats() {
782
+ const now = Date.now();
783
+ if (now - lastPrint < 30000) return;
784
+ lastPrint = now;
785
+ console.log(c.dim + ' --- ' + blockCount + ' blocks total | ' + new Date().toLocaleTimeString() + ' ---' + c.reset);
786
+ }
787
+
788
+ // Print existing log entries
789
+ if (existsSync(LOG_PATH)) {
790
+ const rl = createRL({ input: createReadStream(LOG_PATH) });
791
+ for await (const line of rl) {
792
+ if (line.trim()) console.log(formatLine(line));
793
+ }
794
+ if (blockCount > 0) {
795
+ console.log();
796
+ console.log(c.dim + ' === History: ' + blockCount + ' blocks ===' + c.reset);
797
+ console.log(c.dim + ' Watching for new blocks... (Ctrl+C to stop)' + c.reset);
798
+ console.log();
799
+ }
800
+ } else {
801
+ console.log(c.dim + ' No blocked-commands.log yet. Hooks will create it on first block.' + c.reset);
802
+ console.log(c.dim + ' Watching... (Ctrl+C to stop)' + c.reset);
803
+ console.log();
804
+ }
805
+
806
+ // Watch for new entries using tail -f
807
+ let tailProcess;
808
+ try {
809
+ // Ensure log file exists for tail
810
+ if (!existsSync(LOG_PATH)) {
811
+ const { mkdirSync: mkDir, writeFileSync: writeFile } = await import('fs');
812
+ mkDir(dirname(LOG_PATH), { recursive: true });
813
+ writeFile(LOG_PATH, '', 'utf-8');
814
+ }
815
+
816
+ tailProcess = spawn('tail', ['-f', '-n', '0', LOG_PATH], { stdio: ['ignore', 'pipe', 'ignore'] });
817
+
818
+ const tailRL = createRL({ input: tailProcess.stdout });
819
+ for await (const line of tailRL) {
820
+ if (line.trim()) {
821
+ console.log(formatLine(line));
822
+ printStats();
823
+ }
824
+ }
825
+ } catch (e) {
826
+ // tail not available — fall back to polling
827
+ let lastSize = 0;
828
+ try {
829
+ const { statSync } = await import('fs');
830
+ lastSize = statSync(LOG_PATH).size;
831
+ } catch {}
832
+
833
+ console.log(c.dim + ' (tail not available, using polling)' + c.reset);
834
+
835
+ setInterval(async () => {
836
+ try {
837
+ const { statSync, readFileSync: readFile } = await import('fs');
838
+ const stat = statSync(LOG_PATH);
839
+ if (stat.size > lastSize) {
840
+ const content = readFile(LOG_PATH, 'utf-8');
841
+ const lines = content.split('\n').slice(-10);
842
+ for (const line of lines) {
843
+ if (line.trim()) console.log(formatLine(line));
844
+ }
845
+ lastSize = stat.size;
846
+ printStats();
847
+ }
848
+ } catch {}
849
+ }, 2000);
850
+
851
+ // Keep process alive
852
+ await new Promise(() => {});
853
+ }
854
+ }
855
+
856
+ async function doctor() {
857
+ const { execSync, spawnSync } = await import('child_process');
858
+ const { statSync, readdirSync } = await import('fs');
859
+
860
+ console.log();
861
+ console.log(c.bold + ' cc-safe-setup --doctor' + c.reset);
862
+ console.log(c.dim + ' Diagnosing why hooks might not be working...' + c.reset);
863
+ console.log();
864
+
865
+ let issues = 0;
866
+ let warnings = 0;
867
+
868
+ const pass = (msg) => console.log(c.green + ' ✓ ' + c.reset + msg);
869
+ const fail = (msg) => { console.log(c.red + ' ✗ ' + c.reset + msg); issues++; };
870
+ const warn = (msg) => { console.log(c.yellow + ' ! ' + c.reset + msg); warnings++; };
871
+
872
+ // 1. Check jq
873
+ try {
874
+ execSync('which jq', { stdio: 'pipe' });
875
+ const ver = execSync('jq --version', { stdio: 'pipe' }).toString().trim();
876
+ pass('jq installed (' + ver + ')');
877
+ } catch {
878
+ fail('jq is not installed — hooks cannot parse JSON input');
879
+ console.log(c.dim + ' Fix: brew install jq (macOS) | apt install jq (Linux) | choco install jq (Windows)' + c.reset);
880
+ }
881
+
882
+ // 2. Check settings.json exists
883
+ if (!existsSync(SETTINGS_PATH)) {
884
+ fail('~/.claude/settings.json does not exist');
885
+ console.log(c.dim + ' Fix: npx cc-safe-setup' + c.reset);
886
+ } else {
887
+ pass('settings.json exists');
888
+
889
+ // 3. Parse settings.json
890
+ let settings;
891
+ try {
892
+ settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
893
+ pass('settings.json is valid JSON');
894
+ } catch (e) {
895
+ fail('settings.json has invalid JSON: ' + e.message);
896
+ console.log(c.dim + ' Fix: npx cc-safe-setup --uninstall && npx cc-safe-setup' + c.reset);
897
+ }
898
+
899
+ if (settings) {
900
+ // 4. Check hooks section exists
901
+ const hooks = settings.hooks;
902
+ if (!hooks) {
903
+ fail('No "hooks" section in settings.json');
904
+ } else {
905
+ pass('"hooks" section exists in settings.json');
906
+
907
+ // 5. Check each hook trigger type
908
+ for (const trigger of ['PreToolUse', 'PostToolUse', 'Stop']) {
909
+ const entries = hooks[trigger] || [];
910
+ if (entries.length > 0) {
911
+ pass(trigger + ': ' + entries.length + ' hook(s) registered');
912
+
913
+ // 6. Check each hook command path
914
+ for (const entry of entries) {
915
+ const hookList = entry.hooks || [];
916
+ for (const h of hookList) {
917
+ if (h.type !== 'command') continue;
918
+ const cmd = h.command;
919
+ // Extract the script path from commands like "bash ~/.claude/hooks/x.sh" or "~/bin/x.sh arg1 arg2"
920
+ let scriptPath = cmd;
921
+ // Strip leading interpreter (bash, sh, node, python3, etc.)
922
+ scriptPath = scriptPath.replace(/^(bash|sh|node|python3?)\s+/, '');
923
+ // Take first token (before arguments)
924
+ scriptPath = scriptPath.split(/\s+/)[0];
925
+ // Resolve ~ to HOME
926
+ const resolved = scriptPath.replace(/^~/, HOME);
927
+
928
+ if (!existsSync(resolved)) {
929
+ fail('Hook script not found: ' + scriptPath + (scriptPath !== cmd ? ' (from: ' + cmd + ')' : ''));
930
+ console.log(c.dim + ' Fix: create the missing script or update settings.json' + c.reset);
931
+ continue;
932
+ }
933
+
934
+ // 7. Check executable permission
935
+ try {
936
+ const stat = statSync(resolved);
937
+ const isExec = (stat.mode & 0o111) !== 0;
938
+ if (!isExec) {
939
+ fail('Not executable: ' + cmd);
940
+ console.log(c.dim + ' Fix: chmod +x ' + resolved + c.reset);
941
+ }
942
+ } catch {}
943
+
944
+ // 8. Check shebang
945
+ try {
946
+ const content = readFileSync(resolved, 'utf-8');
947
+ if (!content.startsWith('#!/')) {
948
+ warn('Missing shebang (#!/bin/bash) in: ' + cmd);
949
+ console.log(c.dim + ' Add #!/bin/bash as the first line' + c.reset);
950
+ }
951
+ } catch {}
952
+
953
+ // 9. Test hook with empty input
954
+ try {
955
+ const result = spawnSync('bash', [resolved], {
956
+ input: '{}',
957
+ timeout: 5000,
958
+ stdio: ['pipe', 'pipe', 'pipe'],
959
+ });
960
+ if (result.status !== 0 && result.status !== 2) {
961
+ warn('Hook exits with code ' + result.status + ' on empty input: ' + cmd);
962
+ const stderr = (result.stderr || '').toString().trim();
963
+ if (stderr) console.log(c.dim + ' stderr: ' + stderr.slice(0, 200) + c.reset);
964
+ }
965
+ } catch {}
966
+ }
967
+ }
968
+ }
969
+ }
970
+ }
971
+
972
+ // 10. Check for common misconfigurations
973
+ if (settings.defaultMode === 'bypassPermissions') {
974
+ warn('defaultMode is "bypassPermissions" — hooks may be skipped entirely');
975
+ console.log(c.dim + ' Consider using "dontAsk" instead (hooks still run)' + c.reset);
976
+ }
977
+
978
+ // 11. Check for dangerouslySkipPermissions in allow
979
+ const allows = settings.permissions?.allow || [];
980
+ if (allows.includes('Bash(*)')) {
981
+ warn('Bash(*) in allow list — commands auto-approved before hooks run');
982
+ }
983
+ }
984
+ }
985
+
986
+ // 12. Check hooks directory
987
+ if (!existsSync(HOOKS_DIR)) {
988
+ fail('~/.claude/hooks/ directory does not exist');
989
+ console.log(c.dim + ' Fix: npx cc-safe-setup' + c.reset);
990
+ } else {
991
+ const files = readdirSync(HOOKS_DIR).filter(f => f.endsWith('.sh'));
992
+ pass('hooks directory exists (' + files.length + ' scripts)');
993
+ }
994
+
995
+ // 13. Check Claude Code version (needs hooks support)
996
+ try {
997
+ const ver = execSync('claude --version 2>/dev/null || echo "not found"', { stdio: 'pipe' }).toString().trim();
998
+ if (ver === 'not found') {
999
+ warn('Claude Code CLI not found in PATH');
1000
+ } else {
1001
+ pass('Claude Code: ' + ver);
1002
+ }
1003
+ } catch {
1004
+ warn('Could not check Claude Code version');
1005
+ }
1006
+
1007
+ // Summary
1008
+ console.log();
1009
+ if (issues === 0 && warnings === 0) {
1010
+ console.log(c.bold + c.green + ' All checks passed. Hooks should be working.' + c.reset);
1011
+ console.log(c.dim + ' If hooks still don\'t fire, restart Claude Code (hooks load on startup).' + c.reset);
1012
+ } else if (issues === 0) {
1013
+ console.log(c.bold + c.yellow + ' ' + warnings + ' warning(s), but no blocking issues.' + c.reset);
1014
+ console.log(c.dim + ' Hooks should work. Restart Claude Code if they don\'t fire.' + c.reset);
1015
+ } else {
1016
+ console.log(c.bold + c.red + ' ' + issues + ' issue(s) found that prevent hooks from working.' + c.reset);
1017
+ console.log(c.dim + ' Fix the issues above, then restart Claude Code.' + c.reset);
1018
+ }
1019
+ console.log();
1020
+
1021
+ process.exit(issues > 0 ? 1 : 0);
1022
+ }
1023
+
741
1024
  function scan() {
742
1025
  console.log();
743
1026
  console.log(c.bold + ' cc-safe-setup --scan' + c.reset);
@@ -888,6 +1171,8 @@ async function main() {
888
1171
  if (LEARN) return learn();
889
1172
  if (SCAN) return scan();
890
1173
  if (FULL) return fullSetup();
1174
+ if (DOCTOR) return doctor();
1175
+ if (WATCH) return watch();
891
1176
 
892
1177
  console.log();
893
1178
  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.5.0",
3
+ "version": "2.7.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": {