cc-safe-setup 29.6.40 → 29.7.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.
Files changed (79) hide show
  1. package/.claude-plugin/marketplace.json +66 -0
  2. package/.claude-plugin/plugin.json +11 -0
  3. package/README.md +123 -12
  4. package/SETTINGS_REFERENCE.md +2 -0
  5. package/SKILL.md +47 -0
  6. package/examples/README.md +11 -1
  7. package/examples/auto-approve-compound-git.sh +3 -0
  8. package/examples/auto-compact-context-monitor.sh +35 -0
  9. package/examples/auto-mode-safety-enforcer.sh +57 -0
  10. package/examples/background-task-guard.sh +57 -0
  11. package/examples/broad-find-guard.sh +62 -0
  12. package/examples/cache-creation-spike-detector.sh +32 -0
  13. package/examples/case-insensitive-path-guard.sh +96 -0
  14. package/examples/cjk-punctuation-guard.sh +44 -0
  15. package/examples/clipboard-secret-guard.sh +29 -0
  16. package/examples/context-size-alert.sh +38 -0
  17. package/examples/context-usage-drift-alert.sh +33 -0
  18. package/examples/dangerous-pip-flag-guard.sh +51 -0
  19. package/examples/deny-bypass-detector.sh +143 -0
  20. package/examples/dotenv-read-guard.sh +48 -0
  21. package/examples/dotfile-protection-guard.sh +60 -0
  22. package/examples/effort-tracking-logger.sh +30 -0
  23. package/examples/financial-operation-guard.sh +47 -0
  24. package/examples/full-rewrite-detector.sh +63 -0
  25. package/examples/home-critical-bash-guard.sh +56 -0
  26. package/examples/idle-session-cost-alert.sh +36 -0
  27. package/examples/model-version-alert.sh +18 -0
  28. package/examples/model-version-change-alert.sh +31 -0
  29. package/examples/move-delete-sequence-guard.sh +92 -0
  30. package/examples/pii-upload-guard.sh +72 -0
  31. package/examples/pr-duplicate-guard.sh +14 -0
  32. package/examples/production-port-kill-guard.sh +60 -0
  33. package/examples/quota-reset-cycle-monitor.sh +30 -0
  34. package/examples/repo-visibility-guard.sh +33 -0
  35. package/examples/sandbox-relative-path-audit.sh +51 -0
  36. package/examples/session-agent-cost-limiter.sh +43 -0
  37. package/examples/session-cost-alert.sh +62 -0
  38. package/examples/session-memory-watchdog.sh +9 -0
  39. package/examples/settings-integrity-monitor.sh +55 -0
  40. package/examples/settings-json-model-guard.sh +89 -0
  41. package/examples/shell-config-truncation-guard.sh +97 -0
  42. package/examples/shell-wrapper-guard.sh +4 -4
  43. package/examples/subagent-spawn-rate-monitor.sh +34 -0
  44. package/examples/subcommand-chain-guard.sh +44 -0
  45. package/examples/system-dir-protection-guard.sh +100 -0
  46. package/examples/thinking-display-enforcer.sh +25 -0
  47. package/examples/tool-retry-budget-guard.sh +59 -0
  48. package/examples/worktree-branch-pollution-detector.sh +35 -0
  49. package/examples/worktree-create-log.sh +6 -0
  50. package/examples/worktree-hook-linker.sh +72 -0
  51. package/examples/worktree-remove-uncommitted-guard.sh +20 -0
  52. package/hooks/hooks.json +60 -0
  53. package/index.mjs +92 -6
  54. package/memory/market-anthropic-japan-strategy-2026-04-13.md +4 -0
  55. package/package.json +2 -2
  56. package/plugins/credential-guard/.claude-plugin/plugin.json +58 -0
  57. package/plugins/git-protection/.claude-plugin/plugin.json +58 -0
  58. package/plugins/safety-essentials/.claude-plugin/plugin.json +58 -0
  59. package/plugins/token-guard/.claude-plugin/plugin.json +51 -0
  60. package/skills/safety-setup/SKILL.md +47 -0
  61. package/tests/dotenv-read-guard.test.sh +65 -0
  62. package/tests/test-auto-mode-safety-enforcer.sh +55 -0
  63. package/tests/test-case-insensitive-path-guard.sh +78 -0
  64. package/tests/test-context-usage-drift-alert.sh +52 -0
  65. package/tests/test-dangerous-pip-flag-guard.sh +56 -0
  66. package/tests/test-dotfile-protection-guard.sh +68 -0
  67. package/tests/test-effort-tracking-logger.sh +55 -0
  68. package/tests/test-financial-operation-guard.sh +59 -0
  69. package/tests/test-home-critical-bash-guard.sh +59 -0
  70. package/tests/test-model-version-change-alert.sh +55 -0
  71. package/tests/test-move-delete-sequence-guard.sh +63 -0
  72. package/tests/test-pr-duplicate-guard.sh +29 -0
  73. package/tests/test-quota-reset-cycle-monitor.sh +52 -0
  74. package/tests/test-shell-config-truncation-guard.sh +104 -0
  75. package/tests/test-subagent-spawn-rate-monitor.sh +43 -0
  76. package/tests/test-system-dir-protection-guard.sh +81 -0
  77. package/tests/test-tool-retry-budget-guard.sh +75 -0
  78. package/tests/test-worktree-branch-pollution-detector.sh +50 -0
  79. package/tests/test-worktree-lifecycle-hooks.sh +29 -0
