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,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,61 @@
1
+ #!/bin/bash
2
+ # thinking-stall-detector.sh — Detect when Claude's thinking phase stalls
3
+ #
4
+ # Solves: #51092 — Sonnet 4.6 thinking ran for 25 minutes, consuming
5
+ # 16M+ tokens. User lost entire token allowance to a single
6
+ # reasoning phase that never produced output.
7
+ #
8
+ # HOW IT WORKS:
9
+ # Tracks time between consecutive tool calls. If the gap exceeds
10
+ # a threshold (default 5 minutes), it means Claude was "thinking"
11
+ # without taking any action — likely a reasoning stall.
12
+ #
13
+ # On detection, logs a warning with the stall duration and suggests
14
+ # the user interrupt with Ctrl+C.
15
+ #
16
+ # WHY THIS MATTERS:
17
+ # During thinking, tokens are consumed but no hooks fire. This hook
18
+ # fires on the NEXT tool call after the stall, so it can't prevent
19
+ # the stall itself — but it alerts the user that one occurred, so
20
+ # they can watch for it happening again and interrupt early.
21
+ #
22
+ # TRIGGER: PreToolUse MATCHER: ""
23
+ # Also works as: Notification (fires on status changes)
24
+ #
25
+ # CONFIGURATION:
26
+ # CC_STALL_WARN_SECS=300 warn after 5-minute gap (default)
27
+ # CC_STALL_LOG=/tmp/cc-thinking-stalls.log
28
+
29
+ INPUT=$(cat)
30
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
31
+
32
+ STATE_FILE="/tmp/cc-thinking-stall-last-call"
33
+ LOG_FILE="${CC_STALL_LOG:-/tmp/cc-thinking-stalls.log}"
34
+ WARN_SECS="${CC_STALL_WARN_SECS:-300}"
35
+
36
+ NOW=$(date +%s)
37
+
38
+ # Read last tool call timestamp
39
+ LAST=$(cat "$STATE_FILE" 2>/dev/null || echo "$NOW")
40
+
41
+ # Update timestamp
42
+ echo "$NOW" > "$STATE_FILE"
43
+
44
+ # Calculate gap
45
+ GAP=$((NOW - LAST))
46
+
47
+ if [ "$GAP" -ge "$WARN_SECS" ]; then
48
+ MINUTES=$((GAP / 60))
49
+ REMAINDER=$((GAP % 60))
50
+
51
+ # Log the stall
52
+ echo "$(date -Iseconds) STALL ${MINUTES}m${REMAINDER}s before tool=$TOOL" >> "$LOG_FILE"
53
+
54
+ # Warn the user
55
+ echo "⚠️ Thinking stall detected: ${MINUTES}m${REMAINDER}s with no tool activity." >&2
56
+ echo "This may indicate a reasoning loop consuming tokens silently." >&2
57
+ echo "If this happens again, press Ctrl+C to interrupt." >&2
58
+ echo "See #51092: 25-minute thinking stall consumed 16M tokens." >&2
59
+ fi
60
+
61
+ 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
@@ -0,0 +1,35 @@
1
+ #!/bin/bash
2
+ # worktree-branch-pollution-detector.sh — worktreeが親ブランチを汚染していないか検知
3
+ # Why: サブエージェントのworktree操作が親リポを予期しないブランチに移動させ、
4
+ # 意図しないcommit-to-mainが発生する。1週間で3回の事故報告あり (#50207)
5
+ # Event: PostToolUse MATCHER: Bash
6
+ # Action: 現在のブランチが期待値と異なる場合に警告
7
+
8
+ INPUT=$(cat)
9
+
10
+ # 期待ブランチ(セッション開始時に記録)
11
+ EXPECTED_BRANCH_FILE="/tmp/cc-expected-branch-$(pwd | md5sum | cut -c1-8)"
12
+
13
+ # git管理下でなければスキップ
14
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
15
+
16
+ CURRENT_BRANCH=$(git branch --show-current 2>/dev/null)
17
+ [ -z "$CURRENT_BRANCH" ] && exit 0
18
+
19
+ # 初回実行時はブランチを記録
20
+ if [ ! -f "$EXPECTED_BRANCH_FILE" ]; then
21
+ echo "$CURRENT_BRANCH" > "$EXPECTED_BRANCH_FILE"
22
+ exit 0
23
+ fi
24
+
25
+ EXPECTED_BRANCH=$(cat "$EXPECTED_BRANCH_FILE" 2>/dev/null)
26
+
27
+ if [ "$CURRENT_BRANCH" != "$EXPECTED_BRANCH" ]; then
28
+ echo "⚠ BRANCH CHANGED: Expected '$EXPECTED_BRANCH' but now on '$CURRENT_BRANCH'" >&2
29
+ echo "This may be caused by a worktree or subagent switching your branch." >&2
30
+ echo "Run 'git checkout $EXPECTED_BRANCH' to return. See: #50207" >&2
31
+ # 新しいブランチを記録(意図的な切替かもしれない)
32
+ echo "$CURRENT_BRANCH" > "$EXPECTED_BRANCH_FILE"
33
+ fi
34
+
35
+ exit 0
@@ -0,0 +1,6 @@
1
+ LOGFILE="${HOME}/.claude/worktree-audit.log"
2
+ INFO=$(cat)
3
+ BRANCH=$(echo "$INFO" | jq -r '.branch // "unknown"' 2>/dev/null)
4
+ PATH_WT=$(echo "$INFO" | jq -r '.path // "unknown"' 2>/dev/null)
5
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] CREATE branch=$BRANCH path=$PATH_WT" >> "$LOGFILE"
6
+ exit 0
@@ -0,0 +1,72 @@
1
+ #!/bin/bash
2
+ # worktree-hook-linker.sh — Auto-link settings to worktrees
3
+ #
4
+ # Solves: In git worktrees, .claude/settings.json is not found because
5
+ # worktrees share .git but not the working directory. All hooks
6
+ # become silently disabled. (#46808)
7
+ #
8
+ # How it works: On SessionStart, checks if the current directory is a
9
+ # git worktree. If so, creates a symlink from the worktree's
10
+ # .claude/settings.json to the main tree's settings. This ensures
11
+ # hooks work identically in worktrees.
12
+ #
13
+ # {
14
+ # "hooks": {
15
+ # "Notification": [{
16
+ # "matcher": "SessionStart",
17
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/worktree-hook-linker.sh" }]
18
+ # }]
19
+ # }
20
+ # }
21
+ #
22
+ # TRIGGER: Notification
23
+ # MATCHER: "SessionStart"
24
+
25
+ # Detect if we're in a git worktree
26
+ GITDIR=$(git rev-parse --git-dir 2>/dev/null) || exit 0
27
+ echo "$GITDIR" | grep -q "worktrees" || exit 0
28
+
29
+ # We're in a worktree — find the main working tree
30
+ MAIN_GITDIR=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || exit 0
31
+ MAIN_WORKDIR=$(echo "$MAIN_GITDIR" | sed 's|/.git$||')
32
+
33
+ MAIN_CLAUDE_DIR="$MAIN_WORKDIR/.claude"
34
+ LOCAL_CLAUDE_DIR=".claude"
35
+
36
+ # Skip if main tree has no .claude directory
37
+ [ -d "$MAIN_CLAUDE_DIR" ] || exit 0
38
+
39
+ # Create .claude directory in worktree if needed
40
+ mkdir -p "$LOCAL_CLAUDE_DIR" 2>/dev/null
41
+
42
+ # Link settings files if they exist in main but not in worktree
43
+ for f in settings.json settings.local.json; do
44
+ MAIN_FILE="$MAIN_CLAUDE_DIR/$f"
45
+ LOCAL_FILE="$LOCAL_CLAUDE_DIR/$f"
46
+
47
+ [ ! -f "$MAIN_FILE" ] && continue
48
+
49
+ if [ ! -e "$LOCAL_FILE" ]; then
50
+ ln -s "$MAIN_FILE" "$LOCAL_FILE"
51
+ echo "Linked $f from main tree → worktree (hooks now active)" >&2
52
+ elif [ -L "$LOCAL_FILE" ]; then
53
+ # Already a symlink — verify it points to the right place
54
+ TARGET=$(readlink -f "$LOCAL_FILE" 2>/dev/null)
55
+ EXPECTED=$(readlink -f "$MAIN_FILE" 2>/dev/null)
56
+ if [ "$TARGET" != "$EXPECTED" ]; then
57
+ rm "$LOCAL_FILE"
58
+ ln -s "$MAIN_FILE" "$LOCAL_FILE"
59
+ echo "Re-linked $f (was pointing to wrong location)" >&2
60
+ fi
61
+ fi
62
+ done
63
+
64
+ # Also link hooks directory if it exists
65
+ MAIN_HOOKS="$MAIN_CLAUDE_DIR/hooks"
66
+ LOCAL_HOOKS="$LOCAL_CLAUDE_DIR/hooks"
67
+ if [ -d "$MAIN_HOOKS" ] && [ ! -e "$LOCAL_HOOKS" ]; then
68
+ ln -s "$MAIN_HOOKS" "$LOCAL_HOOKS"
69
+ echo "Linked hooks/ directory from main tree → worktree" >&2
70
+ fi
71
+
72
+ exit 0
@@ -0,0 +1,20 @@
1
+ INFO=$(cat)
2
+ PATH_WT=$(echo "$INFO" | jq -r '.path // empty' 2>/dev/null)
3
+ [ -z "$PATH_WT" ] && exit 0
4
+ [ ! -d "$PATH_WT" ] && exit 0
5
+ cd "$PATH_WT" 2>/dev/null || exit 0
6
+ DIRTY=$(git status --porcelain 2>/dev/null | wc -l)
7
+ if [ "$DIRTY" -gt 0 ]; then
8
+ echo "BLOCKED: Worktree at $PATH_WT has $DIRTY uncommitted change(s)." >&2
9
+ echo "Commit or stash changes before removing." >&2
10
+ exit 2
11
+ fi
12
+ BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
13
+ if [ -n "$BRANCH" ]; then
14
+ UNPUSHED=$(git log --oneline "origin/$BRANCH..$BRANCH" 2>/dev/null | wc -l)
15
+ if [ "$UNPUSHED" -gt 0 ]; then
16
+ echo "WARNING: $UNPUSHED unpushed commit(s) on $BRANCH." >&2
17
+ echo "Push before removing: git push origin $BRANCH" >&2
18
+ fi
19
+ fi
20
+ exit 0
@@ -0,0 +1,60 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Bash",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE '^\\s*(sudo\\s+)?rm\\s+.*-[rRf]*[rR]' && ! echo \"$CMD\" | grep -qE '(node_modules|dist|build|__pycache__|/tmp)'; then echo 'BLOCKED: recursive rm on non-safe target. Use specific paths.' >&2; exit 2; fi"
10
+ }
11
+ ]
12
+ },
13
+ {
14
+ "matcher": "Bash",
15
+ "hooks": [
16
+ {
17
+ "type": "command",
18
+ "command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'git\\s+push\\s+.*--force|git\\s+reset\\s+--hard|git\\s+clean\\s+-fd'; then echo 'BLOCKED: destructive git operation. Use safer alternatives.' >&2; exit 2; fi"
19
+ }
20
+ ]
21
+ },
22
+ {
23
+ "matcher": "Bash",
24
+ "hooks": [
25
+ {
26
+ "type": "command",
27
+ "command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qiE '(api.key|secret|password|token).*=.*[A-Za-z0-9]{20}'; then echo 'BLOCKED: potential credential in command. Use environment variables.' >&2; exit 2; fi"
28
+ }
29
+ ]
30
+ },
31
+ {
32
+ "matcher": "Write|Edit",
33
+ "hooks": [
34
+ {
35
+ "type": "command",
36
+ "command": "INPUT=$(cat); FILE=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; if echo \"$FILE\" | grep -qE '\\.(env|pem|key|credentials|secret)$'; then echo 'BLOCKED: writing to sensitive file. Check if this is intentional.' >&2; exit 2; fi"
37
+ }
38
+ ]
39
+ },
40
+ {
41
+ "matcher": "Bash",
42
+ "hooks": [
43
+ {
44
+ "type": "command",
45
+ "command": "~/.claude/hooks/move-delete-sequence-guard.sh"
46
+ }
47
+ ]
48
+ },
49
+ {
50
+ "matcher": "Bash",
51
+ "hooks": [
52
+ {
53
+ "type": "command",
54
+ "command": "~/.claude/hooks/system-dir-protection-guard.sh"
55
+ }
56
+ ]
57
+ }
58
+ ]
59
+ }
60
+ }
package/index.mjs CHANGED
@@ -94,6 +94,7 @@ const GENERATE_CI = process.argv.includes('--generate-ci');
94
94
  const REPORT = process.argv.includes('--report');
