cc-safe-setup 4.0.3 → 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 +155 -34
- 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
|
@@ -374,9 +374,18 @@ function examples() {
|
|
|
374
374
|
console.log();
|
|
375
375
|
|
|
376
376
|
for (const [cat, hooks] of Object.entries(CATEGORIES)) {
|
|
377
|
-
|
|
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
|
+
|
|
378
387
|
console.log(' ' + c.bold + c.blue + cat + c.reset);
|
|
379
|
-
for (const [file, desc] of
|
|
388
|
+
for (const [file, desc] of filteredHooks) {
|
|
380
389
|
console.log(' ' + c.green + '*' + c.reset + ' ' + c.bold + file + c.reset);
|
|
381
390
|
console.log(' ' + c.dim + desc + c.reset);
|
|
382
391
|
}
|
|
@@ -797,70 +806,182 @@ async function fullSetup() {
|
|
|
797
806
|
}
|
|
798
807
|
|
|
799
808
|
async function dashboard() {
|
|
800
|
-
const
|
|
801
|
-
const { createInterface: createRL } = await import('readline');
|
|
809
|
+
const fsModule = await import('fs');
|
|
802
810
|
|
|
803
811
|
const BLOCK_LOG = join(HOME, '.claude', 'blocked-commands.log');
|
|
812
|
+
const ERROR_LOG = join(HOME, '.claude', 'session-errors.log');
|
|
804
813
|
const COST_FILE = '/tmp/cc-cost-tracker-calls';
|
|
805
814
|
const CONTEXT_FILE = '/tmp/cc-context-pct';
|
|
815
|
+
const HANDOFF_FILE = join(HOME, '.claude', 'session-handoff.md');
|
|
816
|
+
const W = 60; // dashboard width
|
|
806
817
|
|
|
807
818
|
const clear = () => process.stdout.write('\x1b[2J\x1b[H');
|
|
808
819
|
|
|
809
|
-
//
|
|
810
|
-
|
|
811
|
-
|
|
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;
|
|
812
847
|
if (existsSync(SETTINGS_PATH)) {
|
|
813
848
|
try {
|
|
814
849
|
const s = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
815
|
-
for (const entries of Object.
|
|
816
|
-
|
|
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;
|
|
817
854
|
}
|
|
818
855
|
} catch {}
|
|
819
856
|
}
|
|
820
|
-
|
|
821
|
-
|
|
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 {}
|
|
822
877
|
|
|
823
878
|
function render() {
|
|
824
879
|
clear();
|
|
825
880
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
// Status row
|
|
831
|
-
const context = existsSync(CONTEXT_FILE) ? readFileSync(CONTEXT_FILE, 'utf-8').trim() + '%' : '?';
|
|
832
|
-
const calls = existsSync(COST_FILE) ? readFileSync(COST_FILE, 'utf-8').trim() : '0';
|
|
833
|
-
const cost = (parseInt(calls) * 0.105).toFixed(2);
|
|
881
|
+
const now = new Date();
|
|
882
|
+
const timeStr = now.toLocaleTimeString();
|
|
883
|
+
const dateStr = now.toLocaleDateString();
|
|
834
884
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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);
|
|
838
889
|
|
|
839
|
-
//
|
|
840
|
-
|
|
890
|
+
// Parse block log
|
|
891
|
+
let blocks = [];
|
|
892
|
+
let blocksByHour = new Array(24).fill(0);
|
|
893
|
+
let blockReasons = {};
|
|
841
894
|
if (existsSync(BLOCK_LOG)) {
|
|
842
895
|
const lines = readFileSync(BLOCK_LOG, 'utf-8').split('\n').filter(l => l.trim());
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
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*(.+)$/);
|
|
846
898
|
if (m) {
|
|
847
|
-
const
|
|
848
|
-
|
|
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() });
|
|
849
904
|
}
|
|
850
905
|
}
|
|
851
|
-
|
|
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));
|
|
852
925
|
} else {
|
|
853
|
-
console.log(c.dim + '
|
|
926
|
+
console.log(pad('Context: ' + c.dim + 'unknown' + c.reset, W));
|
|
854
927
|
}
|
|
855
928
|
|
|
856
|
-
|
|
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));
|
|
966
|
+
}
|
|
967
|
+
if (recent.length === 0) console.log(pad(c.dim + '(none)' + c.reset, W));
|
|
968
|
+
|
|
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));
|
|
857
980
|
console.log(c.dim + ' Refreshing every 3s. Ctrl+C to exit.' + c.reset);
|
|
858
981
|
}
|
|
859
982
|
|
|
860
983
|
render();
|
|
861
984
|
setInterval(render, 3000);
|
|
862
|
-
|
|
863
|
-
// Keep alive
|
|
864
985
|
await new Promise(() => {});
|
|
865
986
|
}
|
|
866
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": {
|