@@ -0,0 +1,43 @@
1
+ #!/bin/bash
2
+ # session-agent-cost-limiter.sh — Cap total subagent spawns per session
3
+ #
4
+ # Solves: #47049 — User lost £140 overnight when Claude spawned 16+
5
+ # subagents. Each agent gets its own context window = 16x token cost.
6
+ # Existing max-concurrent-agents limits simultaneous agents, but not
7
+ # total spawns over a session. This hook limits the cumulative count.
8
+ #
9
+ # How it works: Tracks every Agent spawn in a session-scoped counter.
10
+ # After CC_MAX_SESSION_AGENTS total spawns, blocks further agents.
11
+ # Counter resets when the session ends (file keyed by PPID).
12
+ #
13
+ # CONFIG:
14
+ # CC_MAX_SESSION_AGENTS=10 (default: 10 total agents per session)
15
+ #
16
+ # TRIGGER: PreToolUse
17
+ # MATCHER: "Agent"
18
+ # CATEGORY: cost-control
19
+
20
+ INPUT=$(cat)
21
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
22
+ [ "$TOOL" != "Agent" ] && exit 0
23
+
24
+ MAX_TOTAL=${CC_MAX_SESSION_AGENTS:-10}
25
+ # Use PPID to track the parent Claude Code process, not this subshell
26
+ COUNTER_FILE="/tmp/cc-session-agents-${PPID}"
27
+
28
+ # Initialize if missing
29
+ [ -f "$COUNTER_FILE" ] || echo "0" > "$COUNTER_FILE"
30
+
31
+ CURRENT=$(cat "$COUNTER_FILE" 2>/dev/null || echo 0)
32
+
33
+ if [ "$CURRENT" -ge "$MAX_TOTAL" ]; then
34
+ echo "BLOCKED: Session agent limit reached (${CURRENT}/${MAX_TOTAL} total spawns)." >&2
35
+ echo " Each subagent opens a new context window and consumes tokens independently." >&2
36
+ echo " Consider completing existing work before spawning more agents." >&2
37
+ echo " Override: CC_MAX_SESSION_AGENTS=$((MAX_TOTAL + 5))" >&2
38
+ exit 2
39
+ fi
40
+
41
+ # Increment
42
+ echo $((CURRENT + 1)) > "$COUNTER_FILE"
43
+ exit 0
@@ -0,0 +1,62 @@
1
+ #!/bin/bash
2
+ # session-cost-alert.sh — Alert when estimated session cost exceeds threshold
3
+ #
4
+ # Solves: #47049 — User lost £140 overnight without realizing costs were
5
+ # accumulating. This hook estimates token cost per tool call and warns
6
+ # when the session total exceeds a configurable threshold.
7
+ #
8
+ # How it works: PostToolUse hook that parses session_tokens from tool
9
+ # results (when available) and estimates cost using Anthropic pricing.
10
+ # Warns at $1 and blocks at $5 (configurable).
11
+ #
12
+ # CONFIG:
13
+ # CC_COST_WARN=1 (warn at $1, default)
14
+ # CC_COST_BLOCK=5 (block at $5, default)
15
+ # CC_MODEL_COST=5 ($/M input tokens for Opus, default)
16
+ #
17
+ # TRIGGER: PostToolUse
18
+ # MATCHER: ""
19
+ # CATEGORY: cost-control
20
+
21
+ INPUT=$(cat)
22
+
23
+ WARN_THRESHOLD=${CC_COST_WARN:-1}
24
+ BLOCK_THRESHOLD=${CC_COST_BLOCK:-5}
25
+ COST_PER_M=${CC_MODEL_COST:-5}
26
+
27
+ COST_FILE="/tmp/cc-session-cost-${PPID}"
28
+
29
+ # Initialize
30
+ if [ ! -f "$COST_FILE" ]; then
31
+ echo "0" > "$COST_FILE"
32
+ fi
33
+
34
+ # Try to extract token count from tool result
35
+ # Note: Not all tool results contain token info. This is a best-effort estimate.
36
+ TOKENS=$(echo "$INPUT" | jq -r '.tool_result // empty' 2>/dev/null | wc -c)
37
+ # Rough estimate: 1 char ≈ 0.3 tokens (for tool output going into context)
38
+ EST_TOKENS=$((TOKENS * 3 / 10))
39
+
40
+ # Add to running total
41
+ CURRENT=$(cat "$COST_FILE" 2>/dev/null || echo 0)
42
+ TOTAL=$((CURRENT + EST_TOKENS))
43
+ echo "$TOTAL" > "$COST_FILE"
44
+
45
+ # Estimate cost
46
+ COST=$(echo "scale=4; $TOTAL * $COST_PER_M / 1000000" | bc 2>/dev/null || echo "0")
47
+ COST_CENTS=$(echo "scale=0; $TOTAL * $COST_PER_M / 10000" | bc 2>/dev/null || echo "0")
48
+
49
+ # Check thresholds
50
+ BLOCK_CENTS=$(echo "scale=0; $BLOCK_THRESHOLD * 100" | bc 2>/dev/null || echo "500")
51
+ WARN_CENTS=$(echo "scale=0; $WARN_THRESHOLD * 100" | bc 2>/dev/null || echo "100")
52
+
53
+ if [ "$COST_CENTS" -ge "$BLOCK_CENTS" ] 2>/dev/null; then
54
+ echo "BLOCKED: Estimated session cost \$${COST} exceeds \$${BLOCK_THRESHOLD} limit." >&2
55
+ echo " Estimated tokens used: ${TOTAL}" >&2
56
+ echo " Override: CC_COST_BLOCK=$((BLOCK_THRESHOLD * 2))" >&2
57
+ exit 2
58
+ elif [ "$COST_CENTS" -ge "$WARN_CENTS" ] 2>/dev/null; then
59
+ echo "WARNING: Estimated session cost \$${COST} approaching \$${BLOCK_THRESHOLD} limit." >&2
60
+ fi
61
+
62
+ exit 0
@@ -1,6 +1,15 @@
1
1
  MAX_RSS_MB="${CC_MAX_RSS_MB:-4096}"
