cc-safe-setup 2.5.0 → 2.6.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 +171 -0
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -72,6 +72,7 @@ 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');
75
76
 
76
77
  if (HELP) {
77
78
  console.log(`
@@ -90,6 +91,7 @@ if (HELP) {
90
91
  npx cc-safe-setup --audit --fix Auto-fix missing protections
91
92
  npx cc-safe-setup --scan Detect tech stack, recommend hooks
92
93
  npx cc-safe-setup --learn Learn from your block history
94
+ npx cc-safe-setup --doctor Diagnose why hooks aren't working
93
95
  npx cc-safe-setup --help Show this help
94
96
 
95
97
  Hooks installed:
@@ -738,6 +740,174 @@ async function fullSetup() {
738
740
  console.log();
739
741
  }
740
742
 
743
+ async function doctor() {
744
+ const { execSync, spawnSync } = await import('child_process');
745
+ const { statSync, readdirSync } = await import('fs');
746
+
747
+ console.log();
748
+ console.log(c.bold + ' cc-safe-setup --doctor' + c.reset);
749
+ console.log(c.dim + ' Diagnosing why hooks might not be working...' + c.reset);
750
+ console.log();
751
+
752
+ let issues = 0;
753
+ let warnings = 0;
754
+
755
+ const pass = (msg) => console.log(c.green + ' ✓ ' + c.reset + msg);
756
+ const fail = (msg) => { console.log(c.red + ' ✗ ' + c.reset + msg); issues++; };
757
+ const warn = (msg) => { console.log(c.yellow + ' ! ' + c.reset + msg); warnings++; };
758
+
759
+ // 1. Check jq
760
+ try {
761
+ execSync('which jq', { stdio: 'pipe' });
762
+ const ver = execSync('jq --version', { stdio: 'pipe' }).toString().trim();
763
+ pass('jq installed (' + ver + ')');
764
+ } catch {
765
+ fail('jq is not installed — hooks cannot parse JSON input');
766
+ console.log(c.dim + ' Fix: brew install jq (macOS) | apt install jq (Linux) | choco install jq (Windows)' + c.reset);
767
+ }
768
+
769
+ // 2. Check settings.json exists
770
+ if (!existsSync(SETTINGS_PATH)) {
771
+ fail('~/.claude/settings.json does not exist');
772
+ console.log(c.dim + ' Fix: npx cc-safe-setup' + c.reset);
773
+ } else {
774
+ pass('settings.json exists');
775
+
776
+ // 3. Parse settings.json
777
+ let settings;
778
+ try {
779
+ settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
780
+ pass('settings.json is valid JSON');
781
+ } catch (e) {
782
+ fail('settings.json has invalid JSON: ' + e.message);
783
+ console.log(c.dim + ' Fix: npx cc-safe-setup --uninstall && npx cc-safe-setup' + c.reset);
784
+ }
785
+
786
+ if (settings) {
787
+ // 4. Check hooks section exists
788
+ const hooks = settings.hooks;
789
+ if (!hooks) {
790
+ fail('No "hooks" section in settings.json');
791
+ } else {
792
+ pass('"hooks" section exists in settings.json');
793
+
794
+ // 5. Check each hook trigger type
795
+ for (const trigger of ['PreToolUse', 'PostToolUse', 'Stop']) {
796
+ const entries = hooks[trigger] || [];
797
+ if (entries.length > 0) {
798
+ pass(trigger + ': ' + entries.length + ' hook(s) registered');
799
+
800
+ // 6. Check each hook command path
801
+ for (const entry of entries) {
802
+ const hookList = entry.hooks || [];
803
+ for (const h of hookList) {
804
+ if (h.type !== 'command') continue;
805
+ const cmd = h.command;
806
+ // Extract the script path from commands like "bash ~/.claude/hooks/x.sh" or "~/bin/x.sh arg1 arg2"
807
+ let scriptPath = cmd;
808
+ // Strip leading interpreter (bash, sh, node, python3, etc.)
809
+ scriptPath = scriptPath.replace(/^(bash|sh|node|python3?)\s+/, '');
810
+ // Take first token (before arguments)
811
+ scriptPath = scriptPath.split(/\s+/)[0];
812
+ // Resolve ~ to HOME
813
+ const resolved = scriptPath.replace(/^~/, HOME);
814
+
815
+ if (!existsSync(resolved)) {
816
+ fail('Hook script not found: ' + scriptPath + (scriptPath !== cmd ? ' (from: ' + cmd + ')' : ''));
817
+ console.log(c.dim + ' Fix: create the missing script or update settings.json' + c.reset);
818
+ continue;
819
+ }
820
+
821
+ // 7. Check executable permission
822
+ try {
823
+ const stat = statSync(resolved);
824
+ const isExec = (stat.mode & 0o111) !== 0;
825
+ if (!isExec) {
826
+ fail('Not executable: ' + cmd);
827
+ console.log(c.dim + ' Fix: chmod +x ' + resolved + c.reset);
828
+ }
829
+ } catch {}
830
+
831
+ // 8. Check shebang
832
+ try {
833
+ const content = readFileSync(resolved, 'utf-8');
834
+ if (!content.startsWith('#!/')) {
835
+ warn('Missing shebang (#!/bin/bash) in: ' + cmd);
836
+ console.log(c.dim + ' Add #!/bin/bash as the first line' + c.reset);
837
+ }
838
+ } catch {}
839
+
840
+ // 9. Test hook with empty input
841
+ try {
842
+ const result = spawnSync('bash', [resolved], {
843
+ input: '{}',
844
+ timeout: 5000,
845
+ stdio: ['pipe', 'pipe', 'pipe'],
846
+ });
847
+ if (result.status !== 0 && result.status !== 2) {
848
+ warn('Hook exits with code ' + result.status + ' on empty input: ' + cmd);
849
+ const stderr = (result.stderr || '').toString().trim();
850
+ if (stderr) console.log(c.dim + ' stderr: ' + stderr.slice(0, 200) + c.reset);
851
+ }
852
+ } catch {}
853
+ }
854
+ }
855
+ }
856
+ }
857
+ }
858
+
859
+ // 10. Check for common misconfigurations
860
+ if (settings.defaultMode === 'bypassPermissions') {
861
+ warn('defaultMode is "bypassPermissions" — hooks may be skipped entirely');
862
+ console.log(c.dim + ' Consider using "dontAsk" instead (hooks still run)' + c.reset);
863
+ }
864
+
865
+ // 11. Check for dangerouslySkipPermissions in allow
866
+ const allows = settings.permissions?.allow || [];
867
+ if (allows.includes('Bash(*)')) {
868
+ warn('Bash(*) in allow list — commands auto-approved before hooks run');
869
+ }
870
+ }
871
+ }
872
+
873
+ // 12. Check hooks directory
874
+ if (!existsSync(HOOKS_DIR)) {
875
+ fail('~/.claude/hooks/ directory does not exist');
876
+ console.log(c.dim + ' Fix: npx cc-safe-setup' + c.reset);
877
+ } else {
878
+ const files = readdirSync(HOOKS_DIR).filter(f => f.endsWith('.sh'));
879
+ pass('hooks directory exists (' + files.length + ' scripts)');
880
+ }
881
+
882
+ // 13. Check Claude Code version (needs hooks support)
883
+ try {
884
+ const ver = execSync('claude --version 2>/dev/null || echo "not found"', { stdio: 'pipe' }).toString().trim();
885
+ if (ver === 'not found') {
886
+ warn('Claude Code CLI not found in PATH');
887
+ } else {
888
+ pass('Claude Code: ' + ver);
889
+ }
890
+ } catch {
891
+ warn('Could not check Claude Code version');
892
+ }
893
+
894
+ // Summary
895
+ console.log();
896
+ if (issues === 0 && warnings === 0) {
897
+ console.log(c.bold + c.green + ' All checks passed. Hooks should be working.' + c.reset);
898
+ console.log(c.dim + ' If hooks still don\'t fire, restart Claude Code (hooks load on startup).' + c.reset);
899
+ } else if (issues === 0) {
900
+ console.log(c.bold + c.yellow + ' ' + warnings + ' warning(s), but no blocking issues.' + c.reset);
901
+ console.log(c.dim + ' Hooks should work. Restart Claude Code if they don\'t fire.' + c.reset);
902
+ } else {
903
+ console.log(c.bold + c.red + ' ' + issues + ' issue(s) found that prevent hooks from working.' + c.reset);
904
+ console.log(c.dim + ' Fix the issues above, then restart Claude Code.' + c.reset);
905
+ }
906
+ console.log();
907
+
908
+ process.exit(issues > 0 ? 1 : 0);
909
+ }
910
+
741
911
  function scan() {
742
912
  console.log();
743
913
  console.log(c.bold + ' cc-safe-setup --scan' + c.reset);
@@ -888,6 +1058,7 @@ async function main() {
888
1058
  if (LEARN) return learn();
889
1059
  if (SCAN) return scan();
890
1060
  if (FULL) return fullSetup();
1061
+ if (DOCTOR) return doctor();
891
1062
 
892
1063
  console.log();
893
1064
  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.6.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": {