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.
- package/index.mjs +285 -0
- 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.
|
|
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": {
|