2
2
  CHECK_INTERVAL=300
3
+ PID_FILE="/tmp/cc-memory-watchdog.pid"
4
+
5
+ # Prevent duplicate instances — only one watchdog should run at a time
6
+ if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
7
+ exit 0
8
+ fi
9
+
3
10
  (
11
+ echo $$ > "$PID_FILE"
12
+ trap 'rm -f "$PID_FILE"' EXIT
4
13
  while true; do
5
14
  sleep "$CHECK_INTERVAL"
6
15
  pgrep -f "claude" | while read pid; do
@@ -0,0 +1,55 @@
1
+ #!/bin/bash
2
+ # settings-integrity-monitor.sh — Detect unexpected settings.json changes
3
+ # Trigger: PreToolUse
4
+ # Matcher: (empty — runs on every tool use)
5
+ #
6
+ # The /model command silently rewrites settings.json from scratch,
7
+ # removing sandbox restrictions and hook configurations.
8
+ # See: https://github.com/anthropics/claude-code/issues/44791
9
+ #
10
+ # This hook maintains a checksum of settings.json and warns when
11
+ # it changes outside your control. It also creates automatic backups
12
+ # so you can restore your configuration.
13
+ #
14
+ # TRIGGER: PreToolUse MATCHER: ""
15
+
16
+ SETTINGS="${CLAUDE_SETTINGS_FILE:-$HOME/.claude/settings.json}"
17
+ BACKUP_DIR="$HOME/.claude/settings-backups"
18
+ CHECKSUM_FILE="$BACKUP_DIR/.checksum"
19
+
20
+ # Exit silently if settings.json doesn't exist
21
+ [ -f "$SETTINGS" ] || exit 0
22
+
23
+ mkdir -p "$BACKUP_DIR"
24
+
25
+ CURRENT_HASH=$(sha256sum "$SETTINGS" 2>/dev/null | cut -d' ' -f1)
26
+
27
+ if [ ! -f "$CHECKSUM_FILE" ]; then
28
+ # First run: save baseline
29
+ echo "$CURRENT_HASH" > "$CHECKSUM_FILE"
30
+ cp "$SETTINGS" "$BACKUP_DIR/settings.json.baseline"
31
+ exit 0
32
+ fi
33
+
34
+ SAVED_HASH=$(cat "$CHECKSUM_FILE" 2>/dev/null)
35
+
36
+ if [ "$CURRENT_HASH" != "$SAVED_HASH" ]; then
37
+ # Settings changed — create timestamped backup of PREVIOUS version
38
+ TIMESTAMP=$(date +%Y%m%d-%H%M%S)
39
+ if [ -f "$BACKUP_DIR/settings.json.latest" ]; then
40
+ cp "$BACKUP_DIR/settings.json.latest" "$BACKUP_DIR/settings.json.$TIMESTAMP"
41
+ fi
42
+ # Save current as latest
43
+ cp "$SETTINGS" "$BACKUP_DIR/settings.json.latest"
44
+ echo "$CURRENT_HASH" > "$CHECKSUM_FILE"
45
+
46
+ # Count hooks in old vs new
47
+ OLD_HOOKS=$(jq '[.hooks | to_entries[].value[].hooks[]?] | length' "$BACKUP_DIR/settings.json.$TIMESTAMP" 2>/dev/null || echo "?")
48
+ NEW_HOOKS=$(jq '[.hooks | to_entries[].value[].hooks[]?] | length' "$SETTINGS" 2>/dev/null || echo "?")
49
+
50
+ echo "⚠ settings.json was modified (hooks: $OLD_HOOKS → $NEW_HOOKS)" >&2
51
+ echo " Backup saved: $BACKUP_DIR/settings.json.$TIMESTAMP" >&2
52
+ echo " Restore: cp $BACKUP_DIR/settings.json.$TIMESTAMP $SETTINGS" >&2
53
+ fi
54
+
55
+ exit 0
@@ -0,0 +1,89 @@
1
+ #!/bin/bash
2
+ # settings-json-model-guard.sh — Backup settings.json before /model changes
3
+ #
4
+ # Solves: The /model slash command rewrites settings.json completely,
5
+ # wiping all hook configurations and custom settings. (#46921)
6
+ #
7
+ # How it works: PreToolUse(Bash) detects commands that modify
8
+ # settings.json (especially from /model). Creates a timestamped
9
+ # backup before allowing the write. If hooks are lost after the
10
+ # write, the PostToolUse phase restores them.
11
+ #
12
+ # Usage: Add TWO hooks
13
+ #
14
+ # {
15
+ # "hooks": {
16
+ # "PreToolUse": [{
17
+ # "matcher": "Edit|Write",
18
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/settings-json-model-guard.sh" }]
19
+ # }],
20
+ # "PostToolUse": [{
21
+ # "matcher": "Edit|Write",
22
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/settings-json-model-guard.sh" }]
23
+ # }]
24
+ # }
25
+ # }
26
+ #
27
+ # TRIGGER: PreToolUse+PostToolUse MATCHER: "Edit|Write"
28
+
29
+ set -euo pipefail
30
+
31
+ INPUT=$(cat)
32
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
33
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
34
+ HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // empty' 2>/dev/null)
35
+
36
+ # Only care about settings.json writes
37
+ case "$FILE" in
38
+ */.claude/settings.json|*/.claude/settings.local.json) ;;
39
+ *) exit 0 ;;
40
+ esac
41
+
42
+ BACKUP_DIR="$HOME/.claude/settings-backups"
43
+ mkdir -p "$BACKUP_DIR"
44
+
45
+ SETTINGS_FILE="$FILE"
46
+ [ ! -f "$SETTINGS_FILE" ] && exit 0
47
+
48
+ # --- PreToolUse: backup before modification ---
49
+ if [[ "$HOOK_EVENT" == "PreToolUse" ]]; then
50
+ TIMESTAMP=$(date +%Y%m%d-%H%M%S)
51
+ BASENAME=$(basename "$SETTINGS_FILE" .json)
52
+ BACKUP="$BACKUP_DIR/${BASENAME}-pre-model-${TIMESTAMP}.json"
53
+ cp "$SETTINGS_FILE" "$BACKUP"
54
+
55
+ # Count hooks in current settings
56
+ HOOK_COUNT=$(jq '[.hooks // {} | to_entries[] | .value[] | .hooks // [] | length] | add // 0' "$SETTINGS_FILE" 2>/dev/null || echo 0)
57
+ if [ "$HOOK_COUNT" -gt 0 ]; then
58
+ echo "Settings backup created: $BACKUP ($HOOK_COUNT hooks preserved)" >&2
59
+ # Store hook count for PostToolUse comparison
60
+ echo "$HOOK_COUNT" > "/tmp/cc-settings-hook-count-pre"
61
+ fi
62
+ exit 0
63
+ fi
64
+
65
+ # --- PostToolUse: verify hooks survived ---
66
+ if [[ "$HOOK_EVENT" == "PostToolUse" ]]; then
67
+ PRE_COUNT_FILE="/tmp/cc-settings-hook-count-pre"
68
+ [ ! -f "$PRE_COUNT_FILE" ] && exit 0
69
+
70
+ PRE_COUNT=$(cat "$PRE_COUNT_FILE" 2>/dev/null || echo 0)
71
+ rm -f "$PRE_COUNT_FILE"
72
+
73
+ [ "$PRE_COUNT" -eq 0 ] && exit 0
74
+
75
+ # Count hooks after modification
76
+ POST_COUNT=$(jq '[.hooks // {} | to_entries[] | .value[] | .hooks // [] | length] | add // 0' "$SETTINGS_FILE" 2>/dev/null || echo 0)
77
+
78
+ if [ "$POST_COUNT" -lt "$PRE_COUNT" ]; then
79
+ LATEST_BACKUP=$(ls -t "$BACKUP_DIR"/settings-pre-model-*.json 2>/dev/null | head -1)
80
+ echo "WARNING: Hook count dropped from $PRE_COUNT to $POST_COUNT after settings modification!" >&2
81
+ echo " This typically happens when /model rewrites settings.json." >&2
82
+ if [ -n "$LATEST_BACKUP" ]; then
83
+ echo " Restore hooks: jq -s '.[0] * {hooks: .[1].hooks}' '$SETTINGS_FILE' '$LATEST_BACKUP' > /tmp/merged.json && mv /tmp/merged.json '$SETTINGS_FILE'" >&2
84
+ fi
85
+ fi
86
+ exit 0
87
+ fi
88
+
89
+ exit 0
@@ -0,0 +1,97 @@
1
+ #!/bin/bash
2
+ # shell-config-truncation-guard.sh — Block writes that would truncate shell config files
3
+ #
4
+ # Solves: Claude Code installer auto-update truncating ~/.bash_profile and
5
+ # ~/.zshrc to 0 bytes, destroying all user shell configuration (#49615).
6
+ # Also catches Claude itself attempting to overwrite these files with
7
+ # minimal or empty content.
8
+ #
9
+ # How it works: PreToolUse hook intercepts Write/Bash operations targeting
10
+ # shell config files. If the new content would be significantly shorter
11
+ # than the existing file (>60% reduction), the operation is blocked.
12
+ # Empty/near-empty writes are always blocked.
13
+ #
14
+ # TRIGGER: PreToolUse
15
+ # MATCHER: "Bash|Write"
16
+ #
17
+ # Usage:
18
+ # {
19
+ # "hooks": {
20
+ # "PreToolUse": [{
21
+ # "matcher": "Bash|Write",
22
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/shell-config-truncation-guard.sh" }]
23
+ # }]
24
+ # }
25
+ # }
26
+
27
+ INPUT=$(cat)
28
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
29
+
30
+ # Shell config files to protect
31
+ PROTECTED_FILES=(
32
+ "$HOME/.bashrc"
33
+ "$HOME/.bash_profile"
34
+ "$HOME/.zshrc"
35
+ "$HOME/.zprofile"
36
+ "$HOME/.profile"
37
+ "$HOME/.zshenv"
38
+ )
39
+
40
+ check_file_truncation() {
41
+ local target_file="$1"
42
+ local new_size="$2"
43
+
44
+ for protected in "${PROTECTED_FILES[@]}"; do
45
+ if [ "$target_file" = "$protected" ] || [ "$(realpath "$target_file" 2>/dev/null)" = "$(realpath "$protected" 2>/dev/null)" ]; then
46
+ if [ ! -f "$protected" ]; then
47
+ return 0
48
+ fi
49
+ local current_size
50
+ current_size=$(wc -c < "$protected" 2>/dev/null || echo 0)
51
+
52
+ # Block if writing 0 bytes or near-empty (< 10 bytes)
53
+ if [ "$new_size" -lt 10 ] && [ "$current_size" -gt 50 ]; then
54
+ echo "BLOCKED: Attempted to truncate $protected to $new_size bytes (current: $current_size bytes)" >&2
55
+ echo "This would destroy your shell configuration. See: github.com/anthropics/claude-code/issues/49615" >&2
56
+ exit 2
57
+ fi
58
+
59
+ # Block if >60% size reduction
60
+ if [ "$current_size" -gt 100 ]; then
61
+ local threshold=$((current_size * 40 / 100))
62
+ if [ "$new_size" -lt "$threshold" ]; then
63
+ echo "BLOCKED: Write to $protected would reduce size by >60% ($current_size → $new_size bytes)" >&2
64
+ echo "If intentional, back up first: cp $protected ${protected}.bak" >&2
65
+ exit 2
66
+ fi
67
+ fi
68
+ return 0
69
+ fi
70
+ done
71
+ return 0
72
+ }
73
+
74
+ if [ "$TOOL" = "Write" ]; then
75
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
76
+ CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty' 2>/dev/null)
77
+ if [ -n "$FILE_PATH" ]; then
78
+ NEW_SIZE=${#CONTENT}
79
+ check_file_truncation "$FILE_PATH" "$NEW_SIZE"
80
+ fi
81
+ elif [ "$TOOL" = "Bash" ]; then
82
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
83
+ [ -z "$COMMAND" ] && exit 0
84
+
85
+ # Detect redirect truncation: > ~/.bashrc, >~/.zshrc, etc.
86
+ for protected in "${PROTECTED_FILES[@]}"; do
87
+ base=$(basename "$protected")
88
+ # Match: > file, >file, truncate file, : > file, echo "" > file
89
+ if echo "$COMMAND" | grep -qE "(^|[;&|])\s*(>|truncate\s+-s\s*0|:\s*>)\s*~?(/[^;]*)?${base}"; then
90
+ echo "BLOCKED: Command would truncate $protected" >&2
91
+ echo "If intentional, back up first: cp $protected ${protected}.bak" >&2
92
+ exit 2
93
+ fi
94
+ done
95
+ fi
96
+
97
+ exit 0
@@ -34,8 +34,8 @@ if echo "$COMMAND" | grep -qE '(sh|bash|zsh|dash)\s+-c\s+'; then
34
34
  fi
35
35
 
36
36
  # === Check 2: Python one-liners ===
37
- if echo "$COMMAND" | grep -qE 'python[23]?\s+-c\s+'; then
38
- INNER=$(echo "$COMMAND" | sed -E "s/.*python[23]?\s+-c\s+['\"]?//" | sed "s/['\"]?\s*$//")
37
+ if echo "$COMMAND" | grep -qE 'python[23]?(\.[0-9]+)?\s+-c\s+'; then
38
+ INNER=$(echo "$COMMAND" | sed -E "s/.*python[23]?(\.[0-9]+)?\s+-c\s+['\"]?//" | sed "s/['\"]?\s*$//")
39
39
  if echo "$INNER" | grep -qiE "os\.system\(.*($DESTRUCT_PATTERN)|subprocess\.(run|call|Popen)\(.*($DESTRUCT_PATTERN)|shutil\.rmtree\s*\(\s*['\"/~]"; then
40
40
  echo "BLOCKED: Destructive command in Python one-liner" >&2
41
41
  exit 2
@@ -69,9 +69,9 @@ if echo "$COMMAND" | grep -qE '(sh|bash)\s+-c\s+.*(sh|bash)\s+-c'; then
69
69
  fi
70
70
 
71
71
  # === Check 6: Pipe to shell (echo "rm -rf /" | sh) ===
72
- if echo "$COMMAND" | grep -qE '\|\s*(sh|bash|zsh)\s*$'; then
72
+ if echo "$COMMAND" | grep -qE '\|\s*(sh|bash|zsh)(\s|$)'; then
73
73
  # Extract the piped content
74
- PIPED=$(echo "$COMMAND" | sed -E 's/\s*\|\s*(sh|bash|zsh)\s*$//')
74
+ PIPED=$(echo "$COMMAND" | sed -E 's/\s*\|\s*(sh|bash|zsh)(\s.*)?$//')
75
75
  if echo "$PIPED" | grep -qE "$DESTRUCT_PATTERN"; then
76
76
  echo "BLOCKED: Destructive command piped to shell" >&2
77
77
  exit 2
@@ -0,0 +1,34 @@
1
+ #!/bin/bash
2
+ # subagent-spawn-rate-monitor.sh — サブエージェントの過剰spawn検知
3
+ # Why: サブエージェントは毎回spawnされるたびに~4.7Kトークンがcache_creation
4
+ # (1.25xコスト)として課金される。spawn-heavyなワークフローでは線形に増大し、
5
+ # ユーザーが気づかないうちにquotaを消耗する (#50213, #46968)
6
+ # Event: PreToolUse MATCHER: Agent
7
+ # Action: 短時間に多数のAgent spawnがあれば警告
8
+
9
+ COUNTER_FILE="/tmp/cc-subagent-spawn-counter"
10
+ WINDOW_FILE="/tmp/cc-subagent-spawn-window"
11
+ THRESHOLD=5 # この回数を超えたら警告
12
+ WINDOW_SECS=300 # 5分間のウィンドウ
13
+
14
+ NOW=$(date +%s)
15
+ WINDOW_START=$(cat "$WINDOW_FILE" 2>/dev/null || echo "$NOW")
16
+ COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo "0")
17
+
18
+ # ウィンドウ期限切れならリセット
19
+ ELAPSED=$((NOW - WINDOW_START))
20
+ if [ "$ELAPSED" -gt "$WINDOW_SECS" ]; then
21
+ COUNT=0
22
+ echo "$NOW" > "$WINDOW_FILE"
23
+ fi
24
+
25
+ COUNT=$((COUNT + 1))
26
+ echo "$COUNT" > "$COUNTER_FILE"
27
+
28
+ if [ "$COUNT" -gt "$THRESHOLD" ]; then
29
+ echo "⚠ HIGH SUBAGENT SPAWN RATE: $COUNT agents spawned in ${ELAPSED}s" >&2
30
+ echo "Each spawn costs ~4.7K tokens at 1.25x rate (no cache_control)." >&2
31
+ echo "Consider batching tasks or using fewer parallel agents. See: #50213" >&2
32
+ fi
33
+
34
+ exit 0
@@ -0,0 +1,44 @@
1
+ #!/bin/bash
2
+ # subcommand-chain-guard.sh — Block commands with excessive subcommand chains
3
+ #
4
+ # Solves: Claude Code silently ignores deny rules when a command contains
5
+ # 50+ subcommands (MAX_SUBCOMMANDS_FOR_SECURITY_CHECK = 50).
6
+ # Attackers chain 50 no-op "true" commands before a dangerous command
7
+ # to bypass all security checks. (Adversa AI / CVE disclosure, April 2026)
8
+ #
9
+ # How it works: Counts semicolon-separated and &&/|| chained subcommands.
10
+ # If the count exceeds a threshold (default: 20), blocks execution.
11
+ # This catches the exploit well before the 50-command limit.
12
+ #
13
+ # TRIGGER: PreToolUse
14
+ # MATCHER: "Bash"
15
+
16
+ set -euo pipefail
17
+
18
+ THRESHOLD=${CC_SUBCOMMAND_LIMIT:-20}
19
+
20
+ INPUT=$(cat)
21
+ CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
22
+ [ -z "$CMD" ] && exit 0
23
+
24
+ # Count subcommands: split on ; && ||
25
+ # Use tr to normalize separators, then count
26
+ SUBCOMMAND_COUNT=$(echo "$CMD" | tr ';' '\n' | tr '&' '\n' | tr '|' '\n' | grep -c '[^ ]' 2>/dev/null || echo 1)
27
+
28
+ if [ "$SUBCOMMAND_COUNT" -gt "$THRESHOLD" ]; then
29
+ echo "BLOCKED: Command contains $SUBCOMMAND_COUNT subcommands (limit: $THRESHOLD)." >&2
30
+ echo " Claude Code ignores deny rules after 50 subcommands (CVE disclosure)." >&2
31
+ echo " This hook blocks at $THRESHOLD to prevent security bypass." >&2
32
+ echo " Override: CC_SUBCOMMAND_LIMIT=100 (not recommended)" >&2
33
+ exit 2
34
+ fi
35
+
36
+ # Also detect the specific attack pattern: many "true" or ":" no-ops
37
+ NOOP_COUNT=$(echo "$CMD" | grep -oE '\btrue\b|^:|;\s*:' | wc -l 2>/dev/null || echo 0)
38
+ if [ "$NOOP_COUNT" -gt 10 ]; then
39
+ echo "BLOCKED: Suspicious pattern — $NOOP_COUNT no-op commands detected." >&2
40
+ echo " This resembles the subcommand-chain attack (50x true + dangerous cmd)." >&2
41
+ exit 2
42
+ fi
43
+
44
+ exit 0
@@ -0,0 +1,100 @@
1
+ #!/bin/bash
2
+ # system-dir-protection-guard.sh — Block destructive operations on system directories
3
+ #
4
+ # Solves: Agent deleting or moving system-level directories in auto mode
5
+ # - #49554: Auto mode approved deletion of system directories
6
+ # - #49129: rm -rf on /home subdirectories causing 50GB data loss
7
+ #
8
+ # Difference from existing hooks:
9
+ # rm-safety-net.sh: Blocks rm on critical paths, but only rm commands
10
+ # home-critical-bash-guard.sh: Protects ~/dotfiles only
11
+ # This hook: Blocks rm, mv, chmod -R, chown -R on ALL system directories
12
+ # including /home/*, /usr, /etc, /var, /opt, /root, /boot, /srv
13
+ # Also blocks mv of system dirs (not covered by rm-safety-net)
14
+ #
15
+ # TRIGGER: PreToolUse MATCHER: "Bash"
16
+
17
+ set -euo pipefail
18
+
19
+ INPUT=$(cat)
20
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
21
+ [ -z "$COMMAND" ] && exit 0
22
+
23
+ # Check if a path is a protected system directory
24
+ is_system_dir() {
25
+ local path="$1"
26
+ # Remove trailing slash
27
+ path="${path%/}"
28
+
29
+ # Expand ~ to $HOME
30
+ if [[ "$path" == "~"* ]]; then
31
+ path="${HOME}${path#\~}"
32
+ fi
33
+
34
+ # Top-level system directories
35
+ case "$path" in
36
+ /|/home|/etc|/usr|/var|/opt|/root|/boot|/srv|/sys|/proc)
37
+ return 0 ;;
38
+ esac
39
+
40
+ # /home/<username> (1 level deep)
41
+ if echo "$path" | grep -qE '^/home/[^/]+$'; then
42
+ return 0
43
+ fi
44
+
45
+ # System subdirectories (e.g., /etc/nginx, /usr/local, /var/lib)
46
+ if echo "$path" | grep -qE '^/(etc|usr|var|opt|root|boot|srv|sys|proc)/'; then
47
+ return 0
48
+ fi
49
+
50
+ # Critical home directories: ~/.ssh, ~/.config, ~/.local, ~/.gnupg, ~/.cache
51
+ if echo "$path" | grep -qE "^${HOME}/\.(ssh|config|local|gnupg|cache)(/[^/]*)?$"; then
52
+ return 0
53
+ fi
54
+
55
+ return 1
56
+ }
57
+
58
+ # --- rm / unlink on system directories ---
59
+ if echo "$COMMAND" | grep -qE '^\s*(sudo\s+)?(rm|unlink)\s'; then
60
+ # Extract targets after rm and flags
61
+ TARGETS=$(echo "$COMMAND" | grep -oP '(rm|unlink)\s+(-[a-zA-Z]+\s+)*\K[^;|&]+' 2>/dev/null || true)
62
+ for target in $TARGETS; do
63
+ if is_system_dir "$target"; then
64
+ echo "BLOCKED: Destructive operation on system directory: $target" >&2
65
+ echo "Command: $COMMAND" >&2
66
+ echo "" >&2
67
+ echo "System directories must not be deleted. Use specific file paths instead." >&2
68
+ echo "See: https://github.com/anthropics/claude-code/issues/49554" >&2
69
+ exit 2
70
+ fi
71
+ done
72
+ fi
73
+
74
+ # --- mv (moving system directories) ---
75
+ if echo "$COMMAND" | grep -qE '^\s*(sudo\s+)?mv\s'; then
76
+ # Get the source of the mv (first non-flag argument)
77
+ MV_SOURCE=$(echo "$COMMAND" | grep -oP 'mv\s+(-[a-zA-Z]+\s+)*\K\S+' 2>/dev/null || true)
78
+ if is_system_dir "$MV_SOURCE"; then
79
+ echo "BLOCKED: Moving system directory: $MV_SOURCE" >&2
80
+ echo "Command: $COMMAND" >&2
81
+ echo "" >&2
82
+ echo "System directories must not be moved." >&2
83
+ echo "See: https://github.com/anthropics/claude-code/issues/49554" >&2
84
+ exit 2
85
+ fi
86
+ fi
87
+
88
+ # --- chmod -R / chown -R on system directories ---
89
+ if echo "$COMMAND" | grep -qE '^\s*(sudo\s+)?(chmod|chown)\s+.*-R'; then
90
+ TARGETS=$(echo "$COMMAND" | grep -oP '(chmod|chown)\s+[^;|&]+' 2>/dev/null | awk '{print $NF}' || true)
91
+ for target in $TARGETS; do
92
+ if is_system_dir "$target"; then
93
+ echo "BLOCKED: Recursive permission change on system directory: $target" >&2
94
+ echo "Command: $COMMAND" >&2
95
+ exit 2
96
+ fi
97
+ done
98
+ fi
99
+
100
+ exit 0
@@ -0,0 +1,25 @@
1
+ #!/bin/bash
2
+ # thinking-display-enforcer.sh — Opus 4.7 thinking summaries消失を検知
3
+ # Why: Opus 4.7でthinking displayのデフォルトがsummarized→omittedに変更された(#49268, 17👍)
4
+ # セッション開始時にモデルを確認し、Opus 4.7でthinkingが非表示の場合に警告する
5
+ # Event: Notification (セッション開始時に確認)
6
+ # Fix: claude --thinking-display summarized
7
+
8
+ # チェック頻度制御(100回に1回)
9
+ COUNTER_FILE="/tmp/thinking-display-check-counter"
10
+ COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo "0")
11
+ COUNT=$((COUNT + 1))
12
+ echo "$COUNT" > "$COUNTER_FILE"
13
+ [ $((COUNT % 100)) -ne 1 ] && exit 0
14
+
15
+ # settings.jsonにthinking display設定があるか確認
16
+ SETTINGS_FILE="${HOME}/.claude/settings.json"
17
+ if [ -f "$SETTINGS_FILE" ]; then
18
+ HAS_THINKING=$(grep -c "showThinkingSummaries\|thinkingDisplay" "$SETTINGS_FILE" 2>/dev/null || echo "0")
19
+ if [ "$HAS_THINKING" -eq 0 ]; then
20
+ echo "INFO: Opus 4.7ではthinking summariesがデフォルトで非表示です。" >&2
21
+ echo "修正: claude --thinking-display summarized で起動するか、settings.jsonに設定を追加してください。" >&2
22
+ echo "詳細: https://github.com/anthropics/claude-code/issues/49268" >&2
23
+ fi
24
+ fi
25
+ exit 0
@@ -0,0 +1,59 @@
1
+ #!/bin/bash
2
+ # tool-retry-budget-guard.sh — Stop Claude from wasting tokens on repeated failures
3
+ #
4
+ # Solves: #50986 — Claude fails simple UI change after 10+ attempts, wastes entire
5
+ # token budget. Also prevents retry spirals on any file.
6
+ #
7
+ # Tracks consecutive tool calls (Edit, Write, Bash) targeting the same file.
8
+ # After 5 attempts, blocks further edits and forces a different approach.
9
+ #
10
+ # TRIGGER: PreToolUse MATCHER: "Edit|Write"
11
+
12
+ INPUT=$(cat)
13
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
14
+
15
+ case "$TOOL" in
16
+ Edit|Write) ;;
17
+ *) exit 0 ;;
18
+ esac
19
+
20
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
21
+ [ -z "$FILE" ] && exit 0
22
+
23
+ # Use file hash as state key
24
+ HASH=$(echo "$FILE" | md5sum | cut -c1-8)
25
+ STATE_DIR="/tmp/.cc-retry-budget"
26
+ mkdir -p "$STATE_DIR"
27
+ STATE_FILE="$STATE_DIR/$HASH"
28
+
29
+ # Track attempt count and timestamp
30
+ NOW=$(date +%s)
31
+ COUNT=1
32
+
33
+ if [ -f "$STATE_FILE" ]; then
34
+ PREV_TIME=$(head -1 "$STATE_FILE" 2>/dev/null || echo "0")
35
+ PREV_COUNT=$(tail -1 "$STATE_FILE" 2>/dev/null || echo "0")
36
+
37
+ # Reset if more than 5 minutes since last attempt (new task)
38
+ ELAPSED=$(( NOW - PREV_TIME ))
39
+ if [ "$ELAPSED" -lt 300 ]; then
40
+ COUNT=$(( PREV_COUNT + 1 ))
41
+ fi
42
+ fi
43
+
44
+ printf '%s\n%s\n' "$NOW" "$COUNT" > "$STATE_FILE"
45
+
46
+ if [ "$COUNT" -ge 7 ]; then
47
+ echo "BLOCKED: You've attempted to modify $(basename "$FILE") $COUNT times in the last 5 minutes." >&2
48
+ echo " This pattern wastes tokens. Stop and try a completely different approach:" >&2
49
+ echo " 1. Read the file first to understand current state" >&2
50
+ echo " 2. Use a different strategy (smaller change, different tool)" >&2
51
+ echo " 3. If stuck, explain the problem and ask for help" >&2
52
+ rm -f "$STATE_FILE"
53
+ exit 2
54
+ elif [ "$COUNT" -ge 5 ]; then
55
+ echo "WARNING: $COUNT consecutive edits to $(basename "$FILE") — approaching retry limit (7)." >&2
56
+ echo " Consider reading the file to verify your assumptions before the next attempt." >&2
57
+ fi
58
+
59
+ exit 0