cc-safe-setup 8.1.0 → 8.3.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 +230 -0
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -91,6 +91,9 @@ const GENERATE_CI = process.argv.includes('--generate-ci');
|
|
|
91
91
|
const REPORT = process.argv.includes('--report');
|
|
92
92
|
const QUICKFIX = process.argv.includes('--quickfix');
|
|
93
93
|
const SHIELD = process.argv.includes('--shield');
|
|
94
|
+
const ANALYZE = process.argv.includes('--analyze');
|
|
95
|
+
const PROFILE_IDX = process.argv.findIndex(a => a === '--profile');
|
|
96
|
+
const PROFILE = PROFILE_IDX !== -1 ? process.argv[PROFILE_IDX + 1] : null;
|
|
94
97
|
const COMPARE_IDX = process.argv.findIndex(a => a === '--compare');
|
|
95
98
|
const COMPARE = COMPARE_IDX !== -1 ? { a: process.argv[COMPARE_IDX + 1], b: process.argv[COMPARE_IDX + 2] } : null;
|
|
96
99
|
const CREATE_IDX = process.argv.findIndex(a => a === '--create');
|
|
@@ -126,6 +129,8 @@ if (HELP) {
|
|
|
126
129
|
npx cc-safe-setup --doctor Diagnose why hooks aren't working
|
|
127
130
|
npx cc-safe-setup --watch Live dashboard of blocked commands
|
|
128
131
|
npx cc-safe-setup --create "<desc>" Generate a custom hook from description
|
|
132
|
+
npx cc-safe-setup --profile <level> Switch safety profile (strict/standard/minimal)
|
|
133
|
+
npx cc-safe-setup --analyze Analyze what Claude did in your last session
|
|
129
134
|
npx cc-safe-setup --shield Maximum safety in one command (fix + scan + install + CLAUDE.md)
|
|
130
135
|
npx cc-safe-setup --quickfix Auto-detect and fix common Claude Code problems
|
|
131
136
|
npx cc-safe-setup --stats Block statistics and patterns report
|
|
@@ -833,6 +838,229 @@ async function fullSetup() {
|
|
|
833
838
|
console.log();
|
|
834
839
|
}
|
|
835
840
|
|
|
841
|
+
async function profile(level) {
|
|
842
|
+
const { readdirSync } = await import('fs');
|
|
843
|
+
console.log();
|
|
844
|
+
|
|
845
|
+
const PROFILES = {
|
|
846
|
+
strict: {
|
|
847
|
+
desc: 'Maximum safety — blocks everything dangerous, requires verification',
|
|
848
|
+
hooks: ['destructive-guard', 'branch-guard', 'secret-guard', 'syntax-check',
|
|
849
|
+
'context-monitor', 'comment-strip', 'cd-git-allow', 'api-error-alert',
|
|
850
|
+
'scope-guard', 'no-sudo-guard', 'protect-claudemd', 'env-source-guard',
|
|
851
|
+
'no-install-global', 'deploy-guard', 'protect-dotfiles', 'symlink-guard',
|
|
852
|
+
'strict-allowlist', 'uncommitted-work-guard', 'test-deletion-guard',
|
|
853
|
+
'overwrite-guard', 'error-memory-guard', 'hardcoded-secret-detector',
|
|
854
|
+
'conflict-marker-guard', 'token-budget-guard', 'fact-check-gate',
|
|
855
|
+
'block-database-wipe', 'no-eval', 'file-size-limit', 'large-read-guard',
|
|
856
|
+
'loop-detector', 'verify-before-done', 'diff-size-guard', 'commit-scope-guard']
|
|
857
|
+
},
|
|
858
|
+
standard: {
|
|
859
|
+
desc: 'Balanced — blocks dangerous commands, auto-approves safe ones',
|
|
860
|
+
hooks: ['destructive-guard', 'branch-guard', 'secret-guard', 'syntax-check',
|
|
861
|
+
'context-monitor', 'comment-strip', 'cd-git-allow', 'api-error-alert',
|
|
862
|
+
'scope-guard', 'no-sudo-guard', 'protect-claudemd',
|
|
863
|
+
'auto-approve-build', 'auto-approve-python', 'auto-approve-docker',
|
|
864
|
+
'loop-detector', 'deploy-guard', 'block-database-wipe',
|
|
865
|
+
'compound-command-approver', 'session-handoff', 'cost-tracker']
|
|
866
|
+
},
|
|
867
|
+
minimal: {
|
|
868
|
+
desc: 'Essential only — just the 8 core safety hooks',
|
|
869
|
+
hooks: ['destructive-guard', 'branch-guard', 'secret-guard', 'syntax-check',
|
|
870
|
+
'context-monitor', 'comment-strip', 'cd-git-allow', 'api-error-alert']
|
|
871
|
+
},
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
if (!level || !PROFILES[level]) {
|
|
875
|
+
console.log(c.bold + ' Safety Profiles' + c.reset);
|
|
876
|
+
console.log();
|
|
877
|
+
for (const [name, prof] of Object.entries(PROFILES)) {
|
|
878
|
+
console.log(` ${c.bold}${name}${c.reset} (${prof.hooks.length} hooks)`);
|
|
879
|
+
console.log(` ${c.dim}${prof.desc}${c.reset}`);
|
|
880
|
+
console.log(` ${c.dim}npx cc-safe-setup --profile ${name}${c.reset}`);
|
|
881
|
+
console.log();
|
|
882
|
+
}
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const prof = PROFILES[level];
|
|
887
|
+
console.log(c.bold + ` Applying "${level}" profile` + c.reset);
|
|
888
|
+
console.log(c.dim + ` ${prof.desc}` + c.reset);
|
|
889
|
+
console.log();
|
|
890
|
+
|
|
891
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
892
|
+
let installed = 0;
|
|
893
|
+
|
|
894
|
+
for (const hookId of prof.hooks) {
|
|
895
|
+
const hookPath = join(HOOKS_DIR, `${hookId}.sh`);
|
|
896
|
+
if (existsSync(hookPath)) {
|
|
897
|
+
console.log(c.dim + ' ✓' + c.reset + ` ${hookId}`);
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Try built-in first
|
|
902
|
+
if (SCRIPTS[hookId]) {
|
|
903
|
+
writeFileSync(hookPath, SCRIPTS[hookId]);
|
|
904
|
+
chmodSync(hookPath, 0o755);
|
|
905
|
+
installed++;
|
|
906
|
+
console.log(c.green + ' +' + c.reset + ` ${hookId}`);
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Try examples
|
|
911
|
+
const exPath = join(__dirname, 'examples', `${hookId}.sh`);
|
|
912
|
+
if (existsSync(exPath)) {
|
|
913
|
+
copyFileSync(exPath, hookPath);
|
|
914
|
+
chmodSync(hookPath, 0o755);
|
|
915
|
+
installed++;
|
|
916
|
+
console.log(c.green + ' +' + c.reset + ` ${hookId}`);
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
console.log(c.yellow + ' ?' + c.reset + ` ${hookId} (not found)`);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Update settings.json
|
|
924
|
+
let settings = {};
|
|
925
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
926
|
+
try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
|
|
927
|
+
}
|
|
928
|
+
if (!settings.hooks) settings.hooks = {};
|
|
929
|
+
|
|
930
|
+
// Register all hooks in settings
|
|
931
|
+
const hookFiles = prof.hooks.filter(h => existsSync(join(HOOKS_DIR, `${h}.sh`)));
|
|
932
|
+
const bashHooks = hookFiles.map(h => ({ type: 'command', command: `bash ${join(HOOKS_DIR, h + '.sh')}` }));
|
|
933
|
+
|
|
934
|
+
// Simplified: put all under PreToolUse Bash for now
|
|
935
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
936
|
+
const existing = settings.hooks.PreToolUse.find(e => e.matcher === 'Bash');
|
|
937
|
+
if (existing) {
|
|
938
|
+
const existingCmds = new Set(existing.hooks.map(h => h.command));
|
|
939
|
+
for (const h of bashHooks) {
|
|
940
|
+
if (!existingCmds.has(h.command)) existing.hooks.push(h);
|
|
941
|
+
}
|
|
942
|
+
} else {
|
|
943
|
+
settings.hooks.PreToolUse.push({ matcher: 'Bash', hooks: bashHooks });
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
947
|
+
|
|
948
|
+
console.log();
|
|
949
|
+
console.log(c.green + ` ✓ "${level}" profile applied (${installed} new hooks installed)` + c.reset);
|
|
950
|
+
console.log(c.dim + ` ${prof.hooks.length} hooks total in profile` + c.reset);
|
|
951
|
+
console.log();
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
async function analyze() {
|
|
955
|
+
const { execSync } = await import('child_process');
|
|
956
|
+
const { readdirSync, statSync } = await import('fs');
|
|
957
|
+
console.log();
|
|
958
|
+
console.log(c.bold + ' cc-safe-setup --analyze' + c.reset);
|
|
959
|
+
console.log(c.dim + ' What Claude did in your sessions' + c.reset);
|
|
960
|
+
console.log();
|
|
961
|
+
|
|
962
|
+
// 1. Blocked commands log
|
|
963
|
+
const blockLog = join(HOME, '.claude', 'blocked-commands.log');
|
|
964
|
+
let blocks = [];
|
|
965
|
+
if (existsSync(blockLog)) {
|
|
966
|
+
const content = readFileSync(blockLog, 'utf-8');
|
|
967
|
+
blocks = content.split('\n').filter(l => l.trim());
|
|
968
|
+
const recent = blocks.slice(-20);
|
|
969
|
+
console.log(c.bold + ' Blocked Commands' + c.reset + c.dim + ` (${blocks.length} total)` + c.reset);
|
|
970
|
+
if (recent.length > 0) {
|
|
971
|
+
// Count by type
|
|
972
|
+
const types = {};
|
|
973
|
+
for (const line of blocks) {
|
|
974
|
+
const match = line.match(/BLOCKED:\s*(.+?)(?:\s*—|\s*\(|$)/);
|
|
975
|
+
if (match) {
|
|
976
|
+
const type = match[1].trim().substring(0, 40);
|
|
977
|
+
types[type] = (types[type] || 0) + 1;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
const sorted = Object.entries(types).sort((a, b) => b[1] - a[1]);
|
|
981
|
+
for (const [type, count] of sorted.slice(0, 8)) {
|
|
982
|
+
const bar = '█'.repeat(Math.min(count, 20));
|
|
983
|
+
console.log(` ${c.red}${bar}${c.reset} ${count}× ${type}`);
|
|
984
|
+
}
|
|
985
|
+
} else {
|
|
986
|
+
console.log(c.green + ' No blocked commands recorded.' + c.reset);
|
|
987
|
+
}
|
|
988
|
+
console.log();
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// 2. Git activity (last 24h)
|
|
992
|
+
console.log(c.bold + ' Git Activity (last 24h)' + c.reset);
|
|
993
|
+
try {
|
|
994
|
+
const log = execSync('git log --oneline --since="24 hours ago" 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
995
|
+
if (log) {
|
|
996
|
+
const commits = log.split('\n');
|
|
997
|
+
console.log(c.dim + ` ${commits.length} commits` + c.reset);
|
|
998
|
+
for (const commit of commits.slice(0, 10)) {
|
|
999
|
+
console.log(` ${c.blue}•${c.reset} ${commit}`);
|
|
1000
|
+
}
|
|
1001
|
+
if (commits.length > 10) console.log(c.dim + ` ... and ${commits.length - 10} more` + c.reset);
|
|
1002
|
+
} else {
|
|
1003
|
+
console.log(c.dim + ' No commits in last 24h' + c.reset);
|
|
1004
|
+
}
|
|
1005
|
+
} catch {
|
|
1006
|
+
console.log(c.dim + ' Not in a git repository' + c.reset);
|
|
1007
|
+
}
|
|
1008
|
+
console.log();
|
|
1009
|
+
|
|
1010
|
+
// 3. Files changed (last 24h)
|
|
1011
|
+
console.log(c.bold + ' Files Changed (last 24h)' + c.reset);
|
|
1012
|
+
try {
|
|
1013
|
+
const diff = execSync('git diff --stat HEAD~10 2>/dev/null || git diff --stat 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
1014
|
+
if (diff) {
|
|
1015
|
+
const lines = diff.split('\n');
|
|
1016
|
+
const summary = lines[lines.length - 1];
|
|
1017
|
+
console.log(c.dim + ` ${summary.trim()}` + c.reset);
|
|
1018
|
+
}
|
|
1019
|
+
} catch {}
|
|
1020
|
+
console.log();
|
|
1021
|
+
|
|
1022
|
+
// 4. Hook health
|
|
1023
|
+
console.log(c.bold + ' Hook Health' + c.reset);
|
|
1024
|
+
const hookDir = join(HOME, '.claude', 'hooks');
|
|
1025
|
+
if (existsSync(hookDir)) {
|
|
1026
|
+
const hooks = readdirSync(hookDir).filter(f => f.endsWith('.sh') || f.endsWith('.py'));
|
|
1027
|
+
let execCount = 0, nonExec = 0;
|
|
1028
|
+
for (const h of hooks) {
|
|
1029
|
+
const st = statSync(join(hookDir, h));
|
|
1030
|
+
if (st.mode & 0o111) execCount++; else nonExec++;
|
|
1031
|
+
}
|
|
1032
|
+
console.log(` ${c.green}${execCount}${c.reset} hooks executable${nonExec > 0 ? `, ${c.red}${nonExec}${c.reset} missing permissions` : ''}`);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// 5. Context usage estimate
|
|
1036
|
+
console.log();
|
|
1037
|
+
console.log(c.bold + ' Session Estimates' + c.reset);
|
|
1038
|
+
// Check tool call log if exists
|
|
1039
|
+
const toolLog = join(HOME, '.claude', 'tool-calls.log');
|
|
1040
|
+
if (existsSync(toolLog)) {
|
|
1041
|
+
const logContent = readFileSync(toolLog, 'utf-8');
|
|
1042
|
+
const calls = logContent.split('\n').filter(l => l.trim());
|
|
1043
|
+
const today = new Date().toISOString().split('T')[0];
|
|
1044
|
+
const todayCalls = calls.filter(l => l.includes(today));
|
|
1045
|
+
console.log(` Tool calls today: ${todayCalls.length}`);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Token budget state
|
|
1049
|
+
const budgetFiles = existsSync('/tmp') ? readdirSync('/tmp').filter(f => f.startsWith('cc-token-budget-')) : [];
|
|
1050
|
+
if (budgetFiles.length > 0) {
|
|
1051
|
+
for (const bf of budgetFiles.slice(0, 3)) {
|
|
1052
|
+
const tokens = parseInt(readFileSync(join('/tmp', bf), 'utf-8').trim()) || 0;
|
|
1053
|
+
const costCents = Math.round(tokens * 75 / 10000);
|
|
1054
|
+
console.log(` Estimated cost: ~$${(costCents / 100).toFixed(2)} (${tokens.toLocaleString()} tokens)`);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
console.log();
|
|
1059
|
+
console.log(c.dim + ' Tip: Use --stats for block history analytics' + c.reset);
|
|
1060
|
+
console.log(c.dim + ' Tip: Use --dashboard for real-time monitoring' + c.reset);
|
|
1061
|
+
console.log();
|
|
1062
|
+
}
|
|
1063
|
+
|
|
836
1064
|
async function shield() {
|
|
837
1065
|
const { execSync } = await import('child_process');
|
|
838
1066
|
const { readdirSync } = await import('fs');
|
|
@@ -3042,6 +3270,8 @@ async function main() {
|
|
|
3042
3270
|
if (FULL) return fullSetup();
|
|
3043
3271
|
if (DOCTOR) return doctor();
|
|
3044
3272
|
if (WATCH) return watch();
|
|
3273
|
+
if (PROFILE_IDX !== -1) return profile(PROFILE);
|
|
3274
|
+
if (ANALYZE) return analyze();
|
|
3045
3275
|
if (SHIELD) return shield();
|
|
3046
3276
|
if (QUICKFIX) return quickfix();
|
|
3047
3277
|
if (REPORT) return report();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.3.0",
|
|
4
4
|
"description": "One command to make Claude Code safe. 59 hooks (8 built-in + 51 examples). 26 CLI commands: dashboard, create, audit, lint, diff, migrate, compare, generate-ci. 284 tests.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"bin": {
|