cc-safe-setup 8.0.0 → 8.2.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 +3 -1
- package/examples/backup-before-refactor.sh +10 -0
- package/examples/branch-naming-convention.sh +13 -0
- package/examples/changelog-reminder.sh +22 -0
- package/examples/file-size-limit.sh +12 -0
- package/examples/hardcoded-secret-detector.sh +60 -0
- package/examples/license-check.sh +13 -0
- package/examples/no-console-log.sh +10 -0
- package/examples/no-eval.sh +9 -0
- package/examples/no-todo-ship.sh +12 -0
- package/examples/no-wildcard-import.sh +9 -0
- package/examples/pr-description-check.sh +11 -0
- package/examples/rate-limit-guard.sh +15 -0
- package/index.mjs +113 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**One command to make Claude Code safe for autonomous operation.** [日本語](docs/README.ja.md)
|
|
8
8
|
|
|
9
|
-
8 built-in +
|
|
9
|
+
8 built-in + 92 examples = **100 hooks**. 29 CLI commands. 433 tests. [Web Tool](https://yurukusa.github.io/cc-safe-setup/) · [Cheat Sheet](https://yurukusa.github.io/cc-safe-setup/hooks-cheatsheet.html) · [Builder](https://yurukusa.github.io/cc-safe-setup/builder.html) · [FAQ](https://yurukusa.github.io/cc-safe-setup/faq.html) · [Playground](https://yurukusa.github.io/cc-hook-registry/playground.html)
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npx cc-safe-setup
|
|
@@ -103,6 +103,8 @@ Each hook exists because a real incident happened without it.
|
|
|
103
103
|
|
|
104
104
|
Safe to run multiple times. Existing settings are preserved. A backup is created if settings.json can't be parsed.
|
|
105
105
|
|
|
106
|
+
**Maximum safety:** `npx cc-safe-setup --shield` — one command: fix environment, install hooks, detect stack, configure settings, generate CLAUDE.md.
|
|
107
|
+
|
|
106
108
|
**Preview first:** `npx cc-safe-setup --dry-run`
|
|
107
109
|
|
|
108
110
|
**Check status:** `npx cc-safe-setup --status` — see which hooks are installed (exit code 1 if missing).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
INPUT=$(cat)
|
|
2
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
3
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null)
|
|
4
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
5
|
+
[ -z "$COMMAND" ] && exit 0
|
|
6
|
+
if echo "$COMMAND" | grep -qE '\bgit\s+mv\b.*\b(src|lib|app)\b'; then
|
|
7
|
+
git stash push -m "pre-refactor-backup-$(date +%s)" 2>/dev/null
|
|
8
|
+
echo "NOTE: Stashed changes as pre-refactor backup." >&2
|
|
9
|
+
fi
|
|
10
|
+
exit 0
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
INPUT=$(cat)
|
|
2
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
3
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null)
|
|
4
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
5
|
+
[ -z "$COMMAND" ] && exit 0
|
|
6
|
+
if echo "$COMMAND" | grep -qE '\bgit\s+(checkout|switch)\s+-b\s+'; then
|
|
7
|
+
BRANCH=$(echo "$COMMAND" | grep -oE '(-b|--create)\s+(\S+)' | awk '{print $2}')
|
|
8
|
+
if [ -n "$BRANCH" ] && ! echo "$BRANCH" | grep -qE '^(feat|fix|chore|docs|test|refactor)/'; then
|
|
9
|
+
echo "WARNING: Branch '$BRANCH' doesn't follow convention." >&2
|
|
10
|
+
echo "Use: feat/, fix/, chore/, docs/, test/, refactor/" >&2
|
|
11
|
+
fi
|
|
12
|
+
fi
|
|
13
|
+
exit 0
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# changelog-reminder.sh — Remind to update CHANGELOG on version bump
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# When Claude bumps a version number (npm version, cargo set-version,
|
|
7
|
+
# etc.), this hook reminds to update CHANGELOG.md with the changes.
|
|
8
|
+
#
|
|
9
|
+
# TRIGGER: PostToolUse MATCHER: "Bash"
|
|
10
|
+
# ================================================================
|
|
11
|
+
|
|
12
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
13
|
+
[ -z "$COMMAND" ] && exit 0
|
|
14
|
+
|
|
15
|
+
# Detect version bump commands
|
|
16
|
+
if echo "$COMMAND" | grep -qE '(npm\s+version|cargo\s+set-version|bump2version|poetry\s+version)'; then
|
|
17
|
+
if [ -f "CHANGELOG.md" ]; then
|
|
18
|
+
echo "REMINDER: Update CHANGELOG.md with the new version's changes." >&2
|
|
19
|
+
fi
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
exit 0
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
INPUT=$(cat)
|
|
2
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
3
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null)
|
|
4
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
5
|
+
[ -z "$CONTENT" ] && exit 0
|
|
6
|
+
LEN=${#CONTENT}
|
|
7
|
+
MAX="${CC_MAX_FILE_SIZE:-1048576}"
|
|
8
|
+
if [ "$LEN" -gt "$MAX" ]; then
|
|
9
|
+
echo "BLOCKED: File content is ${LEN} bytes (limit: ${MAX})." >&2
|
|
10
|
+
exit 2
|
|
11
|
+
fi
|
|
12
|
+
exit 0
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# hardcoded-secret-detector.sh — Detect hardcoded secrets in edits
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude sometimes hardcodes API keys, passwords, or tokens
|
|
7
|
+
# directly into source files instead of using environment
|
|
8
|
+
# variables. This hook checks edited content for secret patterns.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PostToolUse MATCHER: "Edit|Write"
|
|
11
|
+
# ================================================================
|
|
12
|
+
|
|
13
|
+
INPUT=$(cat)
|
|
14
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null)
|
|
15
|
+
[ -z "$CONTENT" ] && exit 0
|
|
16
|
+
|
|
17
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
18
|
+
|
|
19
|
+
# Skip config/env files (secrets are expected there)
|
|
20
|
+
case "$FILE" in
|
|
21
|
+
*.env*|*credentials*|*secret*|*.key|*.pem) exit 0 ;;
|
|
22
|
+
esac
|
|
23
|
+
|
|
24
|
+
FOUND=0
|
|
25
|
+
|
|
26
|
+
# AWS keys (AKIA...)
|
|
27
|
+
if echo "$CONTENT" | grep -qE 'AKIA[0-9A-Z]{16}'; then
|
|
28
|
+
echo "WARNING: Possible AWS access key in $FILE" >&2
|
|
29
|
+
FOUND=1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Generic API key patterns
|
|
33
|
+
if echo "$CONTENT" | grep -qE "(api_key|apikey|api-key|secret_key|access_token)\s*[=:]\s*['\"][a-zA-Z0-9]{20,}['\"]"; then
|
|
34
|
+
echo "WARNING: Possible hardcoded API key in $FILE" >&2
|
|
35
|
+
FOUND=1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Password patterns
|
|
39
|
+
if echo "$CONTENT" | grep -qiE "(password|passwd|pwd)\s*[=:]\s*['\"][^'\"]{8,}['\"]"; then
|
|
40
|
+
echo "WARNING: Possible hardcoded password in $FILE" >&2
|
|
41
|
+
FOUND=1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# JWT tokens
|
|
45
|
+
if echo "$CONTENT" | grep -qE 'eyJ[a-zA-Z0-9_-]{20,}\.eyJ[a-zA-Z0-9_-]{20,}'; then
|
|
46
|
+
echo "WARNING: Possible JWT token in $FILE" >&2
|
|
47
|
+
FOUND=1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Private keys
|
|
51
|
+
if echo "$CONTENT" | grep -qE 'BEGIN (RSA |EC |DSA )?PRIVATE KEY'; then
|
|
52
|
+
echo "WARNING: Private key detected in $FILE" >&2
|
|
53
|
+
FOUND=1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
if [ "$FOUND" -eq 1 ]; then
|
|
57
|
+
echo "Use environment variables instead of hardcoding secrets." >&2
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
exit 0
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# license-check.sh — Warn when creating files without a license header
|
|
3
|
+
# TRIGGER: PostToolUse MATCHER: "Write"
|
|
4
|
+
FILE=$(cat | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
5
|
+
[ -z "$FILE" ] && exit 0
|
|
6
|
+
case "$FILE" in *.js|*.ts|*.py|*.go|*.rs|*.java|*.rb|*.sh) ;; *) exit 0 ;; esac
|
|
7
|
+
[ ! -f "$FILE" ] && exit 0
|
|
8
|
+
if ! head -5 "$FILE" | grep -qiE '(license|copyright|MIT|Apache|GPL)'; then
|
|
9
|
+
if [ -f "LICENSE" ] || [ -f "LICENSE.md" ]; then
|
|
10
|
+
echo "NOTE: New source file $FILE has no license header." >&2
|
|
11
|
+
fi
|
|
12
|
+
fi
|
|
13
|
+
exit 0
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
INPUT=$(cat)
|
|
2
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
3
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null)
|
|
4
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
5
|
+
[ -z "$CONTENT" ] && exit 0
|
|
6
|
+
case "$FILE" in *.test.*|*.spec.*|*debug*) exit 0 ;; esac
|
|
7
|
+
if echo "$CONTENT" | grep -qE '\bconsole\.(log|debug)\b'; then
|
|
8
|
+
echo "WARNING: console.log detected in $FILE. Use proper logging." >&2
|
|
9
|
+
fi
|
|
10
|
+
exit 0
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
INPUT=$(cat)
|
|
2
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
3
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null)
|
|
4
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
5
|
+
[ -z "$CONTENT" ] && exit 0
|
|
6
|
+
if echo "$CONTENT" | grep -qE '\beval\s*\('; then
|
|
7
|
+
echo "WARNING: eval() detected in $FILE. Avoid eval for security." >&2
|
|
8
|
+
fi
|
|
9
|
+
exit 0
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# no-todo-ship.sh — Block commits with TODO/FIXME/HACK markers
|
|
3
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
4
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
5
|
+
[ -z "$COMMAND" ] && exit 0
|
|
6
|
+
echo "$COMMAND" | grep -qE '^\s*git\s+commit' || exit 0
|
|
7
|
+
TODOS=$(git diff --cached 2>/dev/null | grep -cE '^\+.*\b(TODO|FIXME|HACK|XXX)\b' || echo 0)
|
|
8
|
+
if [ "$TODOS" -gt 0 ]; then
|
|
9
|
+
echo "WARNING: $TODOS TODO/FIXME/HACK markers in staged changes." >&2
|
|
10
|
+
echo "Resolve them before shipping, or document why they're needed." >&2
|
|
11
|
+
fi
|
|
12
|
+
exit 0
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
INPUT=$(cat)
|
|
2
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
3
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null)
|
|
4
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
5
|
+
[ -z "$CONTENT" ] && exit 0
|
|
6
|
+
if echo "$CONTENT" | grep -qE '(from\s+\S+\s+import\s+\*|import\s+\*\s+from)'; then
|
|
7
|
+
echo "WARNING: Wildcard import detected. Import specific names." >&2
|
|
8
|
+
fi
|
|
9
|
+
exit 0
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
INPUT=$(cat)
|
|
2
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
3
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null)
|
|
4
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
5
|
+
[ -z "$COMMAND" ] && exit 0
|
|
6
|
+
if echo "$COMMAND" | grep -qE '\bgh\s+pr\s+create\b'; then
|
|
7
|
+
if ! echo "$COMMAND" | grep -qE '\-\-body|\-b\s'; then
|
|
8
|
+
echo "WARNING: PR created without --body description." >&2
|
|
9
|
+
fi
|
|
10
|
+
fi
|
|
11
|
+
exit 0
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
INPUT=$(cat)
|
|
2
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
3
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null)
|
|
4
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
5
|
+
STATE="/tmp/cc-rate-limit-$(echo "$PWD" | md5sum | cut -c1-8)"
|
|
6
|
+
NOW=$(date +%s)
|
|
7
|
+
if [ -f "$STATE" ]; then
|
|
8
|
+
LAST=$(cat "$STATE")
|
|
9
|
+
DIFF=$((NOW - LAST))
|
|
10
|
+
if [ "$DIFF" -lt 1 ]; then
|
|
11
|
+
echo "WARNING: Rapid tool calls (${DIFF}s apart). Slow down." >&2
|
|
12
|
+
fi
|
|
13
|
+
fi
|
|
14
|
+
echo "$NOW" > "$STATE"
|
|
15
|
+
exit 0
|
package/index.mjs
CHANGED
|
@@ -91,6 +91,7 @@ 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');
|
|
94
95
|
const COMPARE_IDX = process.argv.findIndex(a => a === '--compare');
|
|
95
96
|
const COMPARE = COMPARE_IDX !== -1 ? { a: process.argv[COMPARE_IDX + 1], b: process.argv[COMPARE_IDX + 2] } : null;
|
|
96
97
|
const CREATE_IDX = process.argv.findIndex(a => a === '--create');
|
|
@@ -126,6 +127,7 @@ if (HELP) {
|
|
|
126
127
|
npx cc-safe-setup --doctor Diagnose why hooks aren't working
|
|
127
128
|
npx cc-safe-setup --watch Live dashboard of blocked commands
|
|
128
129
|
npx cc-safe-setup --create "<desc>" Generate a custom hook from description
|
|
130
|
+
npx cc-safe-setup --analyze Analyze what Claude did in your last session
|
|
129
131
|
npx cc-safe-setup --shield Maximum safety in one command (fix + scan + install + CLAUDE.md)
|
|
130
132
|
npx cc-safe-setup --quickfix Auto-detect and fix common Claude Code problems
|
|
131
133
|
npx cc-safe-setup --stats Block statistics and patterns report
|
|
@@ -833,6 +835,116 @@ async function fullSetup() {
|
|
|
833
835
|
console.log();
|
|
834
836
|
}
|
|
835
837
|
|
|
838
|
+
async function analyze() {
|
|
839
|
+
const { execSync } = await import('child_process');
|
|
840
|
+
const { readdirSync, statSync } = await import('fs');
|
|
841
|
+
console.log();
|
|
842
|
+
console.log(c.bold + ' cc-safe-setup --analyze' + c.reset);
|
|
843
|
+
console.log(c.dim + ' What Claude did in your sessions' + c.reset);
|
|
844
|
+
console.log();
|
|
845
|
+
|
|
846
|
+
// 1. Blocked commands log
|
|
847
|
+
const blockLog = join(HOME, '.claude', 'blocked-commands.log');
|
|
848
|
+
let blocks = [];
|
|
849
|
+
if (existsSync(blockLog)) {
|
|
850
|
+
const content = readFileSync(blockLog, 'utf-8');
|
|
851
|
+
blocks = content.split('\n').filter(l => l.trim());
|
|
852
|
+
const recent = blocks.slice(-20);
|
|
853
|
+
console.log(c.bold + ' Blocked Commands' + c.reset + c.dim + ` (${blocks.length} total)` + c.reset);
|
|
854
|
+
if (recent.length > 0) {
|
|
855
|
+
// Count by type
|
|
856
|
+
const types = {};
|
|
857
|
+
for (const line of blocks) {
|
|
858
|
+
const match = line.match(/BLOCKED:\s*(.+?)(?:\s*—|\s*\(|$)/);
|
|
859
|
+
if (match) {
|
|
860
|
+
const type = match[1].trim().substring(0, 40);
|
|
861
|
+
types[type] = (types[type] || 0) + 1;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
const sorted = Object.entries(types).sort((a, b) => b[1] - a[1]);
|
|
865
|
+
for (const [type, count] of sorted.slice(0, 8)) {
|
|
866
|
+
const bar = '█'.repeat(Math.min(count, 20));
|
|
867
|
+
console.log(` ${c.red}${bar}${c.reset} ${count}× ${type}`);
|
|
868
|
+
}
|
|
869
|
+
} else {
|
|
870
|
+
console.log(c.green + ' No blocked commands recorded.' + c.reset);
|
|
871
|
+
}
|
|
872
|
+
console.log();
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// 2. Git activity (last 24h)
|
|
876
|
+
console.log(c.bold + ' Git Activity (last 24h)' + c.reset);
|
|
877
|
+
try {
|
|
878
|
+
const log = execSync('git log --oneline --since="24 hours ago" 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
879
|
+
if (log) {
|
|
880
|
+
const commits = log.split('\n');
|
|
881
|
+
console.log(c.dim + ` ${commits.length} commits` + c.reset);
|
|
882
|
+
for (const commit of commits.slice(0, 10)) {
|
|
883
|
+
console.log(` ${c.blue}•${c.reset} ${commit}`);
|
|
884
|
+
}
|
|
885
|
+
if (commits.length > 10) console.log(c.dim + ` ... and ${commits.length - 10} more` + c.reset);
|
|
886
|
+
} else {
|
|
887
|
+
console.log(c.dim + ' No commits in last 24h' + c.reset);
|
|
888
|
+
}
|
|
889
|
+
} catch {
|
|
890
|
+
console.log(c.dim + ' Not in a git repository' + c.reset);
|
|
891
|
+
}
|
|
892
|
+
console.log();
|
|
893
|
+
|
|
894
|
+
// 3. Files changed (last 24h)
|
|
895
|
+
console.log(c.bold + ' Files Changed (last 24h)' + c.reset);
|
|
896
|
+
try {
|
|
897
|
+
const diff = execSync('git diff --stat HEAD~10 2>/dev/null || git diff --stat 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
898
|
+
if (diff) {
|
|
899
|
+
const lines = diff.split('\n');
|
|
900
|
+
const summary = lines[lines.length - 1];
|
|
901
|
+
console.log(c.dim + ` ${summary.trim()}` + c.reset);
|
|
902
|
+
}
|
|
903
|
+
} catch {}
|
|
904
|
+
console.log();
|
|
905
|
+
|
|
906
|
+
// 4. Hook health
|
|
907
|
+
console.log(c.bold + ' Hook Health' + c.reset);
|
|
908
|
+
const hookDir = join(HOME, '.claude', 'hooks');
|
|
909
|
+
if (existsSync(hookDir)) {
|
|
910
|
+
const hooks = readdirSync(hookDir).filter(f => f.endsWith('.sh') || f.endsWith('.py'));
|
|
911
|
+
let execCount = 0, nonExec = 0;
|
|
912
|
+
for (const h of hooks) {
|
|
913
|
+
const st = statSync(join(hookDir, h));
|
|
914
|
+
if (st.mode & 0o111) execCount++; else nonExec++;
|
|
915
|
+
}
|
|
916
|
+
console.log(` ${c.green}${execCount}${c.reset} hooks executable${nonExec > 0 ? `, ${c.red}${nonExec}${c.reset} missing permissions` : ''}`);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// 5. Context usage estimate
|
|
920
|
+
console.log();
|
|
921
|
+
console.log(c.bold + ' Session Estimates' + c.reset);
|
|
922
|
+
// Check tool call log if exists
|
|
923
|
+
const toolLog = join(HOME, '.claude', 'tool-calls.log');
|
|
924
|
+
if (existsSync(toolLog)) {
|
|
925
|
+
const logContent = readFileSync(toolLog, 'utf-8');
|
|
926
|
+
const calls = logContent.split('\n').filter(l => l.trim());
|
|
927
|
+
const today = new Date().toISOString().split('T')[0];
|
|
928
|
+
const todayCalls = calls.filter(l => l.includes(today));
|
|
929
|
+
console.log(` Tool calls today: ${todayCalls.length}`);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Token budget state
|
|
933
|
+
const budgetFiles = existsSync('/tmp') ? readdirSync('/tmp').filter(f => f.startsWith('cc-token-budget-')) : [];
|
|
934
|
+
if (budgetFiles.length > 0) {
|
|
935
|
+
for (const bf of budgetFiles.slice(0, 3)) {
|
|
936
|
+
const tokens = parseInt(readFileSync(join('/tmp', bf), 'utf-8').trim()) || 0;
|
|
937
|
+
const costCents = Math.round(tokens * 75 / 10000);
|
|
938
|
+
console.log(` Estimated cost: ~$${(costCents / 100).toFixed(2)} (${tokens.toLocaleString()} tokens)`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
console.log();
|
|
943
|
+
console.log(c.dim + ' Tip: Use --stats for block history analytics' + c.reset);
|
|
944
|
+
console.log(c.dim + ' Tip: Use --dashboard for real-time monitoring' + c.reset);
|
|
945
|
+
console.log();
|
|
946
|
+
}
|
|
947
|
+
|
|
836
948
|
async function shield() {
|
|
837
949
|
const { execSync } = await import('child_process');
|
|
838
950
|
const { readdirSync } = await import('fs');
|
|
@@ -3042,6 +3154,7 @@ async function main() {
|
|
|
3042
3154
|
if (FULL) return fullSetup();
|
|
3043
3155
|
if (DOCTOR) return doctor();
|
|
3044
3156
|
if (WATCH) return watch();
|
|
3157
|
+
if (ANALYZE) return analyze();
|
|
3045
3158
|
if (SHIELD) return shield();
|
|
3046
3159
|
if (QUICKFIX) return quickfix();
|
|
3047
3160
|
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.2.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": {
|