cc-safe-setup 29.6.40 → 29.8.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 (85) 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/compact-circuit-breaker.sh +72 -0
  17. package/examples/context-size-alert.sh +38 -0
  18. package/examples/context-usage-drift-alert.sh +33 -0
  19. package/examples/dangerous-pip-flag-guard.sh +51 -0
  20. package/examples/deny-bypass-detector.sh +143 -0
  21. package/examples/dotenv-read-guard.sh +48 -0
  22. package/examples/dotfile-protection-guard.sh +60 -0
  23. package/examples/effort-tracking-logger.sh +30 -0
  24. package/examples/exploration-budget-guard.sh +77 -0
  25. package/examples/financial-operation-guard.sh +47 -0
  26. package/examples/full-rewrite-detector.sh +63 -0
  27. package/examples/home-critical-bash-guard.sh +56 -0
  28. package/examples/idle-session-cost-alert.sh +36 -0
  29. package/examples/model-version-alert.sh +18 -0
  30. package/examples/model-version-change-alert.sh +31 -0
  31. package/examples/move-delete-sequence-guard.sh +92 -0
  32. package/examples/pii-upload-guard.sh +72 -0
  33. package/examples/pr-duplicate-guard.sh +14 -0
  34. package/examples/production-port-kill-guard.sh +60 -0
  35. package/examples/quota-reset-cycle-monitor.sh +30 -0
  36. package/examples/repo-visibility-guard.sh +33 -0
  37. package/examples/sandbox-relative-path-audit.sh +51 -0
  38. package/examples/session-agent-cost-limiter.sh +43 -0
  39. package/examples/session-cost-alert.sh +62 -0
  40. package/examples/session-memory-watchdog.sh +9 -0
  41. package/examples/settings-integrity-monitor.sh +55 -0
  42. package/examples/settings-json-model-guard.sh +89 -0
  43. package/examples/shell-config-truncation-guard.sh +97 -0
  44. package/examples/shell-wrapper-guard.sh +4 -4
  45. package/examples/subagent-spawn-rate-monitor.sh +34 -0
  46. package/examples/subcommand-chain-guard.sh +44 -0
  47. package/examples/system-dir-protection-guard.sh +100 -0
  48. package/examples/thinking-display-enforcer.sh +25 -0
  49. package/examples/thinking-stall-detector.sh +61 -0
  50. package/examples/tool-retry-budget-guard.sh +59 -0
  51. package/examples/worktree-branch-pollution-detector.sh +35 -0
  52. package/examples/worktree-create-log.sh +6 -0
  53. package/examples/worktree-hook-linker.sh +72 -0
  54. package/examples/worktree-remove-uncommitted-guard.sh +20 -0
  55. package/hooks/hooks.json +60 -0
  56. package/index.mjs +92 -6
  57. package/memory/market-anthropic-japan-strategy-2026-04-13.md +4 -0
  58. package/package.json +2 -2
  59. package/plugins/credential-guard/.claude-plugin/plugin.json +58 -0
  60. package/plugins/git-protection/.claude-plugin/plugin.json +58 -0
  61. package/plugins/safety-essentials/.claude-plugin/plugin.json +58 -0
  62. package/plugins/token-guard/.claude-plugin/plugin.json +51 -0
  63. package/skills/safety-setup/SKILL.md +47 -0
  64. package/tests/dotenv-read-guard.test.sh +65 -0
  65. package/tests/test-auto-mode-safety-enforcer.sh +55 -0
  66. package/tests/test-case-insensitive-path-guard.sh +78 -0
  67. package/tests/test-compact-circuit-breaker.sh +134 -0
  68. package/tests/test-context-usage-drift-alert.sh +52 -0
  69. package/tests/test-dangerous-pip-flag-guard.sh +56 -0
  70. package/tests/test-dotfile-protection-guard.sh +68 -0
  71. package/tests/test-effort-tracking-logger.sh +55 -0
  72. package/tests/test-exploration-budget-guard.sh +164 -0
  73. package/tests/test-financial-operation-guard.sh +59 -0
  74. package/tests/test-home-critical-bash-guard.sh +59 -0
  75. package/tests/test-model-version-change-alert.sh +55 -0
  76. package/tests/test-move-delete-sequence-guard.sh +63 -0
  77. package/tests/test-pr-duplicate-guard.sh +29 -0
  78. package/tests/test-quota-reset-cycle-monitor.sh +52 -0
  79. package/tests/test-shell-config-truncation-guard.sh +104 -0
  80. package/tests/test-subagent-spawn-rate-monitor.sh +43 -0
  81. package/tests/test-system-dir-protection-guard.sh +81 -0
  82. package/tests/test-thinking-stall-detector.sh +151 -0
  83. package/tests/test-tool-retry-budget-guard.sh +75 -0
  84. package/tests/test-worktree-branch-pollution-detector.sh +50 -0
  85. package/tests/test-worktree-lifecycle-hooks.sh +29 -0
