cc-safe-setup 4.0.2 → 5.0.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/README.md +1 -0
- package/index.mjs +162 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -249,6 +249,7 @@ Or browse all available examples in [`examples/`](examples/):
|
|
|
249
249
|
- [Hooks Cookbook](https://github.com/yurukusa/claude-code-hooks/blob/main/COOKBOOK.md) — 25 recipes from real GitHub Issues ([interactive version](https://yurukusa.github.io/claude-code-hooks/))
|
|
250
250
|
- [Japanese guide (Qiita)](https://qiita.com/yurukusa/items/a9714b33f5d974e8f1e8) — この記事の日本語解説
|
|
251
251
|
- [Hook Test Runner](https://github.com/yurukusa/cc-hook-test) — `npx cc-hook-test <hook.sh>` to auto-test any hook
|
|
252
|
+
- [Hook Registry](https://github.com/yurukusa/cc-hook-registry) — `npx cc-hook-registry search database` to find community hooks
|
|
252
253
|
- [Hooks Cheat Sheet](https://yurukusa.github.io/cc-safe-setup/cheatsheet.html) — printable A4 quick reference
|
|
253
254
|
- [Ecosystem Comparison](https://yurukusa.github.io/cc-safe-setup/ecosystem.html) — all Claude Code hook projects compared
|
|
254
255
|
- [The incident that inspired this tool](https://github.com/anthropics/claude-code/issues/36339) — NTFS junction rm -rf
|
package/index.mjs
CHANGED
|
@@ -363,14 +363,29 @@ function examples() {
|
|
|
363
363
|
},
|
|
364
364
|
};
|
|
365
365
|
|
|
366
|
+
// Optional category filter: --examples safety, --examples ux, etc.
|
|
367
|
+
const filterArg = process.argv[process.argv.indexOf('--examples') + 1] || process.argv[process.argv.indexOf('-e') + 1] || '';
|
|
368
|
+
const filter = filterArg.toLowerCase();
|
|
369
|
+
|
|
366
370
|
console.log();
|
|
367
|
-
console.log(c.bold + ' cc-safe-setup --examples' + c.reset);
|
|
368
|
-
console.log(c.dim + '
|
|
371
|
+
console.log(c.bold + ' cc-safe-setup --examples' + c.reset + (filter ? ' ' + filter : ''));
|
|
372
|
+
console.log(c.dim + ' 38 hooks beyond the 8 built-in ones' + c.reset);
|
|
373
|
+
if (filter) console.log(c.dim + ' Filter: ' + filter + c.reset);
|
|
369
374
|
console.log();
|
|
370
375
|
|
|
371
376
|
for (const [cat, hooks] of Object.entries(CATEGORIES)) {
|
|
377
|
+
// Filter by category name OR hook name/description
|
|
378
|
+
const filteredHooks = filter
|
|
379
|
+
? Object.entries(hooks).filter(([file, desc]) =>
|
|
380
|
+
cat.toLowerCase().includes(filter) ||
|
|
381
|
+
file.toLowerCase().includes(filter) ||
|
|
382
|
+
desc.toLowerCase().includes(filter))
|
|
383
|
+
: Object.entries(hooks);
|
|
384
|
+
|
|
385
|
+
if (filteredHooks.length === 0) continue;
|
|
386
|
+
|
|
372
387
|
console.log(' ' + c.bold + c.blue + cat + c.reset);
|
|
373
|
-
for (const [file, desc] of
|
|
388
|
+
for (const [file, desc] of filteredHooks) {
|
|
374
389
|
console.log(' ' + c.green + '*' + c.reset + ' ' + c.bold + file + c.reset);
|
|
375
390
|
console.log(' ' + c.dim + desc + c.reset);
|
|
376
391
|
}
|
|
@@ -791,70 +806,182 @@ async function fullSetup() {
|
|
|
791
806
|
}
|
|
792
807
|
|
|
793
808
|
async function dashboard() {
|
|
794
|
-
const
|
|
795
|
-
const { createInterface: createRL } = await import('readline');
|
|
809
|
+
const fsModule = await import('fs');
|
|
796
810
|
|
|
797
811
|
const BLOCK_LOG = join(HOME, '.claude', 'blocked-commands.log');
|
|
812
|
+
const ERROR_LOG = join(HOME, '.claude', 'session-errors.log');
|
|
798
813
|
const COST_FILE = '/tmp/cc-cost-tracker-calls';
|
|
799
814
|
const CONTEXT_FILE = '/tmp/cc-context-pct';
|
|
815
|
+
const HANDOFF_FILE = join(HOME, '.claude', 'session-handoff.md');
|
|
816
|
+
const W = 60; // dashboard width
|
|
800
817
|
|
|
801
818
|
const clear = () => process.stdout.write('\x1b[2J\x1b[H');
|
|
802
819
|
|
|
803
|
-
//
|
|
804
|
-
|
|
805
|
-
|
|
820
|
+
// ANSI box drawing helpers
|
|
821
|
+
const box = {
|
|
822
|
+
tl: '┌', tr: '┐', bl: '└', br: '┘',
|
|
823
|
+
h: '─', v: '│', lt: '├', rt: '┤',
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
function hline(left, right, w) { return left + box.h.repeat(w - 2) + right; }
|
|
827
|
+
function pad(text, w) {
|
|
828
|
+
const stripped = text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
829
|
+
const padding = Math.max(0, w - 2 - stripped.length);
|
|
830
|
+
return box.v + ' ' + text + ' '.repeat(padding) + box.v;
|
|
831
|
+
}
|
|
832
|
+
function progressBar(pct, w, filledColor, emptyColor) {
|
|
833
|
+
const barW = w - 2;
|
|
834
|
+
const filled = Math.round(pct / 100 * barW);
|
|
835
|
+
return filledColor + '█'.repeat(filled) + emptyColor + '░'.repeat(barW - filled) + c.reset;
|
|
836
|
+
}
|
|
837
|
+
function sparkline(values, w) {
|
|
838
|
+
const chars = ' ▁▂▃▄▅▆▇';
|
|
839
|
+
const max = Math.max(...values, 1);
|
|
840
|
+
return values.slice(-w).map(v => chars[Math.min(7, Math.round(v / max * 7))]).join('');
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Collect hook info
|
|
844
|
+
let hooksByTrigger = {};
|
|
845
|
+
let totalHooks = 0;
|
|
846
|
+
let scriptCount = 0;
|
|
806
847
|
if (existsSync(SETTINGS_PATH)) {
|
|
807
848
|
try {
|
|
808
849
|
const s = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
809
|
-
for (const entries of Object.
|
|
810
|
-
|
|
850
|
+
for (const [trigger, entries] of Object.entries(s.hooks || {})) {
|
|
851
|
+
const count = entries.reduce((n, e) => n + (e.hooks || []).length, 0);
|
|
852
|
+
hooksByTrigger[trigger] = count;
|
|
853
|
+
totalHooks += count;
|
|
811
854
|
}
|
|
812
855
|
} catch {}
|
|
813
856
|
}
|
|
814
|
-
|
|
815
|
-
|
|
857
|
+
if (existsSync(HOOKS_DIR)) {
|
|
858
|
+
scriptCount = fsModule.readdirSync(HOOKS_DIR).filter(f => f.endsWith('.sh')).length;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Audit score (cached)
|
|
862
|
+
let auditScore = '?';
|
|
863
|
+
try {
|
|
864
|
+
// Quick inline audit
|
|
865
|
+
let risks = 0;
|
|
866
|
+
const s = existsSync(SETTINGS_PATH) ? JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')) : {};
|
|
867
|
+
const pre = s.hooks?.PreToolUse || [];
|
|
868
|
+
const post = s.hooks?.PostToolUse || [];
|
|
869
|
+
if (pre.length === 0) risks += 30;
|
|
870
|
+
const allCmds = JSON.stringify(pre).toLowerCase();
|
|
871
|
+
if (!allCmds.match(/destructive|guard|rm.*rf/)) risks += 20;
|
|
872
|
+
if (!allCmds.match(/branch|push|main/)) risks += 20;
|
|
873
|
+
if (!allCmds.match(/secret|env|credential/)) risks += 20;
|
|
874
|
+
if (post.length === 0) risks += 10;
|
|
875
|
+
auditScore = Math.max(0, 100 - risks);
|
|
876
|
+
} catch {}
|
|
816
877
|
|
|
817
878
|
function render() {
|
|
818
879
|
clear();
|
|
819
880
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
881
|
+
const now = new Date();
|
|
882
|
+
const timeStr = now.toLocaleTimeString();
|
|
883
|
+
const dateStr = now.toLocaleDateString();
|
|
823
884
|
|
|
824
|
-
//
|
|
825
|
-
const
|
|
826
|
-
const
|
|
827
|
-
const
|
|
885
|
+
// Read live data
|
|
886
|
+
const contextPct = existsSync(CONTEXT_FILE) ? parseInt(readFileSync(CONTEXT_FILE, 'utf-8').trim()) || 0 : -1;
|
|
887
|
+
const toolCalls = existsSync(COST_FILE) ? parseInt(readFileSync(COST_FILE, 'utf-8').trim()) || 0 : 0;
|
|
888
|
+
const costEst = (toolCalls * 0.105).toFixed(2);
|
|
828
889
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
// Recent blocks
|
|
834
|
-
console.log(c.bold + ' Recent Blocks' + c.reset);
|
|
890
|
+
// Parse block log
|
|
891
|
+
let blocks = [];
|
|
892
|
+
let blocksByHour = new Array(24).fill(0);
|
|
893
|
+
let blockReasons = {};
|
|
835
894
|
if (existsSync(BLOCK_LOG)) {
|
|
836
895
|
const lines = readFileSync(BLOCK_LOG, 'utf-8').split('\n').filter(l => l.trim());
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
const m = line.match(/^\[([^\]]+)\]\s*BLOCKED:\s*(.+?)\s*\|/);
|
|
896
|
+
for (const line of lines) {
|
|
897
|
+
const m = line.match(/^\[([^\]]+)\]\s*BLOCKED:\s*(.+?)\s*\|\s*cmd:\s*(.+)$/);
|
|
840
898
|
if (m) {
|
|
841
|
-
const
|
|
842
|
-
|
|
899
|
+
const hour = new Date(m[1]).getHours();
|
|
900
|
+
if (!isNaN(hour)) blocksByHour[hour]++;
|
|
901
|
+
const reason = m[2].trim();
|
|
902
|
+
blockReasons[reason] = (blockReasons[reason] || 0) + 1;
|
|
903
|
+
blocks.push({ time: m[1], reason, cmd: m[3].trim() });
|
|
843
904
|
}
|
|
844
905
|
}
|
|
845
|
-
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const totalBlocks = blocks.length;
|
|
909
|
+
const todayBlocks = blocks.filter(b => b.time.startsWith(dateStr.split('/').reverse().join('-'))).length;
|
|
910
|
+
|
|
911
|
+
// === RENDER ===
|
|
912
|
+
console.log(hline(box.tl, box.tr, W));
|
|
913
|
+
console.log(pad(c.bold + 'cc-safe-setup dashboard' + c.reset + ' ' + c.dim + timeStr + c.reset, W));
|
|
914
|
+
console.log(hline(box.lt, box.rt, W));
|
|
915
|
+
|
|
916
|
+
// Status panel
|
|
917
|
+
const scoreColor = auditScore >= 80 ? c.green : auditScore >= 50 ? c.yellow : c.red;
|
|
918
|
+
const grade = auditScore >= 80 ? 'A' : auditScore >= 60 ? 'B' : auditScore >= 40 ? 'C' : 'F';
|
|
919
|
+
console.log(pad('Score: ' + scoreColor + auditScore + '/100' + c.reset + ' (Grade ' + grade + ') Hooks: ' + c.green + totalHooks + c.reset + ' Scripts: ' + scriptCount, W));
|
|
920
|
+
|
|
921
|
+
// Context bar
|
|
922
|
+
if (contextPct >= 0) {
|
|
923
|
+
const ctxColor = contextPct > 40 ? c.green : contextPct > 20 ? c.yellow : c.red;
|
|
924
|
+
console.log(pad('Context: ' + ctxColor + contextPct + '%' + c.reset + ' ' + progressBar(contextPct, 30, ctxColor, c.dim), W));
|
|
846
925
|
} else {
|
|
847
|
-
console.log(c.dim + '
|
|
926
|
+
console.log(pad('Context: ' + c.dim + 'unknown' + c.reset, W));
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Cost
|
|
930
|
+
console.log(pad('Cost: ~$' + costEst + ' (' + toolCalls + ' tool calls, Opus)', W));
|
|
931
|
+
console.log(pad('Blocks: ' + c.red + totalBlocks + c.reset + ' total | Today: ' + todayBlocks, W));
|
|
932
|
+
|
|
933
|
+
// Hooks by trigger
|
|
934
|
+
console.log(hline(box.lt, box.rt, W));
|
|
935
|
+
console.log(pad(c.bold + 'Hooks by Trigger' + c.reset, W));
|
|
936
|
+
for (const [trigger, count] of Object.entries(hooksByTrigger)) {
|
|
937
|
+
const bar = '█'.repeat(Math.min(count, 20));
|
|
938
|
+
console.log(pad(c.dim + trigger.padEnd(18) + c.reset + c.blue + bar + c.reset + ' ' + count, W));
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Hourly activity sparkline
|
|
942
|
+
console.log(hline(box.lt, box.rt, W));
|
|
943
|
+
console.log(pad(c.bold + 'Block Activity (24h)' + c.reset, W));
|
|
944
|
+
console.log(pad(c.yellow + sparkline(blocksByHour, 24) + c.reset + ' ' + c.dim + '0h' + ' '.repeat(20) + '23h' + c.reset, W));
|
|
945
|
+
|
|
946
|
+
// Top block reasons
|
|
947
|
+
console.log(hline(box.lt, box.rt, W));
|
|
948
|
+
console.log(pad(c.bold + 'Top Block Reasons' + c.reset, W));
|
|
949
|
+
const sortedReasons = Object.entries(blockReasons).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
950
|
+
const maxR = sortedReasons[0]?.[1] || 1;
|
|
951
|
+
for (const [reason, count] of sortedReasons) {
|
|
952
|
+
const bar = '▓'.repeat(Math.ceil(count / maxR * 15));
|
|
953
|
+
console.log(pad(c.red + bar + c.reset + ' ' + count + ' ' + c.dim + reason.slice(0, 25) + c.reset, W));
|
|
954
|
+
}
|
|
955
|
+
if (sortedReasons.length === 0) {
|
|
956
|
+
console.log(pad(c.dim + '(no blocks recorded)' + c.reset, W));
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Recent blocks
|
|
960
|
+
console.log(hline(box.lt, box.rt, W));
|
|
961
|
+
console.log(pad(c.bold + 'Recent Blocks' + c.reset, W));
|
|
962
|
+
const recent = blocks.slice(-5);
|
|
963
|
+
for (const b of recent) {
|
|
964
|
+
const time = b.time.replace(/T/, ' ').replace(/\+.*/, '').slice(11, 16);
|
|
965
|
+
console.log(pad(c.dim + time + c.reset + ' ' + c.red + b.reason.slice(0, 35) + c.reset, W));
|
|
848
966
|
}
|
|
967
|
+
if (recent.length === 0) console.log(pad(c.dim + '(none)' + c.reset, W));
|
|
849
968
|
|
|
850
|
-
|
|
969
|
+
// Session errors
|
|
970
|
+
let errorCount = 0;
|
|
971
|
+
if (existsSync(ERROR_LOG)) {
|
|
972
|
+
errorCount = readFileSync(ERROR_LOG, 'utf-8').split('\n').filter(l => l.trim()).length;
|
|
973
|
+
}
|
|
974
|
+
if (errorCount > 0) {
|
|
975
|
+
console.log(hline(box.lt, box.rt, W));
|
|
976
|
+
console.log(pad(c.yellow + 'Session errors: ' + errorCount + c.reset, W));
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
console.log(hline(box.bl, box.br, W));
|
|
851
980
|
console.log(c.dim + ' Refreshing every 3s. Ctrl+C to exit.' + c.reset);
|
|
852
981
|
}
|
|
853
982
|
|
|
854
983
|
render();
|
|
855
984
|
setInterval(render, 3000);
|
|
856
|
-
|
|
857
|
-
// Keep alive
|
|
858
985
|
await new Promise(() => {});
|
|
859
986
|
}
|
|
860
987
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "One command to make Claude Code safe for autonomous operation. 8 built-in + 38 examples. 22 commands including dashboard, create, audit, lint, diff, benchmark. 2,500+ daily npm downloads.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"bin": {
|