cc-safe-setup 29.6.33 → 29.6.37
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/claudemd-violation-detector.sh +36 -0
- package/examples/clear-command-confirm-guard.sh +21 -0
- package/examples/core-file-protect-guard.sh +91 -0
- package/examples/cwd-drift-detector.sh +47 -0
- package/examples/deployment-verify-guard.sh +81 -0
- package/examples/edit-old-string-validator.sh +37 -0
- package/examples/encoding-preserve-guard.sh +34 -0
- package/examples/git-crypt-worktree-guard.sh +36 -0
- package/examples/git-operations-require-approval.sh +99 -0
- package/examples/line-ending-guard.sh +30 -0
- package/examples/permission-pattern-auto-allow.sh +50 -0
- package/examples/read-audit-log.sh +34 -0
- package/examples/session-duration-guard.sh +51 -0
- package/examples/settings-auto-backup.sh +53 -0
- package/examples/settings-mutation-detector.sh +45 -0
- package/examples/subagent-context-size-guard.sh +26 -0
- package/examples/symlink-protect.sh +12 -0
- package/examples/temp-file-cleanup-stop.sh +28 -0
- package/examples/test-before-commit.sh +13 -16
- package/examples/token-spike-alert.sh +51 -0
- package/examples/virtual-cwd-helper.sh +40 -0
- package/examples/worktree-delete-guard.sh +43 -0
- package/examples/worktree-path-validator.sh +42 -0
- package/examples/write-shrink-guard.sh +46 -0
- package/index.mjs +631 -138
- package/package.json +2 -2
- package/scripts/generate-categories.mjs +206 -0
- package/scripts.json +4 -1
- package/test.sh.new_tests +0 -0
- package/test.sh.patch +0 -0
- package/tests/test-core-file-protect-guard.sh +73 -0
- package/tests/test-deployment-verify-guard.sh +74 -0
- package/tests/test-git-operations-require-approval.sh +65 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# settings-auto-backup.sh — Auto-backup settings on session start
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude Code auto-updates have been observed silently wiping
|
|
7
|
+
# settings.json, settings.local.json, and plugin state. (#40714)
|
|
8
|
+
# This hook creates rolling backups on every session start and
|
|
9
|
+
# warns if settings appear to have been reset.
|
|
10
|
+
#
|
|
11
|
+
# TRIGGER: Notification
|
|
12
|
+
# MATCHER: "SessionStart"
|
|
13
|
+
#
|
|
14
|
+
# BACKUPS: ~/.claude/settings-backups/
|
|
15
|
+
# ================================================================
|
|
16
|
+
|
|
17
|
+
BACKUP_DIR="$HOME/.claude/settings-backups"
|
|
18
|
+
mkdir -p "$BACKUP_DIR"
|
|
19
|
+
|
|
20
|
+
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
|
21
|
+
BACKED_UP=0
|
|
22
|
+
|
|
23
|
+
# Backup settings files
|
|
24
|
+
for f in settings.json settings.local.json; do
|
|
25
|
+
SRC="$HOME/.claude/$f"
|
|
26
|
+
if [ -f "$SRC" ] && [ -s "$SRC" ]; then
|
|
27
|
+
cp "$SRC" "$BACKUP_DIR/${f%.json}-${TIMESTAMP}.json"
|
|
28
|
+
BACKED_UP=$((BACKED_UP + 1))
|
|
29
|
+
fi
|
|
30
|
+
done
|
|
31
|
+
|
|
32
|
+
# Keep only last 10 backups per file type
|
|
33
|
+
for prefix in settings settings.local; do
|
|
34
|
+
ls -t "$BACKUP_DIR/${prefix}-"*.json 2>/dev/null | tail -n +11 | xargs rm -f 2>/dev/null
|
|
35
|
+
done
|
|
36
|
+
|
|
37
|
+
# Detect suspicious settings reset
|
|
38
|
+
SETTINGS="$HOME/.claude/settings.json"
|
|
39
|
+
if [ -f "$SETTINGS" ]; then
|
|
40
|
+
KEY_COUNT=$(jq 'keys | length' "$SETTINGS" 2>/dev/null || echo 0)
|
|
41
|
+
if [ "$KEY_COUNT" -le 1 ]; then
|
|
42
|
+
LATEST_BACKUP=$(ls -t "$BACKUP_DIR/settings-"*.json 2>/dev/null | head -2 | tail -1)
|
|
43
|
+
if [ -n "$LATEST_BACKUP" ]; then
|
|
44
|
+
BACKUP_KEYS=$(jq 'keys | length' "$LATEST_BACKUP" 2>/dev/null || echo 0)
|
|
45
|
+
if [ "$BACKUP_KEYS" -gt "$KEY_COUNT" ]; then
|
|
46
|
+
echo "⚠ Settings may have been reset ($KEY_COUNT keys vs $BACKUP_KEYS in backup)" >&2
|
|
47
|
+
echo " Restore: cp '$LATEST_BACKUP' '$SETTINGS'" >&2
|
|
48
|
+
fi
|
|
49
|
+
fi
|
|
50
|
+
fi
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
exit 0
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# settings-mutation-detector.sh — Detect unauthorized changes to Claude settings files
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude Code can modify its own settings files during
|
|
5
|
+
# a session, potentially disabling safety hooks or
|
|
6
|
+
# changing permissions without user awareness.
|
|
7
|
+
#
|
|
8
|
+
# How it works: On first run, takes a hash of key settings files.
|
|
9
|
+
# On subsequent runs, compares the current hash. If changed,
|
|
10
|
+
# warns the user. This catches silent permission escalation
|
|
11
|
+
# or hook removal.
|
|
12
|
+
#
|
|
13
|
+
# TRIGGER: PostToolUse
|
|
14
|
+
# MATCHER: ""
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
HASH_FILE="/tmp/claude-settings-hash-$$"
|
|
19
|
+
|
|
20
|
+
# Files to monitor
|
|
21
|
+
SETTINGS_FILES=""
|
|
22
|
+
for f in \
|
|
23
|
+
".claude/settings.json" \
|
|
24
|
+
".claude/settings.local.json" \
|
|
25
|
+
"${HOME}/.claude/settings.json" \
|
|
26
|
+
"${HOME}/.claude/settings.local.json"; do
|
|
27
|
+
[ -f "$f" ] && SETTINGS_FILES="$SETTINGS_FILES $f"
|
|
28
|
+
done
|
|
29
|
+
|
|
30
|
+
[ -z "$SETTINGS_FILES" ] && exit 0
|
|
31
|
+
|
|
32
|
+
# Calculate current hash
|
|
33
|
+
CURRENT_HASH=$(cat $SETTINGS_FILES 2>/dev/null | md5sum | cut -d' ' -f1)
|
|
34
|
+
|
|
35
|
+
if [ -f "$HASH_FILE" ]; then
|
|
36
|
+
PREV_HASH=$(cat "$HASH_FILE")
|
|
37
|
+
if [ "$CURRENT_HASH" != "$PREV_HASH" ]; then
|
|
38
|
+
echo "WARNING: Claude settings files were modified during this session!" >&2
|
|
39
|
+
echo " Files monitored: $SETTINGS_FILES" >&2
|
|
40
|
+
echo " Review changes to ensure hooks and permissions are intact." >&2
|
|
41
|
+
fi
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
echo "$CURRENT_HASH" > "$HASH_FILE"
|
|
45
|
+
exit 0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# subagent-context-size-guard.sh — Warn on thin subagent prompts
|
|
3
|
+
#
|
|
4
|
+
# Solves: Subagents get spawned with minimal context, leading to
|
|
5
|
+
# poor results because they lack necessary background (#40929).
|
|
6
|
+
# The parent agent assumes shared context, but each subagent
|
|
7
|
+
# starts fresh.
|
|
8
|
+
#
|
|
9
|
+
# How it works: Checks Agent tool's prompt parameter length.
|
|
10
|
+
# If under 100 characters, warns that the prompt may be too thin
|
|
11
|
+
# for a standalone agent to work effectively.
|
|
12
|
+
#
|
|
13
|
+
# TRIGGER: PreToolUse
|
|
14
|
+
# MATCHER: "Agent"
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
INPUT=$(cat)
|
|
18
|
+
PROMPT=$(echo "$INPUT" | jq -r '.tool_input.prompt // empty' 2>/dev/null)
|
|
19
|
+
|
|
20
|
+
[ -z "$PROMPT" ] && exit 0
|
|
21
|
+
|
|
22
|
+
LEN=${#PROMPT}
|
|
23
|
+
if [ "$LEN" -lt 100 ]; then
|
|
24
|
+
echo "WARNING: Agent prompt is only ${LEN} chars. Subagents start with zero context — include enough background for them to work independently." >&2
|
|
25
|
+
fi
|
|
26
|
+
exit 0
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
INPUT=$(cat)
|
|
2
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
3
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
4
|
+
[ -z "$FILE" ] && exit 0
|
|
5
|
+
[[ "$TOOL" != "Write" && "$TOOL" != "Edit" ]] && exit 0
|
|
6
|
+
if [ -L "$FILE" ]; then
|
|
7
|
+
TARGET=$(readlink -f "$FILE")
|
|
8
|
+
echo "NOTE: Redirecting write from symlink $FILE → $TARGET" >&2
|
|
9
|
+
echo "{\"updatedInput\":{\"file_path\":\"$TARGET\"}}"
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
exit 0
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# temp-file-cleanup-stop.sh — Clean up tmpclaude-* files on session end
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude Code creates tmpclaude-{hash}-cwd temporary
|
|
5
|
+
# files in the working directory but doesn't clean them
|
|
6
|
+
# up after the session ends (#17720). These accumulate
|
|
7
|
+
# over time and clutter the project.
|
|
8
|
+
#
|
|
9
|
+
# How it works: On Stop event, finds and removes all
|
|
10
|
+
# tmpclaude-*-cwd files in the current directory and /tmp.
|
|
11
|
+
# Only removes files matching the exact pattern to avoid
|
|
12
|
+
# deleting user files.
|
|
13
|
+
#
|
|
14
|
+
# TRIGGER: Stop
|
|
15
|
+
# MATCHER: ""
|
|
16
|
+
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
|
|
19
|
+
# Clean up tmpclaude-* files in current directory
|
|
20
|
+
find . -maxdepth 1 -name "tmpclaude-*-cwd" -type f -delete 2>/dev/null || true
|
|
21
|
+
|
|
22
|
+
# Also clean up in /tmp
|
|
23
|
+
find /tmp -maxdepth 1 -name "tmpclaude-*" -type f -mmin +60 -delete 2>/dev/null || true
|
|
24
|
+
|
|
25
|
+
# Clean up any .claude-tmp-* files too
|
|
26
|
+
find . -maxdepth 1 -name ".claude-tmp-*" -type f -delete 2>/dev/null || true
|
|
27
|
+
|
|
28
|
+
exit 0
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
2
|
+
INPUT=$(cat)
|
|
3
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
5
4
|
[ -z "$COMMAND" ] && exit 0
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
echo "Run your test suite first, then commit." >&2
|
|
18
|
-
exit 2
|
|
5
|
+
STATE="/tmp/cc-tests-ran-$$"
|
|
6
|
+
if echo "$COMMAND" | grep -qE '^\s*(npm\s+test|npx\s+jest|pytest|python\s+-m\s+pytest|cargo\s+test|go\s+test|make\s+test|bundle\s+exec\s+rspec|mix\s+test)'; then
|
|
7
|
+
echo "1" > "$STATE"
|
|
8
|
+
exit 0
|
|
9
|
+
fi
|
|
10
|
+
if echo "$COMMAND" | grep -qE '^\s*git\s+commit'; then
|
|
11
|
+
if [ ! -f "$STATE" ] || [ "$(cat "$STATE" 2>/dev/null)" != "1" ]; then
|
|
12
|
+
echo "WARNING: No test commands detected since last commit." >&2
|
|
13
|
+
echo " Run tests before committing to verify your changes." >&2
|
|
14
|
+
fi
|
|
15
|
+
rm -f "$STATE"
|
|
19
16
|
fi
|
|
20
17
|
exit 0
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# token-spike-alert.sh — Alert on abnormal token consumption per turn
|
|
3
|
+
#
|
|
4
|
+
# Solves: Users report 10-20% of their 5-hour quota consumed by
|
|
5
|
+
# a single lightweight question (#40524, #38029, #40881).
|
|
6
|
+
# Cache invalidation causes full context re-processing,
|
|
7
|
+
# spiking token usage without user awareness.
|
|
8
|
+
#
|
|
9
|
+
# How it works: Tracks tool call count per session via a counter
|
|
10
|
+
# file. If more than MAX_TOOLS_PER_TURN tool calls happen in
|
|
11
|
+
# rapid succession (within 30 seconds), warns about possible
|
|
12
|
+
# runaway behavior that could spike token usage.
|
|
13
|
+
#
|
|
14
|
+
# TRIGGER: PostToolUse
|
|
15
|
+
# MATCHER: ""
|
|
16
|
+
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
|
|
19
|
+
COUNTER_FILE="/tmp/claude-token-spike-$$"
|
|
20
|
+
MAX_TOOLS_PER_BURST="${MAX_TOOLS_PER_BURST:-15}"
|
|
21
|
+
|
|
22
|
+
# Get current timestamp
|
|
23
|
+
NOW=$(date +%s)
|
|
24
|
+
|
|
25
|
+
# Read last timestamp and count
|
|
26
|
+
if [ -f "$COUNTER_FILE" ]; then
|
|
27
|
+
LAST_TS=$(head -1 "$COUNTER_FILE" 2>/dev/null || echo "0")
|
|
28
|
+
COUNT=$(tail -1 "$COUNTER_FILE" 2>/dev/null || echo "0")
|
|
29
|
+
else
|
|
30
|
+
LAST_TS=0
|
|
31
|
+
COUNT=0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# If within 30-second burst window
|
|
35
|
+
DELTA=$((NOW - LAST_TS))
|
|
36
|
+
if [ "$DELTA" -lt 30 ]; then
|
|
37
|
+
COUNT=$((COUNT + 1))
|
|
38
|
+
else
|
|
39
|
+
COUNT=1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Save state
|
|
43
|
+
echo "$NOW" > "$COUNTER_FILE"
|
|
44
|
+
echo "$COUNT" >> "$COUNTER_FILE"
|
|
45
|
+
|
|
46
|
+
# Alert if burst detected
|
|
47
|
+
if [ "$COUNT" -ge "$MAX_TOOLS_PER_BURST" ]; then
|
|
48
|
+
echo "WARNING: $COUNT tool calls in ${DELTA}s burst. Possible runaway behavior — check token consumption." >&2
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
exit 0
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# virtual-cwd-helper.sh — Remind about virtual working directory
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude Code is bound to the directory where it was
|
|
5
|
+
# spawned. Users can't switch projects mid-session (#3473).
|
|
6
|
+
#
|
|
7
|
+
# How it works: Reads ~/.claude/virtual-cwd file. If set,
|
|
8
|
+
# warns that commands should be prefixed with cd to the
|
|
9
|
+
# virtual CWD. Users can switch directories by updating
|
|
10
|
+
# the file.
|
|
11
|
+
#
|
|
12
|
+
# Setup: echo "/path/to/project" > ~/.claude/virtual-cwd
|
|
13
|
+
#
|
|
14
|
+
# TRIGGER: PreToolUse
|
|
15
|
+
# MATCHER: "Bash"
|
|
16
|
+
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
INPUT=$(cat)
|
|
19
|
+
|
|
20
|
+
VCWD_FILE="${HOME}/.claude/virtual-cwd"
|
|
21
|
+
[ ! -f "$VCWD_FILE" ] && exit 0
|
|
22
|
+
|
|
23
|
+
VCWD=$(cat "$VCWD_FILE" 2>/dev/null)
|
|
24
|
+
[ -z "$VCWD" ] && exit 0
|
|
25
|
+
|
|
26
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
27
|
+
[ -z "$COMMAND" ] && exit 0
|
|
28
|
+
|
|
29
|
+
# Skip if command already starts with cd to the virtual CWD
|
|
30
|
+
if echo "$COMMAND" | grep -q "^cd $VCWD"; then
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Skip cd commands (user is navigating)
|
|
35
|
+
if echo "$COMMAND" | grep -q "^cd "; then
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
echo "NOTE: Virtual CWD is $VCWD — prefix with: cd $VCWD &&" >&2
|
|
40
|
+
exit 0
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# worktree-delete-guard.sh — Block git worktree removal
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Prevents one Claude session from deleting a worktree that
|
|
7
|
+
# another session is actively using. Opus 4.6 has been observed
|
|
8
|
+
# removing worktrees during cleanup without checking for
|
|
9
|
+
# concurrent sessions. (#40850)
|
|
10
|
+
#
|
|
11
|
+
# TRIGGER: PreToolUse
|
|
12
|
+
# MATCHER: "Bash"
|
|
13
|
+
#
|
|
14
|
+
# WHAT IT BLOCKS:
|
|
15
|
+
# - git worktree remove <path>
|
|
16
|
+
# - git worktree prune
|
|
17
|
+
# - rm -rf on worktree directories
|
|
18
|
+
# ================================================================
|
|
19
|
+
|
|
20
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
21
|
+
[ -z "$COMMAND" ] && exit 0
|
|
22
|
+
|
|
23
|
+
# Block explicit worktree removal
|
|
24
|
+
if echo "$COMMAND" | grep -qE 'git\s+worktree\s+(remove|prune)'; then
|
|
25
|
+
echo "BLOCKED: Cannot remove git worktrees — other sessions may depend on them." >&2
|
|
26
|
+
echo "Command: $COMMAND" >&2
|
|
27
|
+
echo "List worktrees first: git worktree list" >&2
|
|
28
|
+
exit 2
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Block rm on worktree paths (if we're in a git repo)
|
|
32
|
+
if git rev-parse --git-dir &>/dev/null; then
|
|
33
|
+
COMMON_DIR=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)
|
|
34
|
+
if [ -n "$COMMON_DIR" ]; then
|
|
35
|
+
WORKTREES_DIR="${COMMON_DIR}/worktrees"
|
|
36
|
+
if echo "$COMMAND" | grep -qE "(rm|rmdir)\s+.*worktrees"; then
|
|
37
|
+
echo "BLOCKED: Cannot delete worktree storage directory." >&2
|
|
38
|
+
exit 2
|
|
39
|
+
fi
|
|
40
|
+
fi
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
exit 0
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# worktree-path-validator.sh — Warn when file operations target main workspace instead of worktree
|
|
3
|
+
#
|
|
4
|
+
# Solves: In worktree sessions, Edit/Read/Write tools target
|
|
5
|
+
# files in the main workspace instead of the worktree
|
|
6
|
+
# directory (#36182). This causes edits to the wrong
|
|
7
|
+
# copy of files.
|
|
8
|
+
#
|
|
9
|
+
# How it works: Detects if running in a worktree (git rev-parse
|
|
10
|
+
# --git-common-dir differs from --git-dir). If so, checks
|
|
11
|
+
# that file_path targets the worktree, not the main workspace.
|
|
12
|
+
#
|
|
13
|
+
# TRIGGER: PreToolUse
|
|
14
|
+
# MATCHER: "Edit|Write|Read"
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
INPUT=$(cat)
|
|
18
|
+
|
|
19
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
20
|
+
[ -z "$FILE_PATH" ] && exit 0
|
|
21
|
+
|
|
22
|
+
# Check if we're in a worktree
|
|
23
|
+
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) || exit 0
|
|
24
|
+
GIT_COMMON=$(git rev-parse --git-common-dir 2>/dev/null) || exit 0
|
|
25
|
+
|
|
26
|
+
# If git-dir == git-common-dir, we're in the main repo (not a worktree)
|
|
27
|
+
[ "$GIT_DIR" = "$GIT_COMMON" ] && exit 0
|
|
28
|
+
|
|
29
|
+
# We're in a worktree — check that file_path is within the worktree
|
|
30
|
+
WORKTREE_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
|
|
31
|
+
MAIN_ROOT=$(cd "$GIT_COMMON/.." && pwd 2>/dev/null) || exit 0
|
|
32
|
+
|
|
33
|
+
# If file_path starts with the main workspace path instead of worktree
|
|
34
|
+
if echo "$FILE_PATH" | grep -q "^$MAIN_ROOT" && ! echo "$FILE_PATH" | grep -q "^$WORKTREE_ROOT"; then
|
|
35
|
+
echo "WARNING: File path targets main workspace, not this worktree." >&2
|
|
36
|
+
echo " File: $FILE_PATH" >&2
|
|
37
|
+
echo " Worktree: $WORKTREE_ROOT" >&2
|
|
38
|
+
echo " Main repo: $MAIN_ROOT" >&2
|
|
39
|
+
echo " Consider using: ${FILE_PATH/$MAIN_ROOT/$WORKTREE_ROOT}" >&2
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
exit 0
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# write-shrink-guard.sh — Block writes that drastically shrink files
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Prevents accidental file truncation. When Claude uses the Write
|
|
7
|
+
# tool, if the new content is <10% of the original file size,
|
|
8
|
+
# it's likely a truncation bug, not an intentional edit.
|
|
9
|
+
#
|
|
10
|
+
# Real case: 31,699-line file truncated to 16 lines, destroying
|
|
11
|
+
# 5 hours of work. (#40807)
|
|
12
|
+
#
|
|
13
|
+
# TRIGGER: PreToolUse
|
|
14
|
+
# MATCHER: "Write"
|
|
15
|
+
#
|
|
16
|
+
# DECISION: exit 2 = block, exit 0 = allow
|
|
17
|
+
# ================================================================
|
|
18
|
+
|
|
19
|
+
INPUT=$(cat)
|
|
20
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
21
|
+
[ "$TOOL" != "Write" ] && exit 0
|
|
22
|
+
|
|
23
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
24
|
+
[ -z "$FILE" ] || [ ! -f "$FILE" ] && exit 0
|
|
25
|
+
|
|
26
|
+
# Get original file size
|
|
27
|
+
OLD_SIZE=$(wc -c < "$FILE" 2>/dev/null || echo 0)
|
|
28
|
+
[ "$OLD_SIZE" -lt 1000 ] && exit 0 # Skip small files
|
|
29
|
+
|
|
30
|
+
# Get new content size
|
|
31
|
+
NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty' 2>/dev/null)
|
|
32
|
+
NEW_SIZE=${#NEW_CONTENT}
|
|
33
|
+
|
|
34
|
+
# Calculate ratio
|
|
35
|
+
if [ "$NEW_SIZE" -gt 0 ] && [ "$OLD_SIZE" -gt 0 ]; then
|
|
36
|
+
RATIO=$((NEW_SIZE * 100 / OLD_SIZE))
|
|
37
|
+
if [ "$RATIO" -lt 10 ]; then
|
|
38
|
+
echo "BLOCKED: Write would shrink $(basename "$FILE") from $OLD_SIZE to $NEW_SIZE bytes (${RATIO}% of original)." >&2
|
|
39
|
+
echo "This looks like accidental truncation. Use Edit for targeted changes instead." >&2
|
|
40
|
+
exit 2
|
|
41
|
+
elif [ "$RATIO" -lt 25 ]; then
|
|
42
|
+
echo "WARNING: Write would significantly reduce $(basename "$FILE") from $OLD_SIZE to $NEW_SIZE bytes (${RATIO}%)." >&2
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
exit 0
|