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,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,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
@@ -0,0 +1,30 @@
1
+ #!/bin/bash
2
+ # quota-reset-cycle-monitor.sh — quotaリセット周期の変更を検知
3
+ # Why: ユーザーのquotaリセット周期が予告なく月曜→金曜に変更された (#49599, 2r/4c)。
4
+ # リセット日を追跡し、周期変更時に警告する。
5
+ # 突然のquota枯渇の原因究明に役立つ。
6
+ # Event: Notification MATCHER: ""
7
+ # Action: 日次でquotaリセット日を記録、周期変更を検知
8
+
9
+ RESET_LOG="/tmp/cc-quota-reset-history"
10
+ TODAY=$(date +%u) # 1=Monday, 7=Sunday
11
+ TODAY_DATE=$(date +%Y-%m-%d)
12
+
13
+ # 1日1回だけチェック(日付で制御)
14
+ LAST_CHECK=$(head -1 "$RESET_LOG" 2>/dev/null | cut -d'|' -f1)
15
+ [ "$LAST_CHECK" = "$TODAY_DATE" ] && exit 0
16
+
17
+ # /costの出力からリセット情報を取得する方法の案内
18
+ # 実際のリセット検知は手動確認が必要だが、ログを残すことで追跡可能
19
+ echo "$TODAY_DATE|$TODAY" >> "$RESET_LOG"
20
+
21
+ # リセット履歴が2件以上ある場合、周期を分析
22
+ ENTRIES=$(wc -l < "$RESET_LOG" 2>/dev/null || echo "0")
23
+ if [ "$ENTRIES" -ge 7 ]; then
24
+ # 過去7日の曜日パターンを表示(週末にquotaが増えたら次週リセット=正常)
25
+ echo "📊 Quota tracking: $ENTRIES days logged. Run '/cost' to check current reset day." >&2
26
+ echo "Known issue: reset cycle may change without notice (#49599)." >&2
27
+ echo "If your quota resets on a different day than expected, report to Anthropic support." >&2
28
+ fi
29
+
30
+ exit 0
@@ -0,0 +1,33 @@
1
+ #!/bin/bash
2
+ # repo-visibility-guard.sh — Block repository visibility changes
3
+ # Prevents Claude Code from making private repos public (or vice versa).
4
+ # Incident: #50353 — Opus 4.7 ran `gh repo edit --visibility public` autonomously,
5
+ # exposing a hardcoded private key. Wallet drained $413 in 60-90 seconds.
6
+ #
7
+ # Hook config (settings.json):
8
+ # {
9
+ # "hooks": {
10
+ # "PreToolUse": [{
11
+ # "matcher": "Bash",
12
+ # "hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/repo-visibility-guard.sh" }]
13
+ # }]
14
+ # }
15
+ # }
16
+
17
+ INPUT=$(cat)
18
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
19
+ [ -z "$COMMAND" ] && exit 0
20
+
21
+ # Block gh repo edit --visibility (public/private/internal)
22
+ if echo "$COMMAND" | grep -qE 'gh\s+repo\s+edit\s+--visibility'; then
23
+ echo "BLOCKED: repository visibility change requires manual confirmation. See #50353." >&2
24
+ exit 2
25
+ fi
26
+
27
+ # Block git push with --set-upstream to unknown remotes (potential exfiltration)
28
+ if echo "$COMMAND" | grep -qE 'git\s+remote\s+add\s' && echo "$COMMAND" | grep -qE 'git\s+push'; then
29
+ echo "BLOCKED: adding remote and pushing in one command. Review the remote URL first." >&2
30
+ exit 2
31
+ fi
32
+
33
+ exit 0
@@ -0,0 +1,51 @@
1
+ #!/bin/bash
2
+ # sandbox-relative-path-audit.sh — Detect relative paths in sandbox settings that are silently ignored
3
+ #
4
+ # CRITICAL: denyWrite, denyRead, and allowWrite in settings.json only work
5
+ # with ABSOLUTE paths. Relative paths are SILENTLY IGNORED — no error, no
6
+ # warning, and zero protection. Users who think they've protected sensitive
7
+ # directories may have no actual protection.
8
+ #
9
+ # Born from: https://github.com/anthropics/claude-code/issues/50454
10
+ #
11
+ # TRIGGER: PreToolUse MATCHER: "Bash|Write|Edit"
12
+ # Best used as a Notification hook (exit 0 always) to alert without blocking.
13
+
14
+ INPUT=$(cat)
15
+ # Only run once per session (check marker file)
16
+ MARKER="/tmp/cc-sandbox-audit-$$"
17
+ [ -f "$MARKER" ] && exit 0
18
+ touch "$MARKER"
19
+
20
+ # Find settings.json locations
21
+ SETTINGS_FILES=""
22
+ [ -f "$HOME/.claude/settings.json" ] && SETTINGS_FILES="$HOME/.claude/settings.json"
23
+ [ -f ".claude/settings.json" ] && SETTINGS_FILES="$SETTINGS_FILES .claude/settings.json"
24
+ [ -f "$HOME/.claude/settings.local.json" ] && SETTINGS_FILES="$SETTINGS_FILES $HOME/.claude/settings.local.json"
25
+
26
+ [ -z "$SETTINGS_FILES" ] && exit 0
27
+
28
+ FOUND_RELATIVE=0
29
+ for SFILE in $SETTINGS_FILES; do
30
+ for KEY in denyWrite denyRead allowWrite; do
31
+ PATHS=$(jq -r ".permissions.${KEY}[]? // empty" "$SFILE" 2>/dev/null)
32
+ [ -z "$PATHS" ] && continue
33
+ while IFS= read -r P; do
34
+ [ -z "$P" ] && continue
35
+ if [[ "$P" != /* ]] && [[ "$P" != "~"* ]]; then
36
+ echo "⚠ SANDBOX WARNING: Relative path in ${KEY} is SILENTLY IGNORED" >&2
37
+ echo " File: $SFILE" >&2
38
+ echo " Path: \"$P\" → has NO effect" >&2
39
+ echo " Fix: Use absolute path: \"$(realpath -m "$P" 2>/dev/null || echo "$PWD/$P")\"" >&2
40
+ FOUND_RELATIVE=1
41
+ fi
42
+ done <<< "$PATHS"
43
+ done
44
+ done
45
+
46
+ if [ "$FOUND_RELATIVE" -eq 1 ]; then
47
+ echo "" >&2
48
+ echo "See: https://github.com/anthropics/claude-code/issues/50454" >&2
49
+ fi
50
+
51
+ exit 0