@@ -0,0 +1,60 @@
1
+ #!/bin/bash
2
+ # dotfile-protection-guard.sh — Block writes to critical dotfiles
3
+ #
4
+ # Solves: Claude Code modifying or truncating critical user dotfiles
5
+ # - #49615: Installer auto-update zeroed ~/.bash_profile and ~/.zshrc
6
+ # - #49539: ~/.git-credentials PATs deleted without confirmation
7
+ # - #49554: auto mode approved ~/.ssh deletion
8
+ #
9
+ # What it blocks (Write/Edit tool):
10
+ # ~/.bash_profile, ~/.bashrc, ~/.zshrc, ~/.profile
11
+ # ~/.ssh/*, ~/.git-credentials, ~/.gitconfig
12
+ # ~/.gnupg/*, ~/.npmrc (may contain auth tokens)
13
+ # ~/.aws/credentials, ~/.config/gh/hosts.yml
14
+ #
15
+ # What it allows:
16
+ # Files in project directories (not under ~/ root)
17
+ # ~/.claude/* (Claude Code's own config)
18
+ #
19
+ # TRIGGER: PreToolUse MATCHER: "Write|Edit"
20
+
21
+ set -euo pipefail
22
+
23
+ INPUT=$(cat)
24
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
25
+ [ -z "$FILE" ] && exit 0
26
+
27
+ # Expand ~ to actual home directory
28
+ HOME_DIR="$HOME"
29
+ RESOLVED=$(echo "$FILE" | sed "s|^~|$HOME_DIR|")
30
+
31
+ # Allow Claude Code's own config
32
+ if echo "$RESOLVED" | grep -qE "^${HOME_DIR}/\.claude(/|$)"; then
33
+ exit 0
34
+ fi
35
+
36
+ # Critical dotfiles — block any modification
37
+ CRITICAL_PATTERNS=(
38
+ "^${HOME_DIR}/\.(bash_profile|bashrc|zshrc|zshenv|profile|login|logout)$"
39
+ "^${HOME_DIR}/\.ssh(/|$)"
40
+ "^${HOME_DIR}/\.git-credentials$"
41
+ "^${HOME_DIR}/\.gitconfig$"
42
+ "^${HOME_DIR}/\.gnupg(/|$)"
43
+ "^${HOME_DIR}/\.npmrc$"
44
+ "^${HOME_DIR}/\.aws/(credentials|config)$"
45
+ "^${HOME_DIR}/\.config/gh/hosts\.yml$"
46
+ "^${HOME_DIR}/\.netrc$"
47
+ "^${HOME_DIR}/\.docker/config\.json$"
48
+ "^${HOME_DIR}/\.kube/config$"
49
+ )
50
+
51
+ for PATTERN in "${CRITICAL_PATTERNS[@]}"; do
52
+ if echo "$RESOLVED" | grep -qE "$PATTERN"; then
53
+ echo "BLOCKED: Modifying critical dotfile: $FILE" >&2
54
+ echo "This file contains shell config or credentials that should not be altered by AI." >&2
55
+ echo "If you need to modify this file, do it manually." >&2
56
+ exit 2
57
+ fi
58
+ done
59
+
60
+ exit 0
@@ -0,0 +1,30 @@
1
+ #!/bin/bash
2
+ # effort-tracking-logger.sh — ツール使用ごとのエフォートログを記録
3
+ # Why: OTEL互換のエフォート追跡への要望が急増 (#49893, 18👍)。
4
+ # 公式対応を待たずに、hookでツール呼び出しごとのログを残す。
5
+ # コスト分析・セッション振り返り・ボトルネック特定に使える。
6
+ # Event: PostToolUse MATCHER: ""
7
+ # Output: ~/.claude/effort-log/YYYY-MM-DD.jsonl
8
+
9
+ LOG_DIR="${HOME}/.claude/effort-log"
10
+ mkdir -p "$LOG_DIR"
11
+ LOG_FILE="$LOG_DIR/$(date +%Y-%m-%d).jsonl"
12
+
13
+ # stdinからツール情報を取得
14
+ TOOL_INPUT=$(cat)
15
+ TOOL_NAME=$(echo "$TOOL_INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_name','unknown'))" 2>/dev/null)
16
+ TOOL_STATUS=$(echo "$TOOL_INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('was_error','false'))" 2>/dev/null)
17
+
18
+ # JSONLログに追記
19
+ python3 -c "
20
+ import json, datetime
21
+ entry = {
22
+ 'timestamp': datetime.datetime.now().isoformat(),
23
+ 'tool': '$TOOL_NAME',
24
+ 'error': '$TOOL_STATUS' == 'true',
25
+ 'session_pid': $(echo $$)
26
+ }
27
+ print(json.dumps(entry))
28
+ " >> "$LOG_FILE"
29
+
30
+ exit 0
@@ -0,0 +1,77 @@
1
+ #!/bin/bash
2
+ # exploration-budget-guard.sh — Stop excessive exploration from draining tokens
3
+ #
4
+ # Solves: #51054 — Claude wastes 20% of weekly allowance exploring files
5
+ # on a simple task instead of acting. Read/Glob/Grep loops with
6
+ # no Edit/Write progress.
7
+ #
8
+ # Tracks read-only tool calls (Read, Glob, Grep) per session. After 25
9
+ # consecutive reads without a write, warns. After 40, blocks.
10
+ # Resets when an Edit/Write/Bash(write) occurs.
11
+ #
12
+ # TRIGGER: PreToolUse MATCHER: "Read|Glob|Grep|Edit|Write"
13
+
14
+ INPUT=$(cat)
15
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
16
+
17
+ STATE_DIR="/tmp/.cc-exploration-budget"
18
+ mkdir -p "$STATE_DIR"
19
+
20
+ # Session-scoped state file (keyed by parent PID)
21
+ STATE_FILE="$STATE_DIR/session-$$"
22
+ # Fall back to a shared file if $$ changes between hook calls
23
+ STATE_FILE="$STATE_DIR/exploration-count"
24
+
25
+ NOW=$(date +%s)
26
+
27
+ case "$TOOL" in
28
+ Edit|Write)
29
+ # Write operation = progress. Reset exploration counter.
30
+ echo "0 $NOW" > "$STATE_FILE"
31
+ exit 0
32
+ ;;
33
+ Read|Glob|Grep)
34
+ # Read operation = exploration. Increment counter.
35
+ ;;
36
+ *)
37
+ exit 0
38
+ ;;
39
+ esac
40
+
41
+ # Read current count
42
+ COUNT=0
43
+ LAST_TIME=0
44
+ if [ -f "$STATE_FILE" ]; then
45
+ read -r COUNT LAST_TIME < "$STATE_FILE" 2>/dev/null || true
46
+ # Reset if more than 10 minutes since last call (new task)
47
+ ELAPSED=$(( NOW - LAST_TIME ))
48
+ if [ "$ELAPSED" -gt 600 ]; then
49
+ COUNT=0
50
+ fi
51
+ fi
52
+
53
+ COUNT=$(( COUNT + 1 ))
54
+ echo "$COUNT $NOW" > "$STATE_FILE"
55
+
56
+ WARN_THRESHOLD=25
57
+ BLOCK_THRESHOLD=40
58
+
59
+ if [ "$COUNT" -ge "$BLOCK_THRESHOLD" ]; then
60
+ echo "BLOCKED: $COUNT consecutive read operations without writing anything." >&2
61
+ echo " You're stuck in an exploration loop — this wastes tokens." >&2
62
+ echo " Take action NOW:" >&2
63
+ echo " 1. Write your solution based on what you've already read" >&2
64
+ echo " 2. If unsure, make a small change and test it" >&2
65
+ echo " 3. Ask the user for clarification instead of reading more files" >&2
66
+ echo "" >&2
67
+ echo " Exploration budget: $COUNT/$BLOCK_THRESHOLD (EXCEEDED)" >&2
68
+ exit 2
69
+ fi
70
+
71
+ if [ "$COUNT" -ge "$WARN_THRESHOLD" ]; then
72
+ echo "WARNING: $COUNT consecutive reads without any write." >&2
73
+ echo " You may be over-exploring. Consider acting on what you know." >&2
74
+ echo " Budget: $COUNT/$BLOCK_THRESHOLD reads before block." >&2
75
+ fi
76
+
77
+ exit 0
@@ -0,0 +1,47 @@
1
+ #!/bin/bash
2
+ # financial-operation-guard.sh — Block unauthorized financial operations
3
+ #
4
+ # Solves: Claude Code transferred $1,446 from spot to futures without
5
+ # authorization when told to "close a position". Financial APIs
6
+ # should never be called without explicit per-transaction approval. (#46828)
7
+ #
8
+ # How it works: Detects commands that interact with exchange APIs,
9
+ # wallet transfers, payment processors, or any operation involving
10
+ # fund movement. Blocks with exit 2 and requires explicit user
11
+ # confirmation.
12
+ #
13
+ # TRIGGER: PreToolUse
14
+ # MATCHER: "Bash"
15
+
16
+ set -euo pipefail
17
+
18
+ INPUT=$(cat)
19
+ CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
20
+ [ -z "$CMD" ] && exit 0
21
+
22
+ # Detect financial API calls
23
+ # Exchange APIs
24
+ if echo "$CMD" | grep -qiE '(binance|bitget|bybit|kraken|coinbase|ftx|okx|kucoin|gate\.io|huobi).*(transfer|withdraw|swap|order|trade|margin|futures|spot|deposit)'; then
25
+ echo "BLOCKED: Financial exchange operation detected." >&2
26
+ echo " Command: $(echo "$CMD" | head -c 200)" >&2
27
+ echo " Fund transfers require explicit user approval for EACH transaction." >&2
28
+ exit 2
29
+ fi
30
+
31
+ # Generic payment/transfer patterns
32
+ if echo "$CMD" | grep -qiE '(transfer|withdraw|send|swap|bridge)[^a-z].*\b(usdt|usdc|eth|btc|sol|bnb|funds|balance|wallet)\b'; then
33
+ echo "BLOCKED: Cryptocurrency transfer operation detected." >&2
34
+ echo " Command: $(echo "$CMD" | head -c 200)" >&2
35
+ echo " Wallet/fund operations require explicit user approval." >&2
36
+ exit 2
37
+ fi
38
+
39
+ # Payment processor APIs
40
+ if echo "$CMD" | grep -qiE 'stripe.*(charges?|transfers?|payouts?)|paypal.*(payments?|send|transfers?)|square.*(payments?|charges?)'; then
41
+ echo "BLOCKED: Payment processor operation detected." >&2
42
+ echo " Command: $(echo "$CMD" | head -c 200)" >&2
43
+ echo " Payment operations require explicit user approval." >&2
44
+ exit 2
45
+ fi
46
+
47
+ exit 0
@@ -0,0 +1,63 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # full-rewrite-detector.sh — Warn on full-file rewrites
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # Claude Code sometimes rewrites entire files when a small edit
7
+ # would suffice. AMD's analysis of 6,852 sessions found this
8
+ # pattern increasing over time — a sign of quality degradation.
9
+ # This hook detects when >80% of a file's lines were changed
10
+ # and warns the user.
11
+ #
12
+ # TRIGGER: PostToolUse
13
+ # MATCHER: "Write"
14
+ #
15
+ # HOW IT WORKS:
16
+ # After a Write operation, checks git diff for the target file.
17
+ # If the ratio of changed lines to total lines exceeds the
18
+ # threshold (default 80%), emits a warning.
19
+ #
20
+ # CONFIGURATION:
21
+ # CC_REWRITE_THRESHOLD=80 — percentage threshold (default 80)
22
+ #
23
+ # NOTE: Only works in git repositories. No-op outside git repos.
24
+ # ================================================================
25
+
26
+ INPUT=$(cat)
27
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
28
+
29
+ # Skip if no file path
30
+ [ -z "$FILE" ] && exit 0
31
+
32
+ # Skip if not in a git repo
33
+ git rev-parse --is-inside-work-tree &>/dev/null || exit 0
34
+
35
+ # Skip if file doesn't exist (new file creation is fine)
36
+ [ -f "$FILE" ] || exit 0
37
+
38
+ # Get change stats from git
39
+ STATS=$(git diff --numstat -- "$FILE" 2>/dev/null)
40
+ [ -z "$STATS" ] && exit 0
41
+
42
+ ADDED=$(echo "$STATS" | awk '{print $1}')
43
+ DELETED=$(echo "$STATS" | awk '{print $2}')
44
+
45
+ # Handle binary files (git outputs "-" for binary)
46
+ [ "$ADDED" = "-" ] && exit 0
47
+
48
+ CHANGED=$((ADDED + DELETED))
49
+ TOTAL=$(wc -l < "$FILE" 2>/dev/null || echo 0)
50
+
51
+ # Avoid division by zero; skip very small files
52
+ [ "$TOTAL" -lt 5 ] && exit 0
53
+
54
+ RATIO=$((CHANGED * 100 / TOTAL))
55
+ THRESHOLD=${CC_REWRITE_THRESHOLD:-80}
56
+
57
+ if [ "$RATIO" -gt "$THRESHOLD" ]; then
58
+ echo "WARNING: Full rewrite detected on $(basename "$FILE")" >&2
59
+ echo " ${RATIO}% of lines changed (${CHANGED} lines changed / ${TOTAL} total)" >&2
60
+ echo " Consider: was a partial edit sufficient?" >&2
61
+ fi
62
+
63
+ exit 0
@@ -0,0 +1,56 @@
1
+ #!/bin/bash
2
+ # home-critical-bash-guard.sh — Block Bash commands that delete/modify critical home files
3
+ #
4
+ # Solves: Bash commands that rm/mv/truncate critical dotfiles and directories
5
+ # - #49554: auto mode approved ~/.ssh directory deletion
6
+ # - #49539: ~/.git-credentials PATs deleted without confirmation
7
+ # - #49464: ./~ misinterpreted as ~/ leading to home directory deletion attempt
8
+ #
9
+ # Complements dotfile-protection-guard.sh (which covers Write/Edit tools).
10
+ # This hook covers the Bash tool path — rm, mv, truncate, > redirect on dotfiles.
11
+ #
12
+ # TRIGGER: PreToolUse MATCHER: "Bash"
13
+
14
+ set -euo pipefail
15
+
16
+ INPUT=$(cat)
17
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
18
+ [ -z "$COMMAND" ] && exit 0
19
+
20
+ HOME_DIR="$HOME"
21
+
22
+ # Critical paths (regex patterns)
23
+ CRITICAL="(${HOME_DIR}|\~)/\.(bashrc|bash_profile|zshrc|zshenv|profile|login|logout|ssh|git-credentials|gitconfig|gnupg|npmrc|netrc|docker|kube|aws)"
24
+
25
+ # Check for rm/unlink targeting critical paths
26
+ if echo "$COMMAND" | grep -qE "(rm|unlink)\s" && echo "$COMMAND" | grep -qE "$CRITICAL"; then
27
+ echo "BLOCKED: Deleting critical home directory file" >&2
28
+ echo "Command: $COMMAND" >&2
29
+ exit 2
30
+ fi
31
+
32
+ # Check for mv (rename/move) of critical paths
33
+ if echo "$COMMAND" | grep -qE "mv\s" && echo "$COMMAND" | grep -qE "$CRITICAL"; then
34
+ echo "BLOCKED: Moving/renaming critical home directory file" >&2
35
+ echo "Command: $COMMAND" >&2
36
+ exit 2
37
+ fi
38
+
39
+ # Check for truncation via redirect (> ~/.bashrc or : > ~/.bashrc)
40
+ if echo "$COMMAND" | grep -qE ">\s*(${HOME_DIR}|\~)/\."; then
41
+ TARGET=$(echo "$COMMAND" | grep -oP ">\s*\K(${HOME_DIR}|~)/\.[^\s;|&]+")
42
+ if echo "$TARGET" | grep -qE "$CRITICAL"; then
43
+ echo "BLOCKED: Truncating critical home directory file" >&2
44
+ echo "Command: $COMMAND" >&2
45
+ exit 2
46
+ fi
47
+ fi
48
+
49
+ # Check for chmod on critical credential files
50
+ if echo "$COMMAND" | grep -qE "chmod\s.*777" && echo "$COMMAND" | grep -qE "$CRITICAL"; then
51
+ echo "BLOCKED: Removing permissions on critical file" >&2
52
+ echo "Command: $COMMAND" >&2
53
+ exit 2
54
+ fi
55
+
56
+ exit 0
@@ -0,0 +1,36 @@
1
+ #!/bin/bash
2
+ # idle-session-cost-alert.sh — Warn when session has been idle too long
3
+ # An idle session can still consume tokens via background processes.
4
+ # Incident: #50389 — Idle session consumed 18% usage limit over 2 hours with zero user input.
5
+ #
6
+ # This hook runs on Notification events and warns if the session has been
7
+ # idle for more than 5 minutes, reminding the user to exit if not actively working.
8
+ #
9
+ # Hook config (settings.json):
10
+ # {
11
+ # "hooks": {
12
+ # "Notification": [{
13
+ # "matcher": "",
14
+ # "hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/idle-session-cost-alert.sh" }]
15
+ # }]
16
+ # }
17
+ # }
18
+
19
+ INPUT=$(cat)
20
+
21
+ # Track last activity timestamp
22
+ IDLE_FILE="/tmp/claude-idle-tracker-$$"
23
+ CURRENT_TIME=$(date +%s)
24
+
25
+ if [ -f "$IDLE_FILE" ]; then
26
+ LAST_ACTIVE=$(cat "$IDLE_FILE")
27
+ IDLE_SECONDS=$((CURRENT_TIME - LAST_ACTIVE))
28
+
29
+ if [ "$IDLE_SECONDS" -gt 300 ]; then
30
+ IDLE_MINUTES=$((IDLE_SECONDS / 60))
31
+ echo "WARNING: Session idle for ${IDLE_MINUTES} minutes. Idle sessions can consume tokens via background processes (#50389). Consider exiting if not actively working." >&2
32
+ fi
33
+ fi
34
+
35
+ echo "$CURRENT_TIME" > "$IDLE_FILE"
36
+ exit 0
@@ -0,0 +1,18 @@
1
+ INPUT=$(cat)
2
+ COUNTER_FILE="/tmp/.cc-model-check-counter"
3
+ COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo 0)
4
+ COUNT=$((COUNT + 1))
5
+ echo "$COUNT" > "$COUNTER_FILE"
6
+ if [ $((COUNT % 50)) -ne 0 ]; then
7
+ exit 0
8
+ fi
9
+ SESSION_FILE=$(ls -t ~/.claude/projects/*/session.jsonl 2>/dev/null | head -1)
10
+ if [ -n "$SESSION_FILE" ]; then
11
+ MODEL=$(grep -o '"model":"[^"]*"' "$SESSION_FILE" 2>/dev/null | tail -1 | cut -d'"' -f4)
12
+ if echo "$MODEL" | grep -qi "opus-4-7\|opus-4.7"; then
13
+ echo "⚠ Model alert: You're using $MODEL which may consume 3x more tokens than Opus 4.6."
14
+ echo "Consider: claude --model claude-opus-4-6 or add \"model\": \"claude-opus-4-6\" to settings.json"
15
+ echo "See: https://github.com/anthropics/claude-code/issues/49601"
16
+ fi
17
+ fi
18
+ exit 0
@@ -0,0 +1,31 @@
1
+ #!/bin/bash
2
+ # model-version-change-alert.sh — モデルバージョン変更を検知して警告
3
+ # Why: Opus 4.6がモデルピッカーから突然削除された (#49689, 14👍)。
4
+ # ユーザーが意図せず別モデルに切り替えられるケースが多発。
5
+ # モデルが変わるとhookの挙動・トークン消費・品質が全て変わる。
6
+ # Event: Notification MATCHER: ""
7
+ # Action: 前回のモデルと現在のモデルを比較し、変更時に警告
8
+
9
+ MODEL_HISTORY="/tmp/cc-model-version-history"
10
+ CURRENT_MODEL="${CLAUDE_MODEL:-unknown}"
11
+
12
+ # Notificationイベントのbodyからモデル情報を取得試行
13
+ if [ -n "$1" ]; then
14
+ BODY_MODEL=$(echo "$1" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('model',''))" 2>/dev/null)
15
+ [ -n "$BODY_MODEL" ] && CURRENT_MODEL="$BODY_MODEL"
16
+ fi
17
+
18
+ # 前回のモデルを読み取り
19
+ PREV_MODEL=$(cat "$MODEL_HISTORY" 2>/dev/null || echo "")
20
+
21
+ if [ -n "$PREV_MODEL" ] && [ "$PREV_MODEL" != "$CURRENT_MODEL" ] && [ "$CURRENT_MODEL" != "unknown" ]; then
22
+ echo "⚠ MODEL CHANGED: $PREV_MODEL → $CURRENT_MODEL" >&2
23
+ echo "Your model was switched. This affects token consumption, quality, and hook behavior." >&2
24
+ echo "If unintended, check your settings: claude --model $PREV_MODEL" >&2
25
+ echo "Known issue: Opus 4.6 was removed from the Desktop picker (#49689)" >&2
26
+ fi
27
+
28
+ # 現在のモデルを記録
29
+ [ "$CURRENT_MODEL" != "unknown" ] && echo "$CURRENT_MODEL" > "$MODEL_HISTORY"
30
+
31
+ exit 0
@@ -0,0 +1,92 @@
1
+ #!/bin/bash
2
+ # move-delete-sequence-guard.sh — Detect move+delete sequences that cause data loss
3
+ #
4
+ # Solves: Agent moves files to a temp location, then deletes the parent directory,
5
+ # effectively destroying the moved files' original context and siblings.
6
+ # - #49129: mv files to /tmp && rm -rf parent/ — lost 50GB of data
7
+ # - #49792: Opus 4.7 moves files, then deletes the source directory
8
+ #
9
+ # Pattern detected:
10
+ # mv <source> <dest> && rm -rf <source_parent>
11
+ # mv <source> <dest> ; rm -rf <source_parent>
12
+ # mv <source> <dest> || rm -rf <source_parent>
13
+ # Any compound command containing both mv and rm -r on related paths
14
+ #
15
+ # This is distinct from rm-safety-net.sh (blocks rm on critical paths)
16
+ # and bulk-file-delete-guard.sh (blocks large recursive deletes).
17
+ # This hook specifically targets the mv+rm compound pattern.
18
+ #
19
+ # TRIGGER: PreToolUse MATCHER: "Bash"
20
+
21
+ set -euo pipefail
22
+
23
+ INPUT=$(cat)
24
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
25
+ [ -z "$COMMAND" ] && exit 0
26
+
27
+ # Only check compound commands that contain both mv and rm
28
+ if ! echo "$COMMAND" | grep -qE '\bmv\s' || ! echo "$COMMAND" | grep -qE '\brm\s'; then
29
+ exit 0
30
+ fi
31
+
32
+ # Extract mv source directory and rm target, check for overlap
33
+ # Pattern: mv <src> <dst> [&&;||] rm [-rf] <target>
34
+ # We check if the rm target is a parent of, or the same as, the mv source
35
+
36
+ # Get all mv source paths (first arg after mv and optional flags)
37
+ MV_SOURCES=$(echo "$COMMAND" | grep -oP '\bmv\s+(-[a-zA-Z]+\s+)*\K\S+' 2>/dev/null || true)
38
+ # Get all rm targets
39
+ RM_TARGETS=$(echo "$COMMAND" | grep -oP '\brm\s+(-[a-zA-Z]+\s+)*\K\S+' 2>/dev/null || true)
40
+
41
+ [ -z "$MV_SOURCES" ] && exit 0
42
+ [ -z "$RM_TARGETS" ] && exit 0
43
+
44
+ # Normalize: get parent directory of mv source
45
+ for mv_src in $MV_SOURCES; do
46
+ mv_parent=$(dirname "$mv_src" 2>/dev/null || echo "")
47
+ [ -z "$mv_parent" ] && continue
48
+
49
+ for rm_target in $RM_TARGETS; do
50
+ # Check if rm target matches the mv source's parent or the mv source itself
51
+ # Normalize trailing slashes
52
+ rm_clean="${rm_target%/}"
53
+ mv_parent_clean="${mv_parent%/}"
54
+ mv_src_clean="${mv_src%/}"
55
+
56
+ # Case 1: rm deletes the parent of the moved file
57
+ if [ "$rm_clean" = "$mv_parent_clean" ]; then
58
+ echo "BLOCKED: Move+delete sequence detected — rm target ($rm_target) is parent of mv source ($mv_src)" >&2
59
+ echo "This pattern destroys sibling files. Move files individually instead." >&2
60
+ echo "Command: $COMMAND" >&2
61
+ echo "" >&2
62
+ echo "See: https://github.com/anthropics/claude-code/issues/49129" >&2
63
+ exit 2
64
+ fi
65
+
66
+ # Case 2: rm deletes the exact path that was moved from
67
+ if [ "$rm_clean" = "$mv_src_clean" ]; then
68
+ # mv a dir then rm -rf the same dir — likely the user moved the dir,
69
+ # but rm -rf on the source after mv is suspicious if recursive
70
+ if echo "$COMMAND" | grep -qE "rm\s+.*-[rRf]*[rR].*$rm_target|rm\s+.*-[rRf]*[rR]\s+$rm_target"; then
71
+ echo "BLOCKED: Move+delete sequence detected — rm -r on the same path as mv source ($mv_src)" >&2
72
+ echo "If the directory was already moved, rm -r should not be needed." >&2
73
+ echo "Command: $COMMAND" >&2
74
+ echo "" >&2
75
+ echo "See: https://github.com/anthropics/claude-code/issues/49129" >&2
76
+ exit 2
77
+ fi
78
+ fi
79
+
80
+ # Case 3: rm deletes an ancestor directory of the mv source
81
+ if echo "$mv_src_clean" | grep -qE "^${rm_clean}/"; then
82
+ echo "BLOCKED: Move+delete sequence detected — rm target ($rm_target) is ancestor of mv source ($mv_src)" >&2
83
+ echo "This pattern destroys the source tree. Use targeted operations instead." >&2
84
+ echo "Command: $COMMAND" >&2
85
+ echo "" >&2
86
+ echo "See: https://github.com/anthropics/claude-code/issues/49129" >&2
87
+ exit 2
88
+ fi
89
+ done
90
+ done
91
+
92
+ exit 0
@@ -0,0 +1,72 @@
1
+ #!/bin/bash
2
+ # pii-upload-guard.sh — Detect PII in outbound data before upload
3
+ #
4
+ # Solves: Claude uploaded physical coordinates to a public website
5
+ # despite CLAUDE.md stating "no PII" for 17 sessions. CLAUDE.md
6
+ # instructions are suggestions; hooks are enforcement. (#46910)
7
+ #
8
+ # How it works: Scans Bash commands for outbound data operations
9
+ # (curl POST/PUT, scp, rsync to remote, git push with config files)
10
+ # and checks if the data being sent contains PII patterns:
11
+ # coordinates, emails, phone numbers, API keys, addresses.
12
+ #
13
+ # TRIGGER: PreToolUse
14
+ # MATCHER: "Bash"
15
+
16
+ set -euo pipefail
17
+
18
+ INPUT=$(cat)
19
+ CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
20
+ [ -z "$CMD" ] && exit 0
21
+
22
+ # Only check outbound data commands
23
+ IS_OUTBOUND=0
24
+ if echo "$CMD" | grep -qiE 'curl\s+.*-X\s*(POST|PUT|PATCH)|curl\s+.*--data|curl\s+.*-d\s'; then
25
+ IS_OUTBOUND=1
26
+ elif echo "$CMD" | grep -qiE '\bscp\b.*:|\brsync\b.*:|\bsftp\b'; then
27
+ IS_OUTBOUND=1
28
+ elif echo "$CMD" | grep -qiE 'curl\s+.*upload|wget\s+.*--post'; then
29
+ IS_OUTBOUND=1
30
+ fi
31
+
32
+ [ "$IS_OUTBOUND" -eq 0 ] && exit 0
33
+
34
+ # Check for PII patterns in the command
35
+ PII_FOUND=""
36
+
37
+ # GPS coordinates (latitude/longitude pairs)
38
+ if echo "$CMD" | grep -qE '[-]?[0-9]{1,3}\.[0-9]{4,}.*[-]?[0-9]{1,3}\.[0-9]{4,}'; then
39
+ PII_FOUND="GPS coordinates"
40
+ fi
41
+
42
+ # Email addresses
43
+ if echo "$CMD" | grep -qiE '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'; then
44
+ PII_FOUND="${PII_FOUND:+$PII_FOUND, }email address"
45
+ fi
46
+
47
+ # Phone numbers (various formats)
48
+ if echo "$CMD" | grep -qE '\+?[0-9]{1,4}[-. ]?\(?[0-9]{1,4}\)?[-. ]?[0-9]{3,4}[-. ]?[0-9]{3,4}'; then
49
+ PII_FOUND="${PII_FOUND:+$PII_FOUND, }phone number"
50
+ fi
51
+
52
+ # API keys / tokens (long hex or base64 strings in data)
53
+ if echo "$CMD" | grep -qE '(key|token|secret|password|api_key|apikey)=[A-Za-z0-9+/=_-]{20,}'; then
54
+ PII_FOUND="${PII_FOUND:+$PII_FOUND, }API key/token"
55
+ fi
56
+
57
+ # Physical addresses (street patterns)
58
+ if echo "$CMD" | grep -qiE '[0-9]+\s+(street|st|avenue|ave|road|rd|boulevard|blvd|drive|dr|lane|ln)\b'; then
59
+ PII_FOUND="${PII_FOUND:+$PII_FOUND, }physical address"
60
+ fi
61
+
62
+ if [ -n "$PII_FOUND" ]; then
63
+ echo "WARNING: Possible PII detected in outbound data: $PII_FOUND" >&2
64
+ echo " Command: $(echo "$CMD" | head -c 200)" >&2
65
+ echo " Review the data being sent before proceeding." >&2
66
+ echo " If this is intentional, acknowledge the PII and re-run." >&2
67
+ # exit 1 = warning (allow with notice), not exit 2 (block)
68
+ # Some legitimate uses send coordinates/emails
69
+ exit 1
70
+ fi
71
+
72
+ exit 0
@@ -0,0 +1,14 @@
1
+ COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
2
+ [ -z "$COMMAND" ] && exit 0
3
+ echo "$COMMAND" | grep -qE '\bgh\s+pr\s+create\b' || exit 0
4
+ BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
5
+ [ -z "$BRANCH" ] && exit 0
6
+ EXISTING=$(gh pr list --head "$BRANCH" --state open --json number,title --jq '.[0].number' 2>/dev/null)
7
+ if [ -n "$EXISTING" ]; then
8
+ TITLE=$(gh pr list --head "$BRANCH" --state open --json title --jq '.[0].title' 2>/dev/null)
9
+ echo "BLOCKED: An open PR already exists for branch '$BRANCH'." >&2
10
+ echo " PR #$EXISTING: $TITLE" >&2
11
+ echo " Update the existing PR instead of creating a new one." >&2
12
+ exit 2
13
+ fi
14
+ exit 0
@@ -0,0 +1,60 @@
1
+ #!/bin/bash
2
+ # production-port-kill-guard.sh — Block commands that kill processes by port number
3
+ #
4
+ # Solves: Claude Code killing production services running on ports without
5
+ # understanding their purpose. Real incident: user's CLAUDE.md said
6
+ # port 7000, but Claude killed the process on port 8000 — $1,000 loss.
7
+ # (GitHub Issue #50971)
8
+ #
9
+ # Detects:
10
+ # lsof -ti :PORT | xargs kill (find process by port, then kill)
11
+ # lsof -t -i :PORT | kill (same, different flag style)
12
+ # fuser -k PORT/tcp (directly kill process on port)
13
+ # fuser --kill PORT/tcp (same, long flag)
14
+ # kill $(lsof -ti :PORT) (subshell variant)
15
+ #
16
+ # Does NOT block:
17
+ # lsof -i :PORT (just listing, no kill)
18
+ # fuser PORT/tcp (just checking, no kill)
19
+ # netstat / ss (read-only port inspection)
20
+ #
21
+ # TRIGGER: PreToolUse MATCHER: "Bash"
22
+
23
+ INPUT=$(cat)
24
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
25
+
26
+ [ -z "$COMMAND" ] && exit 0
27
+
28
+ # Block lsof-to-kill pipeline (lsof -ti :PORT piped to kill/xargs kill)
29
+ # Covers: -ti :PORT, -t -i :PORT, -i :PORT -t, and combined flags like -sti
30
+ if echo "$COMMAND" | grep -qE 'lsof\s.*-[a-zA-Z]*t.*:'; then
31
+ if echo "$COMMAND" | grep -qE '\|\s*(xargs\s+)?kill|kill\s+\$\('; then
32
+ PORT=$(echo "$COMMAND" | grep -oP ':\K\d+' | head -1)
33
+ echo "BLOCKED: Killing process by port number is dangerous." >&2
34
+ echo " Port ${PORT} may be running a production service." >&2
35
+ echo " First check what's running: lsof -i :${PORT}" >&2
36
+ echo " Then decide manually whether to stop it." >&2
37
+ echo " Command: $COMMAND" >&2
38
+ exit 2
39
+ fi
40
+ fi
41
+
42
+ # Block kill $(lsof ...) subshell pattern
43
+ if echo "$COMMAND" | grep -qE 'kill\s+\$\(lsof\s'; then
44
+ echo "BLOCKED: Killing process found by lsof is dangerous." >&2
45
+ echo " Verify the process identity before terminating." >&2
46
+ echo " Command: $COMMAND" >&2
47
+ exit 2
48
+ fi
49
+
50
+ # Block fuser -k (directly kills process on port)
51
+ if echo "$COMMAND" | grep -qE '\bfuser\s+(-[a-zA-Z]*k|--kill)\s'; then
52
+ PORT=$(echo "$COMMAND" | grep -oP '\d+(?=/tcp)' | head -1)
53
+ echo "BLOCKED: fuser -k kills the process on port ${PORT:-unknown} immediately." >&2
54
+ echo " First check: fuser ${PORT:-PORT}/tcp" >&2
55
+ echo " Then stop the service gracefully if needed." >&2
56
+ echo " Command: $COMMAND" >&2
57
+ exit 2
58
+ fi
59
+
60
+ exit 0