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.
Files changed (34) hide show
  1. package/README.md +1 -1
  2. package/examples/claudemd-violation-detector.sh +36 -0
  3. package/examples/clear-command-confirm-guard.sh +21 -0
  4. package/examples/core-file-protect-guard.sh +91 -0
  5. package/examples/cwd-drift-detector.sh +47 -0
  6. package/examples/deployment-verify-guard.sh +81 -0
  7. package/examples/edit-old-string-validator.sh +37 -0
  8. package/examples/encoding-preserve-guard.sh +34 -0
  9. package/examples/git-crypt-worktree-guard.sh +36 -0
  10. package/examples/git-operations-require-approval.sh +99 -0
  11. package/examples/line-ending-guard.sh +30 -0
  12. package/examples/permission-pattern-auto-allow.sh +50 -0
  13. package/examples/read-audit-log.sh +34 -0
  14. package/examples/session-duration-guard.sh +51 -0
  15. package/examples/settings-auto-backup.sh +53 -0
  16. package/examples/settings-mutation-detector.sh +45 -0
  17. package/examples/subagent-context-size-guard.sh +26 -0
  18. package/examples/symlink-protect.sh +12 -0
  19. package/examples/temp-file-cleanup-stop.sh +28 -0
  20. package/examples/test-before-commit.sh +13 -16
  21. package/examples/token-spike-alert.sh +51 -0
  22. package/examples/virtual-cwd-helper.sh +40 -0
  23. package/examples/worktree-delete-guard.sh +43 -0
  24. package/examples/worktree-path-validator.sh +42 -0
  25. package/examples/write-shrink-guard.sh +46 -0
  26. package/index.mjs +631 -138
  27. package/package.json +2 -2
  28. package/scripts/generate-categories.mjs +206 -0
  29. package/scripts.json +4 -1
  30. package/test.sh.new_tests +0 -0
  31. package/test.sh.patch +0 -0
  32. package/tests/test-core-file-protect-guard.sh +73 -0
  33. package/tests/test-deployment-verify-guard.sh +74 -0
  34. 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
- # TRIGGER: PreToolUse MATCHER: "Bash"
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
- echo "$COMMAND" | grep -qE '^\s*git\s+commit' || exit 0
7
- RECENT=0
8
- TIMEOUT=${CC_TEST_TIMEOUT:-600}
9
- NOW=$(date +%s)
10
- for marker in coverage/.last-run.json test-results .nyc_output junit.xml; do
11
- [ -e "$marker" ] || continue
12
- MTIME=$(stat -c %Y "$marker" 2>/dev/null || echo 0)
13
- [ $((NOW - MTIME)) -lt "$TIMEOUT" ] && RECENT=1 && break
14
- done
15
- if [ "$RECENT" -eq 0 ]; then
16
- echo "BLOCKED: No recent test results (within $((TIMEOUT/60)) min)" >&2
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