cc-safe-setup 2.4.0 → 2.5.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 +8 -1
- package/SAFETY_CHECKLIST.md +53 -0
- package/audit-web/index.html +494 -38
- package/docs/index.html +603 -0
- package/examples/case-sensitive-guard.sh +145 -0
- package/index.mjs +5 -1
- package/package.json +2 -2
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# case-sensitive-guard.sh — Case-Insensitive Filesystem Safety Guard
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Detects case-insensitive filesystems (exFAT, NTFS, HFS+, APFS
|
|
7
|
+
# case-insensitive) and warns before mkdir/rm that would collide
|
|
8
|
+
# due to case folding.
|
|
9
|
+
#
|
|
10
|
+
# Real incident: GitHub #37875 — Claude created "Content" dir on
|
|
11
|
+
# exFAT drive where "content" already existed. Both resolved to
|
|
12
|
+
# the same path. Claude then ran rm -rf on "content", destroying
|
|
13
|
+
# all user data.
|
|
14
|
+
#
|
|
15
|
+
# TRIGGER: PreToolUse
|
|
16
|
+
# MATCHER: "Bash"
|
|
17
|
+
#
|
|
18
|
+
# WHAT IT BLOCKS (exit 2):
|
|
19
|
+
# - rm -rf on case-insensitive FS when a case-variant directory
|
|
20
|
+
# exists that resolves to the same inode
|
|
21
|
+
# - mkdir that would silently collide with existing dir on
|
|
22
|
+
# case-insensitive FS
|
|
23
|
+
#
|
|
24
|
+
# WHAT IT ALLOWS (exit 0):
|
|
25
|
+
# - All commands on case-sensitive filesystems
|
|
26
|
+
# - rm/mkdir where no case collision exists
|
|
27
|
+
# - Commands that don't involve mkdir or rm
|
|
28
|
+
#
|
|
29
|
+
# HOW IT WORKS:
|
|
30
|
+
# 1. Extract target path from mkdir/rm commands
|
|
31
|
+
# 2. Check if filesystem is case-insensitive (create temp file,
|
|
32
|
+
# check if uppercase variant exists)
|
|
33
|
+
# 3. If case-insensitive, check for case-variant collisions
|
|
34
|
+
# 4. Block if collision detected
|
|
35
|
+
# ================================================================
|
|
36
|
+
|
|
37
|
+
INPUT=$(cat)
|
|
38
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
39
|
+
|
|
40
|
+
if [[ -z "$COMMAND" ]]; then
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Only check mkdir and rm commands
|
|
45
|
+
if ! echo "$COMMAND" | grep -qE '^\s*(mkdir|rm)\s'; then
|
|
46
|
+
exit 0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Extract the target path
|
|
50
|
+
TARGET=""
|
|
51
|
+
if echo "$COMMAND" | grep -qE '^\s*mkdir'; then
|
|
52
|
+
TARGET=$(echo "$COMMAND" | grep -oP 'mkdir\s+(-p\s+)?\K\S+' | tail -1)
|
|
53
|
+
elif echo "$COMMAND" | grep -qE '^\s*rm\s'; then
|
|
54
|
+
TARGET=$(echo "$COMMAND" | grep -oP 'rm\s+(-[rf]+\s+)*\K\S+' | tail -1)
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
if [[ -z "$TARGET" ]]; then
|
|
58
|
+
exit 0
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Resolve the parent directory
|
|
62
|
+
PARENT_DIR=$(dirname "$TARGET" 2>/dev/null)
|
|
63
|
+
BASE_NAME=$(basename "$TARGET" 2>/dev/null)
|
|
64
|
+
|
|
65
|
+
if [[ -z "$PARENT_DIR" ]] || [[ ! -d "$PARENT_DIR" ]]; then
|
|
66
|
+
exit 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# --- Check if filesystem is case-insensitive ---
|
|
70
|
+
is_case_insensitive() {
|
|
71
|
+
local dir="$1"
|
|
72
|
+
local test_file="${dir}/.cc_case_test_$$"
|
|
73
|
+
local test_upper="${dir}/.CC_CASE_TEST_$$"
|
|
74
|
+
|
|
75
|
+
# Create lowercase test file
|
|
76
|
+
if ! touch "$test_file" 2>/dev/null; then
|
|
77
|
+
return 1 # Can't test, assume case-sensitive (safe default)
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# Check if uppercase variant resolves to the same file
|
|
81
|
+
if [[ -f "$test_upper" ]]; then
|
|
82
|
+
rm -f "$test_file" 2>/dev/null
|
|
83
|
+
return 0 # Case-insensitive
|
|
84
|
+
else
|
|
85
|
+
rm -f "$test_file" 2>/dev/null
|
|
86
|
+
return 1 # Case-sensitive
|
|
87
|
+
fi
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# --- Check for case-variant collisions ---
|
|
91
|
+
find_case_collision() {
|
|
92
|
+
local dir="$1"
|
|
93
|
+
local name="$2"
|
|
94
|
+
local name_lower
|
|
95
|
+
name_lower=$(echo "$name" | tr '[:upper:]' '[:lower:]')
|
|
96
|
+
|
|
97
|
+
# List directory entries and check for case variants
|
|
98
|
+
while IFS= read -r entry; do
|
|
99
|
+
local entry_lower
|
|
100
|
+
entry_lower=$(echo "$entry" | tr '[:upper:]' '[:lower:]')
|
|
101
|
+
if [[ "$entry_lower" == "$name_lower" ]] && [[ "$entry" != "$name" ]]; then
|
|
102
|
+
echo "$entry"
|
|
103
|
+
return 0
|
|
104
|
+
fi
|
|
105
|
+
done < <(ls -1 "$dir" 2>/dev/null)
|
|
106
|
+
|
|
107
|
+
return 1
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Only proceed if filesystem is case-insensitive
|
|
111
|
+
if ! is_case_insensitive "$PARENT_DIR"; then
|
|
112
|
+
exit 0
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# Check for collision
|
|
116
|
+
COLLISION=$(find_case_collision "$PARENT_DIR" "$BASE_NAME")
|
|
117
|
+
|
|
118
|
+
if [[ -n "$COLLISION" ]]; then
|
|
119
|
+
if echo "$COMMAND" | grep -qE '^\s*rm\s'; then
|
|
120
|
+
echo "BLOCKED: Case-insensitive filesystem collision detected." >&2
|
|
121
|
+
echo "" >&2
|
|
122
|
+
echo "Command: $COMMAND" >&2
|
|
123
|
+
echo "" >&2
|
|
124
|
+
echo "Target: $TARGET" >&2
|
|
125
|
+
echo "Collides with: $PARENT_DIR/$COLLISION" >&2
|
|
126
|
+
echo "" >&2
|
|
127
|
+
echo "This filesystem is case-insensitive (exFAT, NTFS, HFS+, etc.)." >&2
|
|
128
|
+
echo "'$BASE_NAME' and '$COLLISION' resolve to the SAME path." >&2
|
|
129
|
+
echo "rm -rf would destroy the data you think you're keeping." >&2
|
|
130
|
+
echo "" >&2
|
|
131
|
+
echo "Verify with: ls -la \"$PARENT_DIR\" | grep -i \"$BASE_NAME\"" >&2
|
|
132
|
+
exit 2
|
|
133
|
+
elif echo "$COMMAND" | grep -qE '^\s*mkdir'; then
|
|
134
|
+
echo "WARNING: Case-insensitive filesystem — directory already exists." >&2
|
|
135
|
+
echo "" >&2
|
|
136
|
+
echo "Command: $COMMAND" >&2
|
|
137
|
+
echo "Existing: $PARENT_DIR/$COLLISION" >&2
|
|
138
|
+
echo "" >&2
|
|
139
|
+
echo "On this filesystem, '$BASE_NAME' and '$COLLISION' are the same path." >&2
|
|
140
|
+
echo "mkdir will either fail or silently use the existing directory." >&2
|
|
141
|
+
exit 2
|
|
142
|
+
fi
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
exit 0
|
package/index.mjs
CHANGED
|
@@ -85,7 +85,11 @@ if (HELP) {
|
|
|
85
85
|
npx cc-safe-setup --uninstall Remove all installed hooks
|
|
86
86
|
npx cc-safe-setup --examples List 25 example hooks (5 categories)
|
|
87
87
|
npx cc-safe-setup --install-example <name> Install a specific example
|
|
88
|
-
npx cc-safe-setup --
|
|
88
|
+
npx cc-safe-setup --full Complete setup: hooks + scan + audit + badge
|
|
89
|
+
npx cc-safe-setup --audit Safety score (0-100) with fixes
|
|
90
|
+
npx cc-safe-setup --audit --fix Auto-fix missing protections
|
|
91
|
+
npx cc-safe-setup --scan Detect tech stack, recommend hooks
|
|
92
|
+
npx cc-safe-setup --learn Learn from your block history
|
|
89
93
|
npx cc-safe-setup --help Show this help
|
|
90
94
|
|
|
91
95
|
Hooks installed:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "One command to make Claude Code safe for autonomous operation. 8 built-in hooks +
|
|
3
|
+
"version": "2.5.0",
|
|
4
|
+
"description": "One command to make Claude Code safe for autonomous operation. 8 built-in hooks + 26 installable examples. Destructive blocker, branch guard, database wipe protection, case-insensitive FS guard, and more.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cc-safe-setup": "index.mjs"
|