95
95
  const QUICKFIX = process.argv.includes('--quickfix');
96
96
  const SHIELD = process.argv.includes('--shield');
97
+ const OPUS47 = process.argv.includes('--opus47');
97
98
  const ANALYZE = process.argv.includes('--analyze');
98
99
  const TEAM = process.argv.includes('--team');
99
100
  const MIGRATE_FROM_IDX = process.argv.findIndex(a => a === '--migrate-from');
@@ -192,8 +193,9 @@ if (HELP) {
192
193
  Find hooks: npx cc-hook-registry search <keyword>
193
194
  Test hooks: npx cc-hook-test <hook.sh>
194
195
 
195
- Support: https://github.com/sponsors/yurukusa
196
- Book: https://zenn.dev/yurukusa/books/6076c23b1cb18b
196
+ Token Checkup: https://yurukusa.github.io/cc-safe-setup/token-checkup.html
197
+ Token Book: https://yurukusa.github.io/cc-safe-setup/token-book.html
198
+ Safety Guide: https://zenn.dev/yurukusa/books/6076c23b1cb18b
197
199
  `);
198
200
  process.exit(0);
199
201
  }
@@ -2916,6 +2918,79 @@ async function analyze() {
2916
2918
  console.log();
2917
2919
  }
2918
2920
 
2921
+ async function opus47() {
2922
+ console.log();
2923
+ console.log(c.bold + ' 🚨 cc-safe-setup --opus47' + c.reset);
2924
+ console.log(c.dim + ' Opus 4.7 protection — fixes for known critical issues' + c.reset);
2925
+ console.log();
2926
+
2927
+ // First install core hooks if not already installed
2928
+ mkdirSync(HOOKS_DIR, { recursive: true });
2929
+ let coreInstalled = 0;
2930
+ for (const [hookId, hookMeta] of Object.entries(HOOKS)) {
2931
+ const hookPath = join(HOOKS_DIR, `${hookId}.sh`);
2932
+ if (!existsSync(hookPath)) {
2933
+ writeFileSync(hookPath, SCRIPTS[hookId]);
2934
+ chmodSync(hookPath, 0o755);
2935
+ coreInstalled++;
2936
+ }
2937
+ }
2938
+ if (coreInstalled > 0) {
2939
+ // Update settings.json for core hooks
2940
+ let settings = {};
2941
+ if (existsSync(SETTINGS_PATH)) {
2942
+ settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf8'));
2943
+ }
2944
+ if (!settings.hooks) settings.hooks = {};
2945
+ for (const [hookId, hookMeta] of Object.entries(HOOKS)) {
2946
+ const trigger = hookMeta.trigger;
2947
+ if (!settings.hooks[trigger]) settings.hooks[trigger] = [];
2948
+ const hookPath = toBashPath(join(HOOKS_DIR, `${hookId}.sh`));
2949
+ const exists = settings.hooks[trigger].some(h => h.hooks?.some(hh => hh.command?.includes(hookId)));
2950
+ if (!exists) {
2951
+ settings.hooks[trigger].push({
2952
+ matcher: hookMeta.matcher,
2953
+ hooks: [{ type: 'command', command: hookPath }]
2954
+ });
2955
+ }
2956
+ }
2957
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
2958
+ console.log(c.green + ' ✓' + c.reset + ` ${coreInstalled} core safety hooks installed`);
2959
+ } else {
2960
+ console.log(c.dim + ' ✓ Core safety hooks already installed' + c.reset);
2961
+ }
2962
+
2963
+ // Install Opus 4.7-specific hooks
2964
+ const opus47Hooks = [
2965
+ { name: 'model-version-alert', desc: 'Warns when Opus 4.7 is silently active (#49541)', issue: '4x token consumption' },
2966
+ { name: 'shell-config-truncation-guard', desc: 'Blocks installer from truncating ~/.bash_profile (#49615)', issue: 'config destruction' },
2967
+ { name: 'credential-exfil-guard', desc: 'Blocks credential file access (#49539, #49554)', issue: 'credential deletion' },
2968
+ { name: 'home-critical-bash-guard', desc: 'Blocks destructive ops on critical dotfiles (#49554)', issue: 'home directory attacks' },
2969
+ ];
2970
+
2971
+ console.log();
2972
+ console.log(c.bold + ' Opus 4.7 protection hooks:' + c.reset);
2973
+ let added = 0;
2974
+ for (const hook of opus47Hooks) {
2975
+ try {
2976
+ await installExample(hook.name);
2977
+ added++;
2978
+ } catch (e) {
2979
+ // installExample may exit on error, but we want to continue
2980
+ }
2981
+ }
2982
+
2983
+ console.log();
2984
+ console.log(c.bold + ' Why these hooks matter:' + c.reset);
2985
+ console.log(c.dim + ' Opus 4.7\'s safety classifier is hardcoded to 4.6 (#49618).' + c.reset);
2986
+ console.log(c.dim + ' Auto mode can\'t block dangerous commands on 4.7.' + c.reset);
2987
+ console.log(c.dim + ' These hooks run at process level — independent of the model.' + c.reset);
2988
+ console.log();
2989
+ console.log(c.green + ' Done.' + c.reset + ' Your setup is protected against known Opus 4.7 issues.');
2990
+ console.log(c.dim + ' Guide: https://yurukusa.github.io/cc-safe-setup/opus-47-survival-guide.html' + c.reset);
2991
+ console.log();
2992
+ }
2993
+
2919
2994
  async function shield() {
2920
2995
  const { execSync } = await import('child_process');
2921
2996
  const { readdirSync } = await import('fs');
@@ -2969,6 +3044,9 @@ async function shield() {
2969
3044
  // Always include these for maximum safety
2970
3045
  extras.push('scope-guard', 'no-sudo-guard', 'protect-claudemd', 'memory-write-guard', 'skill-gate', 'auto-approve-test', 'auto-approve-readonly');
2971
3046
 
3047
+ // Opus 4.7 safety: classifier is hardcoded to 4.6 (#49618) — hooks are the only defense
3048
+ extras.push('dotfile-protection-guard', 'home-critical-bash-guard');
3049
+
2972
3050
  for (const ex of extras) {
2973
3051
  const exPath = join(__dirname, 'examples', `${ex}.sh`);
2974
3052
  const hookPath = join(HOOKS_DIR, `${ex}.sh`);
@@ -3118,6 +3196,9 @@ async function shield() {
3118
3196
  console.log(c.dim + ' Verify: npx cc-safe-setup --verify' + c.reset);
3119
3197
  console.log(c.dim + ' Status: npx cc-safe-setup --status' + c.reset);
3120
3198
  console.log();
3199
+ console.log(c.dim + ' Burning tokens too fast? Free diagnosis:' + c.reset);
3200
+ console.log(c.blue + ' https://yurukusa.github.io/cc-safe-setup/token-checkup.html' + c.reset);
3201
+ console.log();
3121
3202
  }
3122
3203
 
3123
3204
  async function quickfix() {
@@ -5784,6 +5865,7 @@ async function main() {
5784
5865
  if (TEAM) return team();
5785
5866
  if (PROFILE_IDX !== -1) return profile(PROFILE);
5786
5867
  if (ANALYZE) return analyze();
5868
+ if (OPUS47) return opus47();
5787
5869
  if (SHIELD) return shield();
5788
5870
  if (QUICKFIX) return quickfix();
5789
5871
  if (REPORT) return report();
@@ -5898,12 +5980,16 @@ async function main() {
5898
5980
  console.log(' ' + c.blue + ' --doctor' + c.reset + ' Verify hooks work');
5899
5981
  console.log(' ' + c.blue + ' --simulate "cmd"' + c.reset + ' Test how hooks react');
5900
5982
  console.log(' ' + c.blue + ' --shield' + c.reset + ' Maximum safety (recommended)');
5983
+ console.log(' ' + c.blue + ' --opus47' + c.reset + ' Opus 4.7 crisis protection');
5901
5984
  console.log();
5902
- console.log(' ' + c.dim + '23 web tools: https://yurukusa.github.io/cc-safe-setup/hub.html' + c.reset);
5985
+ console.log(' ' + c.dim + 'Free tools:' + c.reset);
5986
+ console.log(' ' + c.blue + ' Token Checkup' + c.reset + ' https://yurukusa.github.io/cc-safe-setup/token-checkup.html');
5987
+ console.log(' ' + c.blue + ' Token Book' + c.reset + ' https://yurukusa.github.io/cc-safe-setup/token-book.html');
5988
+ console.log(' ' + c.dim + ' 28 web tools: https://yurukusa.github.io/cc-safe-setup/hub.html' + c.reset);
5903
5989
  console.log();
5904
- console.log(' ' + c.dim + 'Like this tool? Support development:' + c.reset);
5905
- console.log(' ' + c.dim + ' Sponsor: https://github.com/sponsors/yurukusa' + c.reset);
5906
- console.log(' ' + c.dim + ' Book: https://zenn.dev/yurukusa/books/6076c23b1cb18b' + c.reset);
5990
+ console.log(' ' + c.dim + 'Tokens disappearing too fast?' + c.reset);
5991
+ console.log(' ' + c.blue + ' Token Book' + c.reset + ' Cut consumption in half — https://yurukusa.github.io/cc-safe-setup/token-book.html');
5992
+ console.log(' ' + c.dim + ' Safety Guide: https://zenn.dev/yurukusa/books/6076c23b1cb18b' + c.reset);
5907
5993
  console.log();
5908
5994
  }
5909
5995
 
@@ -0,0 +1,4 @@
1
+ This file was created in the wrong location. The correct version is at:
2
+ ~/.claude/projects/-home-namakusa-projects-cc-loop/memory/market-anthropic-japan-strategy-2026-04-13.md
3
+
4
+ This file can be deleted.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cc-safe-setup",
3
- "version": "29.6.40",
4
- "description": "One command to make Claude Code safe. 650 example hooks + 8 built-in. 56 CLI commands. Token consumption diagnosis. Works with Auto Mode.",
3
+ "version": "29.8.0",
4
+ "description": "One command to make Claude Code safe. 701 example hooks + 8 built-in. 56 CLI commands. Token consumption diagnosis. Works with Auto Mode.",
5
5
  "main": "index.mjs",
6
6
  "bin": {
7
7
  "cc-safe-setup": "index.mjs"