cc-safe-setup 7.2.0 → 7.4.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/examples/disk-space-guard.sh +28 -0
- package/examples/memory-write-guard.sh +40 -0
- package/examples/output-length-guard.sh +14 -0
- package/examples/overwrite-guard.sh +32 -0
- package/examples/prompt-injection-guard.sh +51 -0
- package/examples/test-deletion-guard.sh +48 -0
- package/examples/uncommitted-work-guard.sh +45 -0
- package/examples/verify-before-done.sh +43 -0
- package/index.mjs +197 -0
- package/package.json +1 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# disk-space-guard.sh — Warn when disk space is running low
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Long Claude Code sessions can generate large files, logs, and
|
|
7
|
+
# build artifacts. This hook warns before writes when disk space
|
|
8
|
+
# is below a threshold.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PreToolUse MATCHER: "Write|Bash"
|
|
11
|
+
#
|
|
12
|
+
# CONFIG:
|
|
13
|
+
# CC_DISK_WARN_PCT=90 (warn at this percentage used)
|
|
14
|
+
# ================================================================
|
|
15
|
+
|
|
16
|
+
WARN_PCT="${CC_DISK_WARN_PCT:-90}"
|
|
17
|
+
|
|
18
|
+
# Check disk usage (percentage used on the working directory's partition)
|
|
19
|
+
USAGE=$(df --output=pcent . 2>/dev/null | tail -1 | tr -d ' %')
|
|
20
|
+
[ -z "$USAGE" ] && exit 0
|
|
21
|
+
|
|
22
|
+
if [ "$USAGE" -ge "$WARN_PCT" ]; then
|
|
23
|
+
AVAIL=$(df -h --output=avail . 2>/dev/null | tail -1 | tr -d ' ')
|
|
24
|
+
echo "WARNING: Disk usage is ${USAGE}% (${AVAIL} available)." >&2
|
|
25
|
+
echo "Consider cleaning up build artifacts, logs, or /tmp files." >&2
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
exit 0
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# memory-write-guard.sh — Log writes to ~/.claude/ directory
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude auto-writes to ~/.claude/projects/*/memory/ without
|
|
7
|
+
# user visibility. This hook logs all writes to ~/.claude/ paths
|
|
8
|
+
# so users know what's being stored.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PreToolUse MATCHER: "Write|Edit"
|
|
11
|
+
#
|
|
12
|
+
# Born from: https://github.com/anthropics/claude-code/issues/38040
|
|
13
|
+
# "No way to enforce approval on all file modifications"
|
|
14
|
+
# ================================================================
|
|
15
|
+
|
|
16
|
+
INPUT=$(cat)
|
|
17
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
18
|
+
[ -z "$FILE" ] && exit 0
|
|
19
|
+
|
|
20
|
+
# Check if targeting ~/.claude/
|
|
21
|
+
case "$FILE" in
|
|
22
|
+
*/.claude/*|~/.claude/*)
|
|
23
|
+
# Log the write
|
|
24
|
+
LOG="$HOME/.claude/memory-writes.log"
|
|
25
|
+
echo "[$(date -Iseconds)] Write to: $FILE" >> "$LOG" 2>/dev/null
|
|
26
|
+
|
|
27
|
+
# Only warn (don't block) — memory writes are usually intentional
|
|
28
|
+
echo "NOTE: Writing to Claude config directory: $FILE" >&2
|
|
29
|
+
|
|
30
|
+
# Block writes to settings.json unless explicitly allowed
|
|
31
|
+
case "$FILE" in
|
|
32
|
+
*/settings.json|*/settings.local.json)
|
|
33
|
+
echo "WARNING: Modifying Claude Code settings file." >&2
|
|
34
|
+
echo "Verify this change is intentional." >&2
|
|
35
|
+
;;
|
|
36
|
+
esac
|
|
37
|
+
;;
|
|
38
|
+
esac
|
|
39
|
+
|
|
40
|
+
exit 0
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# output-length-guard.sh — Warn when tool output is very large
|
|
3
|
+
# TRIGGER: PostToolUse MATCHER: ""
|
|
4
|
+
# Checks tool output size and warns when it's consuming too much context
|
|
5
|
+
INPUT=$(cat)
|
|
6
|
+
OUTPUT=$(echo "$INPUT" | jq -r '.tool_result // empty' 2>/dev/null)
|
|
7
|
+
if [ -n "$OUTPUT" ]; then
|
|
8
|
+
LEN=${#OUTPUT}
|
|
9
|
+
if [ "$LEN" -gt 50000 ]; then
|
|
10
|
+
echo "WARNING: Tool output is ${LEN} chars. Large outputs consume context rapidly." >&2
|
|
11
|
+
echo "Consider using head/tail/grep to limit output, or redirect to a file." >&2
|
|
12
|
+
fi
|
|
13
|
+
fi
|
|
14
|
+
exit 0
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# overwrite-guard.sh — Warn before overwriting existing files
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude's Write tool can silently overwrite files without
|
|
7
|
+
# confirmation. This hook warns when a Write targets a file
|
|
8
|
+
# that already exists, giving visibility into potential data loss.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PreToolUse MATCHER: "Write"
|
|
11
|
+
#
|
|
12
|
+
# Born from: https://github.com/anthropics/claude-code/issues/37595
|
|
13
|
+
# "/export overwrites existing files without warning"
|
|
14
|
+
# ================================================================
|
|
15
|
+
|
|
16
|
+
INPUT=$(cat)
|
|
17
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
18
|
+
[ -z "$FILE" ] && exit 0
|
|
19
|
+
|
|
20
|
+
# Expand ~ to home directory
|
|
21
|
+
FILE="${FILE/#\~/$HOME}"
|
|
22
|
+
|
|
23
|
+
if [ -f "$FILE" ]; then
|
|
24
|
+
SIZE=$(wc -c < "$FILE" 2>/dev/null || echo 0)
|
|
25
|
+
if [ "$SIZE" -gt 0 ]; then
|
|
26
|
+
LINES=$(wc -l < "$FILE" 2>/dev/null || echo 0)
|
|
27
|
+
echo "WARNING: Overwriting existing file: $FILE ($LINES lines, $SIZE bytes)" >&2
|
|
28
|
+
echo "Use Edit tool instead to make targeted changes." >&2
|
|
29
|
+
fi
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
exit 0
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# prompt-injection-guard.sh — Detect prompt injection in tool output
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# When Claude reads files or fetches web content, malicious
|
|
7
|
+
# instructions can be injected. This hook warns when tool output
|
|
8
|
+
# contains common prompt injection patterns.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PostToolUse MATCHER: ""
|
|
11
|
+
#
|
|
12
|
+
# Born from: https://github.com/anthropics/claude-code/issues/38046
|
|
13
|
+
# "Prompt Injection in /insights output"
|
|
14
|
+
# ================================================================
|
|
15
|
+
|
|
16
|
+
INPUT=$(cat)
|
|
17
|
+
OUTPUT=$(echo "$INPUT" | jq -r '.tool_result // empty' 2>/dev/null)
|
|
18
|
+
[ -z "$OUTPUT" ] && exit 0
|
|
19
|
+
|
|
20
|
+
# Check for common prompt injection patterns
|
|
21
|
+
SUSPICIOUS=0
|
|
22
|
+
|
|
23
|
+
# "Ignore previous instructions" pattern
|
|
24
|
+
if echo "$OUTPUT" | grep -qiE 'ignore\s+(all\s+)?previous\s+instructions'; then
|
|
25
|
+
echo "WARNING: Possible prompt injection detected: 'ignore previous instructions'" >&2
|
|
26
|
+
SUSPICIOUS=1
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# "You are now" role reassignment
|
|
30
|
+
if echo "$OUTPUT" | grep -qiE 'you\s+are\s+now\s+(a|an)\s+'; then
|
|
31
|
+
echo "WARNING: Possible prompt injection detected: role reassignment" >&2
|
|
32
|
+
SUSPICIOUS=1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# "System prompt" manipulation
|
|
36
|
+
if echo "$OUTPUT" | grep -qiE '(new|updated|override)\s+system\s+prompt'; then
|
|
37
|
+
echo "WARNING: Possible prompt injection detected: system prompt override" >&2
|
|
38
|
+
SUSPICIOUS=1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Hidden instructions in HTML comments or zero-width chars
|
|
42
|
+
if echo "$OUTPUT" | grep -qP '<!--.*(?:execute|run|delete|remove).*-->'; then
|
|
43
|
+
echo "WARNING: Possible prompt injection in HTML comment" >&2
|
|
44
|
+
SUSPICIOUS=1
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
if [ "$SUSPICIOUS" -eq 1 ]; then
|
|
48
|
+
echo "Review the output carefully before acting on it." >&2
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
exit 0
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# test-deletion-guard.sh — Block deletion of test assertions
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude sometimes deletes or comments out failing tests instead
|
|
7
|
+
# of fixing the underlying code. This hook detects when an Edit
|
|
8
|
+
# to a test file removes test assertions.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PreToolUse MATCHER: "Edit"
|
|
11
|
+
#
|
|
12
|
+
# Born from: https://github.com/anthropics/claude-code/issues/38050
|
|
13
|
+
# "Claude skips/deletes tests instead of fixing them"
|
|
14
|
+
# ================================================================
|
|
15
|
+
|
|
16
|
+
INPUT=$(cat)
|
|
17
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
18
|
+
[ -z "$FILE" ] && exit 0
|
|
19
|
+
|
|
20
|
+
# Only check test files
|
|
21
|
+
case "$FILE" in
|
|
22
|
+
*test*|*spec*|*__tests__*|*_test.go|*_test.py|*Test.java|*Test.kt)
|
|
23
|
+
;;
|
|
24
|
+
*)
|
|
25
|
+
exit 0
|
|
26
|
+
;;
|
|
27
|
+
esac
|
|
28
|
+
|
|
29
|
+
OLD=$(echo "$INPUT" | jq -r '.tool_input.old_string // empty' 2>/dev/null)
|
|
30
|
+
NEW=$(echo "$INPUT" | jq -r '.tool_input.new_string // empty' 2>/dev/null)
|
|
31
|
+
[ -z "$OLD" ] && exit 0
|
|
32
|
+
|
|
33
|
+
# Count test assertions in old vs new
|
|
34
|
+
count_tests() {
|
|
35
|
+
echo "$1" | grep -cE '(it\(|test\(|describe\(|def test_|#\[test\]|@Test|assert|expect\(|should\b)' 2>/dev/null || echo 0
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
OLD_COUNT=$(count_tests "$OLD")
|
|
39
|
+
NEW_COUNT=$(count_tests "$NEW")
|
|
40
|
+
|
|
41
|
+
if [ "$OLD_COUNT" -gt 0 ] && [ "$NEW_COUNT" -lt "$OLD_COUNT" ]; then
|
|
42
|
+
REMOVED=$((OLD_COUNT - NEW_COUNT))
|
|
43
|
+
echo "WARNING: This edit removes $REMOVED test assertion(s) from $FILE." >&2
|
|
44
|
+
echo "If tests are failing, fix the code instead of deleting tests." >&2
|
|
45
|
+
echo "Old assertions: $OLD_COUNT → New assertions: $NEW_COUNT" >&2
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
exit 0
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# uncommitted-work-guard.sh — Block destructive git when dirty
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude sometimes runs git checkout --, git reset --hard, or
|
|
7
|
+
# git stash drop when there are uncommitted changes, destroying
|
|
8
|
+
# hours of work. This hook checks git status before allowing
|
|
9
|
+
# destructive git commands.
|
|
10
|
+
#
|
|
11
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
12
|
+
#
|
|
13
|
+
# Born from: https://github.com/anthropics/claude-code/issues/37888
|
|
14
|
+
# "Claude runs forbidden destructive git commands, destroys work twice"
|
|
15
|
+
# ================================================================
|
|
16
|
+
|
|
17
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
18
|
+
[ -z "$COMMAND" ] && exit 0
|
|
19
|
+
|
|
20
|
+
# Only check destructive git commands
|
|
21
|
+
DESTRUCTIVE=0
|
|
22
|
+
echo "$COMMAND" | grep -qE '\bgit\s+checkout\s+--\s' && DESTRUCTIVE=1
|
|
23
|
+
echo "$COMMAND" | grep -qE '\bgit\s+checkout\s+\.\s*$' && DESTRUCTIVE=1
|
|
24
|
+
echo "$COMMAND" | grep -qE '\bgit\s+restore\s+--staged\s+\.' && DESTRUCTIVE=1
|
|
25
|
+
echo "$COMMAND" | grep -qE '\bgit\s+restore\s+\.\s*$' && DESTRUCTIVE=1
|
|
26
|
+
echo "$COMMAND" | grep -qE '\bgit\s+reset\s+--hard' && DESTRUCTIVE=1
|
|
27
|
+
echo "$COMMAND" | grep -qE '\bgit\s+clean\s+-[a-zA-Z]*f' && DESTRUCTIVE=1
|
|
28
|
+
echo "$COMMAND" | grep -qE '\bgit\s+stash\s+drop' && DESTRUCTIVE=1
|
|
29
|
+
|
|
30
|
+
[ "$DESTRUCTIVE" -eq 0 ] && exit 0
|
|
31
|
+
|
|
32
|
+
# Check for uncommitted changes
|
|
33
|
+
DIRTY=$(git status --porcelain 2>/dev/null | head -20)
|
|
34
|
+
if [ -n "$DIRTY" ]; then
|
|
35
|
+
COUNT=$(echo "$DIRTY" | wc -l)
|
|
36
|
+
echo "BLOCKED: Destructive git command with $COUNT uncommitted change(s)." >&2
|
|
37
|
+
echo "Changes that would be lost:" >&2
|
|
38
|
+
echo "$DIRTY" | head -10 | sed 's/^/ /' >&2
|
|
39
|
+
[ "$COUNT" -gt 10 ] && echo " ... and $((COUNT-10)) more" >&2
|
|
40
|
+
echo "" >&2
|
|
41
|
+
echo "Commit or stash your changes first, then retry." >&2
|
|
42
|
+
exit 2
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
exit 0
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# verify-before-done.sh — Warn when committing without running tests
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude Code often declares fixes "done" and commits without
|
|
7
|
+
# verifying the fix actually works. This hook warns when a commit
|
|
8
|
+
# is made in a project that has tests, but no test command was
|
|
9
|
+
# run recently in the session.
|
|
10
|
+
#
|
|
11
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
12
|
+
#
|
|
13
|
+
# Born from: https://github.com/anthropics/claude-code/issues/37818
|
|
14
|
+
# "Claude repeatedly declares fixes done without verification"
|
|
15
|
+
# ================================================================
|
|
16
|
+
|
|
17
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
18
|
+
[ -z "$COMMAND" ] && exit 0
|
|
19
|
+
|
|
20
|
+
# Only check on git commit
|
|
21
|
+
echo "$COMMAND" | grep -qE '^\s*git\s+commit' || exit 0
|
|
22
|
+
|
|
23
|
+
# Track test execution via state file
|
|
24
|
+
STATE="/tmp/cc-tests-ran-$(pwd | md5sum | cut -c1-8)"
|
|
25
|
+
|
|
26
|
+
# Check if tests were run in this session
|
|
27
|
+
if [ ! -f "$STATE" ]; then
|
|
28
|
+
# Detect if project has tests
|
|
29
|
+
HAS_TESTS=0
|
|
30
|
+
[ -f "package.json" ] && grep -q '"test"' package.json 2>/dev/null && HAS_TESTS=1
|
|
31
|
+
[ -f "pytest.ini" ] || [ -f "setup.cfg" ] || [ -f "pyproject.toml" ] && HAS_TESTS=1
|
|
32
|
+
[ -f "Cargo.toml" ] && HAS_TESTS=1
|
|
33
|
+
[ -f "go.mod" ] && HAS_TESTS=1
|
|
34
|
+
[ -f "Makefile" ] && grep -q 'test:' Makefile 2>/dev/null && HAS_TESTS=1
|
|
35
|
+
|
|
36
|
+
if [ "$HAS_TESTS" -eq 1 ]; then
|
|
37
|
+
echo "WARNING: Committing without running tests first." >&2
|
|
38
|
+
echo "Run your test suite before committing to verify changes work." >&2
|
|
39
|
+
echo "To suppress: touch $STATE" >&2
|
|
40
|
+
fi
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
exit 0
|
package/index.mjs
CHANGED
|
@@ -89,6 +89,7 @@ const ISSUES = process.argv.includes('--issues');
|
|
|
89
89
|
const MIGRATE = process.argv.includes('--migrate');
|
|
90
90
|
const GENERATE_CI = process.argv.includes('--generate-ci');
|
|
91
91
|
const REPORT = process.argv.includes('--report');
|
|
92
|
+
const QUICKFIX = process.argv.includes('--quickfix');
|
|
92
93
|
const COMPARE_IDX = process.argv.findIndex(a => a === '--compare');
|
|
93
94
|
const COMPARE = COMPARE_IDX !== -1 ? { a: process.argv[COMPARE_IDX + 1], b: process.argv[COMPARE_IDX + 2] } : null;
|
|
94
95
|
const CREATE_IDX = process.argv.findIndex(a => a === '--create');
|
|
@@ -124,6 +125,7 @@ if (HELP) {
|
|
|
124
125
|
npx cc-safe-setup --doctor Diagnose why hooks aren't working
|
|
125
126
|
npx cc-safe-setup --watch Live dashboard of blocked commands
|
|
126
127
|
npx cc-safe-setup --create "<desc>" Generate a custom hook from description
|
|
128
|
+
npx cc-safe-setup --quickfix Auto-detect and fix common Claude Code problems
|
|
127
129
|
npx cc-safe-setup --stats Block statistics and patterns report
|
|
128
130
|
npx cc-safe-setup --export Export hooks config for team sharing
|
|
129
131
|
npx cc-safe-setup --import <file> Import hooks from exported config
|
|
@@ -829,6 +831,200 @@ async function fullSetup() {
|
|
|
829
831
|
console.log();
|
|
830
832
|
}
|
|
831
833
|
|
|
834
|
+
async function quickfix() {
|
|
835
|
+
const { execSync } = await import('child_process');
|
|
836
|
+
console.log();
|
|
837
|
+
console.log(c.bold + ' cc-safe-setup --quickfix' + c.reset);
|
|
838
|
+
console.log(c.dim + ' Auto-detect and fix common Claude Code problems' + c.reset);
|
|
839
|
+
console.log();
|
|
840
|
+
|
|
841
|
+
let fixed = 0, warnings = 0, ok = 0;
|
|
842
|
+
|
|
843
|
+
// Check 1: jq installed
|
|
844
|
+
try {
|
|
845
|
+
execSync('which jq', { stdio: 'pipe' });
|
|
846
|
+
console.log(c.green + ' ✓' + c.reset + ' jq is installed');
|
|
847
|
+
ok++;
|
|
848
|
+
} catch {
|
|
849
|
+
console.log(c.red + ' ✗' + c.reset + ' jq is not installed — hooks cannot parse JSON');
|
|
850
|
+
console.log(c.dim + ' Fix: brew install jq (macOS) | sudo apt install jq (Linux)' + c.reset);
|
|
851
|
+
warnings++;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Check 2: ~/.claude directory exists
|
|
855
|
+
const claudeDir = join(HOME, '.claude');
|
|
856
|
+
if (existsSync(claudeDir)) {
|
|
857
|
+
console.log(c.green + ' ✓' + c.reset + ' ~/.claude directory exists');
|
|
858
|
+
ok++;
|
|
859
|
+
} else {
|
|
860
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
861
|
+
console.log(c.yellow + ' ⚡' + c.reset + ' Created ~/.claude directory');
|
|
862
|
+
fixed++;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Check 3: hooks directory exists
|
|
866
|
+
if (existsSync(HOOKS_DIR)) {
|
|
867
|
+
console.log(c.green + ' ✓' + c.reset + ' ~/.claude/hooks directory exists');
|
|
868
|
+
ok++;
|
|
869
|
+
} else {
|
|
870
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
871
|
+
console.log(c.yellow + ' ⚡' + c.reset + ' Created ~/.claude/hooks directory');
|
|
872
|
+
fixed++;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Check 4: settings.json exists and is valid JSON
|
|
876
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
877
|
+
try {
|
|
878
|
+
JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
879
|
+
console.log(c.green + ' ✓' + c.reset + ' settings.json is valid JSON');
|
|
880
|
+
ok++;
|
|
881
|
+
} catch (e) {
|
|
882
|
+
console.log(c.red + ' ✗' + c.reset + ' settings.json has invalid JSON: ' + e.message);
|
|
883
|
+
console.log(c.dim + ' This is the #1 cause of hooks not working.' + c.reset);
|
|
884
|
+
console.log(c.dim + ' Common fix: remove trailing commas, check for comments (JSONC not supported in all contexts)' + c.reset);
|
|
885
|
+
warnings++;
|
|
886
|
+
}
|
|
887
|
+
} else {
|
|
888
|
+
writeFileSync(SETTINGS_PATH, '{}');
|
|
889
|
+
console.log(c.yellow + ' ⚡' + c.reset + ' Created empty settings.json');
|
|
890
|
+
fixed++;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Check 5: hooks have executable permission
|
|
894
|
+
if (existsSync(HOOKS_DIR)) {
|
|
895
|
+
const { readdirSync, statSync } = await import('fs');
|
|
896
|
+
const hooks = readdirSync(HOOKS_DIR).filter(f => f.endsWith('.sh'));
|
|
897
|
+
let nonExec = 0;
|
|
898
|
+
for (const h of hooks) {
|
|
899
|
+
const p = join(HOOKS_DIR, h);
|
|
900
|
+
const st = statSync(p);
|
|
901
|
+
if (!(st.mode & 0o111)) {
|
|
902
|
+
chmodSync(p, 0o755);
|
|
903
|
+
nonExec++;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (nonExec > 0) {
|
|
907
|
+
console.log(c.yellow + ' ⚡' + c.reset + ` Fixed ${nonExec} hook(s) missing executable permission`);
|
|
908
|
+
fixed += nonExec;
|
|
909
|
+
} else if (hooks.length > 0) {
|
|
910
|
+
console.log(c.green + ' ✓' + c.reset + ` All ${hooks.length} hooks have executable permission`);
|
|
911
|
+
ok++;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Check 6: hooks have correct shebang
|
|
916
|
+
if (existsSync(HOOKS_DIR)) {
|
|
917
|
+
const { readdirSync } = await import('fs');
|
|
918
|
+
const hooks = readdirSync(HOOKS_DIR).filter(f => f.endsWith('.sh'));
|
|
919
|
+
let badShebang = 0;
|
|
920
|
+
for (const h of hooks) {
|
|
921
|
+
const content = readFileSync(join(HOOKS_DIR, h), 'utf-8');
|
|
922
|
+
const firstLine = content.split('\n')[0];
|
|
923
|
+
if (!firstLine.startsWith('#!')) {
|
|
924
|
+
badShebang++;
|
|
925
|
+
console.log(c.red + ' ✗' + c.reset + ` ${h} missing shebang (#!/bin/bash)`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (badShebang === 0 && hooks.length > 0) {
|
|
929
|
+
console.log(c.green + ' ✓' + c.reset + ' All hooks have valid shebang lines');
|
|
930
|
+
ok++;
|
|
931
|
+
}
|
|
932
|
+
warnings += badShebang;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Check 7: settings.json hooks reference existing files
|
|
936
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
937
|
+
try {
|
|
938
|
+
const settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
939
|
+
let broken = 0;
|
|
940
|
+
for (const [trigger, groups] of Object.entries(settings.hooks || {})) {
|
|
941
|
+
for (const group of groups) {
|
|
942
|
+
for (const hook of (group.hooks || [])) {
|
|
943
|
+
const cmd = hook.command || '';
|
|
944
|
+
// Extract script path from command
|
|
945
|
+
const match = cmd.match(/bash\s+"?([^"\s]+\.sh)/);
|
|
946
|
+
if (match && !existsSync(match[1])) {
|
|
947
|
+
console.log(c.red + ' ✗' + c.reset + ` Hook references missing file: ${match[1]}`);
|
|
948
|
+
broken++;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (broken === 0) {
|
|
954
|
+
console.log(c.green + ' ✓' + c.reset + ' All hook file references are valid');
|
|
955
|
+
ok++;
|
|
956
|
+
}
|
|
957
|
+
warnings += broken;
|
|
958
|
+
} catch {}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Check 8: No .env in git staging
|
|
962
|
+
try {
|
|
963
|
+
const staged = execSync('git diff --cached --name-only 2>/dev/null', { encoding: 'utf-8' });
|
|
964
|
+
if (/\.env/i.test(staged)) {
|
|
965
|
+
console.log(c.red + ' ✗' + c.reset + ' .env file is staged in git! Run: git reset HEAD .env');
|
|
966
|
+
warnings++;
|
|
967
|
+
} else {
|
|
968
|
+
console.log(c.green + ' ✓' + c.reset + ' No secret files in git staging area');
|
|
969
|
+
ok++;
|
|
970
|
+
}
|
|
971
|
+
} catch {
|
|
972
|
+
console.log(c.dim + ' · Not in a git repository (skipping git checks)' + c.reset);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Check 9: CLAUDE.md exists in project
|
|
976
|
+
if (existsSync('CLAUDE.md')) {
|
|
977
|
+
console.log(c.green + ' ✓' + c.reset + ' CLAUDE.md found in project');
|
|
978
|
+
ok++;
|
|
979
|
+
} else {
|
|
980
|
+
console.log(c.yellow + ' △' + c.reset + ' No CLAUDE.md — consider creating one for project-specific rules');
|
|
981
|
+
warnings++;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Check 10: Safety hooks installed
|
|
985
|
+
let safetyHooks = 0;
|
|
986
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
987
|
+
try {
|
|
988
|
+
const s = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
989
|
+
const allHookCmds = [];
|
|
990
|
+
for (const groups of Object.values(s.hooks || {})) {
|
|
991
|
+
for (const g of groups) {
|
|
992
|
+
for (const h of (g.hooks || [])) allHookCmds.push(h.command || '');
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
const critical = ['destructive-guard', 'branch-guard', 'secret-guard'];
|
|
996
|
+
for (const name of critical) {
|
|
997
|
+
if (allHookCmds.some(c => c.includes(name))) {
|
|
998
|
+
safetyHooks++;
|
|
999
|
+
} else {
|
|
1000
|
+
console.log(c.yellow + ' △' + c.reset + ` Missing critical hook: ${name}`);
|
|
1001
|
+
console.log(c.dim + ' Fix: npx cc-safe-setup' + c.reset);
|
|
1002
|
+
warnings++;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
if (safetyHooks === 3) {
|
|
1006
|
+
console.log(c.green + ' ✓' + c.reset + ' All 3 critical safety hooks installed');
|
|
1007
|
+
ok++;
|
|
1008
|
+
}
|
|
1009
|
+
} catch {}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
console.log();
|
|
1013
|
+
console.log(c.bold + ' Summary' + c.reset);
|
|
1014
|
+
console.log(c.green + ` ${ok} OK` + c.reset + c.yellow + ` · ${fixed} fixed` + c.reset + c.red + ` · ${warnings} warnings` + c.reset);
|
|
1015
|
+
|
|
1016
|
+
if (fixed > 0) {
|
|
1017
|
+
console.log();
|
|
1018
|
+
console.log(c.green + ` ⚡ Auto-fixed ${fixed} issue(s)` + c.reset);
|
|
1019
|
+
}
|
|
1020
|
+
if (warnings > 0) {
|
|
1021
|
+
console.log();
|
|
1022
|
+
console.log(c.yellow + ' Run npx cc-safe-setup to install missing safety hooks' + c.reset);
|
|
1023
|
+
console.log(c.yellow + ' Run npx cc-safe-setup --doctor for detailed diagnosis' + c.reset);
|
|
1024
|
+
}
|
|
1025
|
+
console.log();
|
|
1026
|
+
}
|
|
1027
|
+
|
|
832
1028
|
async function report() {
|
|
833
1029
|
// Generate markdown safety report
|
|
834
1030
|
let hookCount = 0;
|
|
@@ -2640,6 +2836,7 @@ async function main() {
|
|
|
2640
2836
|
if (FULL) return fullSetup();
|
|
2641
2837
|
if (DOCTOR) return doctor();
|
|
2642
2838
|
if (WATCH) return watch();
|
|
2839
|
+
if (QUICKFIX) return quickfix();
|
|
2643
2840
|
if (REPORT) return report();
|
|
2644
2841
|
if (GENERATE_CI) return generateCI();
|
|
2645
2842
|
if (MIGRATE) return migrate();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.4.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": {
|