cc-safe-setup 29.6.10 → 29.6.12
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 -1
- package/examples/file-edit-backup.sh +38 -0
- package/examples/git-checkout-uncommitted-guard.sh +60 -0
- package/examples/git-message-length-check.sh +38 -0
- package/examples/plan-mode-edit-guard.sh +63 -0
- package/examples/unicode-corruption-check.sh +40 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -117,7 +117,7 @@ Install any of these: `npx cc-safe-setup --install-example <name>`
|
|
|
117
117
|
| `--scan [--apply]` | Tech stack detection |
|
|
118
118
|
| `--export / --import` | Team config sharing |
|
|
119
119
|
| `--verify` | Test each hook |
|
|
120
|
-
| `--install-example <name>` | Install from
|
|
120
|
+
| `--install-example <name>` | Install from 421 examples |
|
|
121
121
|
| `--examples [filter]` | Browse examples by keyword |
|
|
122
122
|
| `--full` | All-in-one setup |
|
|
123
123
|
| `--status` | Check installed hooks |
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# file-edit-backup.sh — Auto-backup files before Edit/Write overwrites them
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude Code overwrites important files and the changes are hard
|
|
5
|
+
# to reverse. This creates a timestamped backup before each edit,
|
|
6
|
+
# so you can always recover the previous version.
|
|
7
|
+
#
|
|
8
|
+
# Real incidents:
|
|
9
|
+
# #37478 — .bashrc overwritten without permission
|
|
10
|
+
# #32938 — 11h of inference output deleted
|
|
11
|
+
# #36339 — C:\Users directory wiped (NTFS junction traversal)
|
|
12
|
+
#
|
|
13
|
+
# Backups go to ~/.claude/file-backups/ with timestamps.
|
|
14
|
+
# Old backups (>7 days) are auto-cleaned to prevent disk bloat.
|
|
15
|
+
#
|
|
16
|
+
# TRIGGER: PreToolUse MATCHER: "Edit|Write"
|
|
17
|
+
|
|
18
|
+
INPUT=$(cat)
|
|
19
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
20
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
21
|
+
|
|
22
|
+
[ -z "$FILE" ] && exit 0
|
|
23
|
+
[ ! -f "$FILE" ] && exit 0 # New file, nothing to backup
|
|
24
|
+
|
|
25
|
+
BACKUP_DIR="$HOME/.claude/file-backups"
|
|
26
|
+
mkdir -p "$BACKUP_DIR"
|
|
27
|
+
|
|
28
|
+
# Create backup with timestamp
|
|
29
|
+
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
|
30
|
+
SAFE_NAME=$(echo "$FILE" | tr '/' '_' | sed 's/^_//')
|
|
31
|
+
BACKUP_PATH="${BACKUP_DIR}/${SAFE_NAME}.${TIMESTAMP}"
|
|
32
|
+
|
|
33
|
+
cp "$FILE" "$BACKUP_PATH" 2>/dev/null
|
|
34
|
+
|
|
35
|
+
# Clean old backups (>7 days)
|
|
36
|
+
find "$BACKUP_DIR" -type f -mtime +7 -delete 2>/dev/null
|
|
37
|
+
|
|
38
|
+
exit 0
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# git-checkout-uncommitted-guard.sh — Block branch switching with uncommitted changes
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude Code switching branches while uncommitted changes exist,
|
|
5
|
+
# causing silent data loss when the target branch overwrites modified files.
|
|
6
|
+
# Real incident: #39394 — hours of work lost when Claude decided to
|
|
7
|
+
# organize commits on a different branch.
|
|
8
|
+
#
|
|
9
|
+
# Why this matters: git checkout <branch> silently overwrites modified files
|
|
10
|
+
# if the target branch has different versions. Unlike "git checkout -- .",
|
|
11
|
+
# the user doesn't intend to discard changes — they just wanted to switch
|
|
12
|
+
# branches. The data loss is a side effect, not the goal.
|
|
13
|
+
#
|
|
14
|
+
# Detects:
|
|
15
|
+
# git checkout <branch> (when uncommitted changes exist)
|
|
16
|
+
# git switch <branch> (modern equivalent)
|
|
17
|
+
#
|
|
18
|
+
# Does NOT block:
|
|
19
|
+
# git checkout -b <branch> (creating new branch — preserves changes)
|
|
20
|
+
# git switch -c <branch> (creating new branch — preserves changes)
|
|
21
|
+
# git checkout -- <files> (handled by uncommitted-discard-guard)
|
|
22
|
+
# Any checkout when working tree is clean
|
|
23
|
+
#
|
|
24
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
25
|
+
|
|
26
|
+
INPUT=$(cat)
|
|
27
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
28
|
+
|
|
29
|
+
[ -z "$COMMAND" ] && exit 0
|
|
30
|
+
|
|
31
|
+
# Only check git checkout/switch commands (not -b/-c which create branches)
|
|
32
|
+
IS_CHECKOUT=false
|
|
33
|
+
if echo "$COMMAND" | grep -qE 'git\s+checkout\s+[^-]' && ! echo "$COMMAND" | grep -qE 'git\s+checkout\s+(-b|-B)\s'; then
|
|
34
|
+
# Exclude "git checkout -- <files>" (handled elsewhere)
|
|
35
|
+
echo "$COMMAND" | grep -qE 'git\s+checkout\s+--\s' && exit 0
|
|
36
|
+
IS_CHECKOUT=true
|
|
37
|
+
fi
|
|
38
|
+
if echo "$COMMAND" | grep -qE 'git\s+switch\s+[^-]' && ! echo "$COMMAND" | grep -qE 'git\s+switch\s+(-c|-C)\s'; then
|
|
39
|
+
IS_CHECKOUT=true
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
[ "$IS_CHECKOUT" = "false" ] && exit 0
|
|
43
|
+
|
|
44
|
+
# Check for uncommitted changes
|
|
45
|
+
DIRTY=$(git status --porcelain 2>/dev/null | head -1)
|
|
46
|
+
if [ -n "$DIRTY" ]; then
|
|
47
|
+
CHANGED=$(git status --porcelain 2>/dev/null | wc -l)
|
|
48
|
+
echo "BLOCKED: Cannot switch branches with $CHANGED uncommitted change(s)." >&2
|
|
49
|
+
echo "" >&2
|
|
50
|
+
echo "Command: $COMMAND" >&2
|
|
51
|
+
echo "" >&2
|
|
52
|
+
echo "Uncommitted changes would be overwritten by the target branch." >&2
|
|
53
|
+
echo "Options:" >&2
|
|
54
|
+
echo " git stash # save changes, switch, then git stash pop" >&2
|
|
55
|
+
echo " git commit -m 'WIP' # commit changes before switching" >&2
|
|
56
|
+
echo " git checkout -b <new-branch> # create branch (keeps changes)" >&2
|
|
57
|
+
exit 2
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
exit 0
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# git-message-length-check.sh — Warn on too-short commit messages
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude Code sometimes writes very short commit messages like
|
|
7
|
+
# "fix" or "update". This PostToolUse hook checks the commit
|
|
8
|
+
# message length and warns if it's too short to be meaningful.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PostToolUse
|
|
11
|
+
# MATCHER: "Bash"
|
|
12
|
+
#
|
|
13
|
+
# CONFIGURATION:
|
|
14
|
+
# CC_COMMIT_MIN_LENGTH=10 (minimum message length, default: 10)
|
|
15
|
+
# ================================================================
|
|
16
|
+
|
|
17
|
+
INPUT=$(cat)
|
|
18
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
19
|
+
[ -z "$COMMAND" ] && exit 0
|
|
20
|
+
|
|
21
|
+
# Only check git commit commands
|
|
22
|
+
echo "$COMMAND" | grep -qE 'git\s+commit' || exit 0
|
|
23
|
+
|
|
24
|
+
MIN_LENGTH="${CC_COMMIT_MIN_LENGTH:-10}"
|
|
25
|
+
|
|
26
|
+
# Extract message from -m flag
|
|
27
|
+
MSG=$(echo "$COMMAND" | grep -oE '\-m\s+["'"'"'][^"'"'"']*["'"'"']' | sed "s/-m\s*[\"']\(.*\)[\"']/\1/")
|
|
28
|
+
[ -z "$MSG" ] && exit 0
|
|
29
|
+
|
|
30
|
+
LENGTH=${#MSG}
|
|
31
|
+
|
|
32
|
+
if [ "$LENGTH" -lt "$MIN_LENGTH" ]; then
|
|
33
|
+
echo "⚠ Commit message too short ($LENGTH chars, minimum: $MIN_LENGTH)" >&2
|
|
34
|
+
echo " Message: \"$MSG\"" >&2
|
|
35
|
+
echo " Write descriptive messages explaining WHY, not WHAT." >&2
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
exit 0
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# plan-mode-edit-guard.sh — Warn when editing non-plan files during plan mode
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude editing source files while plan mode is active,
|
|
5
|
+
# bypassing the "plan only" constraint. Real incident: #38255 —
|
|
6
|
+
# Opus edited api/app/routers/ despite system reminders saying
|
|
7
|
+
# plan mode was active.
|
|
8
|
+
#
|
|
9
|
+
# How it works: Uses a flag file to track plan mode state.
|
|
10
|
+
# - Create ~/.claude/plan-mode-active to enable (touch the file)
|
|
11
|
+
# - Delete it to disable (rm the file)
|
|
12
|
+
# - When active, Edit/Write to non-plan files triggers a warning
|
|
13
|
+
#
|
|
14
|
+
# Integrate with your workflow:
|
|
15
|
+
# Before entering plan mode: touch ~/.claude/plan-mode-active
|
|
16
|
+
# After exiting plan mode: rm -f ~/.claude/plan-mode-active
|
|
17
|
+
#
|
|
18
|
+
# Does NOT block (warns only) because false positives during
|
|
19
|
+
# legitimate plan-mode file creation would be disruptive.
|
|
20
|
+
# Change exit 0 to exit 2 at the bottom to enforce strict blocking.
|
|
21
|
+
#
|
|
22
|
+
# TRIGGER: PreToolUse MATCHER: "Edit|Write"
|
|
23
|
+
|
|
24
|
+
INPUT=$(cat)
|
|
25
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
26
|
+
|
|
27
|
+
FLAG="$HOME/.claude/plan-mode-active"
|
|
28
|
+
|
|
29
|
+
# Only check when plan mode flag exists
|
|
30
|
+
[ ! -f "$FLAG" ] && exit 0
|
|
31
|
+
|
|
32
|
+
# Get the file being edited
|
|
33
|
+
case "$TOOL" in
|
|
34
|
+
Edit)
|
|
35
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
36
|
+
;;
|
|
37
|
+
Write)
|
|
38
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
39
|
+
;;
|
|
40
|
+
*)
|
|
41
|
+
exit 0
|
|
42
|
+
;;
|
|
43
|
+
esac
|
|
44
|
+
|
|
45
|
+
[ -z "$FILE" ] && exit 0
|
|
46
|
+
|
|
47
|
+
# Allow plan files
|
|
48
|
+
if echo "$FILE" | grep -qiE '\.plan\.md$|plan\.md$|/plans/|task_plan\.md|findings\.md|progress\.md'; then
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Allow CLAUDE.md and memory files (often updated during planning)
|
|
53
|
+
if echo "$FILE" | grep -qiE 'CLAUDE\.md$|MEMORY\.md$|memory/|\.claude/'; then
|
|
54
|
+
exit 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# Warn about non-plan file edits
|
|
58
|
+
echo "⚠ PLAN MODE ACTIVE: Editing non-plan file: $FILE" >&2
|
|
59
|
+
echo " Plan mode should only modify plan files." >&2
|
|
60
|
+
echo " Remove ~/.claude/plan-mode-active to disable this check." >&2
|
|
61
|
+
|
|
62
|
+
# Warning only (exit 0). Change to exit 2 to block.
|
|
63
|
+
exit 0
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# unicode-corruption-check.sh — Detect Unicode corruption after Edit/Write
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude Code's Edit tool corrupting non-ASCII Unicode characters
|
|
5
|
+
# (typographic quotes, em dashes, accented characters) in string literals.
|
|
6
|
+
# Real incident: #38765 — Edit tool replaced Unicode quotes with
|
|
7
|
+
# escaped sequences, breaking string comparisons.
|
|
8
|
+
#
|
|
9
|
+
# How it works: After Edit/Write, checks if the file contains common
|
|
10
|
+
# corruption patterns (replacement characters, broken UTF-8 sequences).
|
|
11
|
+
#
|
|
12
|
+
# TRIGGER: PostToolUse MATCHER: "Edit|Write"
|
|
13
|
+
|
|
14
|
+
INPUT=$(cat)
|
|
15
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
16
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
17
|
+
|
|
18
|
+
[ -z "$FILE" ] && exit 0
|
|
19
|
+
[ ! -f "$FILE" ] && exit 0
|
|
20
|
+
|
|
21
|
+
# Skip binary files
|
|
22
|
+
file "$FILE" 2>/dev/null | grep -q "text" || exit 0
|
|
23
|
+
|
|
24
|
+
# Check for Unicode replacement character (U+FFFD) — sign of broken encoding
|
|
25
|
+
if grep -Pq '\xef\xbf\xbd' "$FILE" 2>/dev/null; then
|
|
26
|
+
echo "⚠ Unicode corruption detected in $FILE" >&2
|
|
27
|
+
echo " Found U+FFFD replacement characters (broken encoding)." >&2
|
|
28
|
+
echo " Review the edit — non-ASCII characters may have been corrupted." >&2
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Check for common corruption: escaped Unicode in places it shouldn't be
|
|
32
|
+
# e.g., \u2018 appearing in plain text files (not JSON/JS)
|
|
33
|
+
if ! echo "$FILE" | grep -qE '\.(json|js|ts|jsx|tsx)$'; then
|
|
34
|
+
if grep -qE '\\u[0-9a-fA-F]{4}' "$FILE" 2>/dev/null; then
|
|
35
|
+
echo "⚠ Possible Unicode escape in non-JS file: $FILE" >&2
|
|
36
|
+
echo " Found \\uXXXX sequences that may be corrupted characters." >&2
|
|
37
|
+
fi
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
exit 0
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "29.6.
|
|
4
|
-
"description": "One command to make Claude Code safe.
|
|
3
|
+
"version": "29.6.12",
|
|
4
|
+
"description": "One command to make Claude Code safe. 425 example hooks + 8 built-in. 52 CLI commands. 5695 tests. Works with Auto Mode.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cc-safe-setup": "index.mjs"
|