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.
- package/.claude-plugin/marketplace.json +66 -0
- package/.claude-plugin/plugin.json +11 -0
- package/README.md +123 -12
- package/SETTINGS_REFERENCE.md +2 -0
- package/SKILL.md +47 -0
- package/examples/README.md +11 -1
- package/examples/auto-approve-compound-git.sh +3 -0
- package/examples/auto-compact-context-monitor.sh +35 -0
- package/examples/auto-mode-safety-enforcer.sh +57 -0
- package/examples/background-task-guard.sh +57 -0
- package/examples/broad-find-guard.sh +62 -0
- package/examples/cache-creation-spike-detector.sh +32 -0
- package/examples/case-insensitive-path-guard.sh +96 -0
- package/examples/cjk-punctuation-guard.sh +44 -0
- package/examples/clipboard-secret-guard.sh +29 -0
- package/examples/compact-circuit-breaker.sh +72 -0
- package/examples/context-size-alert.sh +38 -0
- package/examples/context-usage-drift-alert.sh +33 -0
- package/examples/dangerous-pip-flag-guard.sh +51 -0
- package/examples/deny-bypass-detector.sh +143 -0
- package/examples/dotenv-read-guard.sh +48 -0
- package/examples/dotfile-protection-guard.sh +60 -0
- package/examples/effort-tracking-logger.sh +30 -0
- package/examples/exploration-budget-guard.sh +77 -0
- package/examples/financial-operation-guard.sh +47 -0
- package/examples/full-rewrite-detector.sh +63 -0
- package/examples/home-critical-bash-guard.sh +56 -0
- package/examples/idle-session-cost-alert.sh +36 -0
- package/examples/model-version-alert.sh +18 -0
- package/examples/model-version-change-alert.sh +31 -0
- package/examples/move-delete-sequence-guard.sh +92 -0
- package/examples/pii-upload-guard.sh +72 -0
- package/examples/pr-duplicate-guard.sh +14 -0
- package/examples/production-port-kill-guard.sh +60 -0
- package/examples/quota-reset-cycle-monitor.sh +30 -0
- package/examples/repo-visibility-guard.sh +33 -0
- package/examples/sandbox-relative-path-audit.sh +51 -0
- package/examples/session-agent-cost-limiter.sh +43 -0
- package/examples/session-cost-alert.sh +62 -0
- package/examples/session-memory-watchdog.sh +9 -0
- package/examples/settings-integrity-monitor.sh +55 -0
- package/examples/settings-json-model-guard.sh +89 -0
- package/examples/shell-config-truncation-guard.sh +97 -0
- package/examples/shell-wrapper-guard.sh +4 -4
- package/examples/subagent-spawn-rate-monitor.sh +34 -0
- package/examples/subcommand-chain-guard.sh +44 -0
- package/examples/system-dir-protection-guard.sh +100 -0
- package/examples/thinking-display-enforcer.sh +25 -0
- package/examples/thinking-stall-detector.sh +61 -0
- package/examples/tool-retry-budget-guard.sh +59 -0
- package/examples/worktree-branch-pollution-detector.sh +35 -0
- package/examples/worktree-create-log.sh +6 -0
- package/examples/worktree-hook-linker.sh +72 -0
- package/examples/worktree-remove-uncommitted-guard.sh +20 -0
- package/hooks/hooks.json +60 -0
- package/index.mjs +92 -6
- package/memory/market-anthropic-japan-strategy-2026-04-13.md +4 -0
- package/package.json +2 -2
- package/plugins/credential-guard/.claude-plugin/plugin.json +58 -0
- package/plugins/git-protection/.claude-plugin/plugin.json +58 -0
- package/plugins/safety-essentials/.claude-plugin/plugin.json +58 -0
- package/plugins/token-guard/.claude-plugin/plugin.json +51 -0
- package/skills/safety-setup/SKILL.md +47 -0
- package/tests/dotenv-read-guard.test.sh +65 -0
- package/tests/test-auto-mode-safety-enforcer.sh +55 -0
- package/tests/test-case-insensitive-path-guard.sh +78 -0
- package/tests/test-compact-circuit-breaker.sh +134 -0
- package/tests/test-context-usage-drift-alert.sh +52 -0
- package/tests/test-dangerous-pip-flag-guard.sh +56 -0
- package/tests/test-dotfile-protection-guard.sh +68 -0
- package/tests/test-effort-tracking-logger.sh +55 -0
- package/tests/test-exploration-budget-guard.sh +164 -0
- package/tests/test-financial-operation-guard.sh +59 -0
- package/tests/test-home-critical-bash-guard.sh +59 -0
- package/tests/test-model-version-change-alert.sh +55 -0
- package/tests/test-move-delete-sequence-guard.sh +63 -0
- package/tests/test-pr-duplicate-guard.sh +29 -0
- package/tests/test-quota-reset-cycle-monitor.sh +52 -0
- package/tests/test-shell-config-truncation-guard.sh +104 -0
- package/tests/test-subagent-spawn-rate-monitor.sh +43 -0
- package/tests/test-system-dir-protection-guard.sh +81 -0
- package/tests/test-thinking-stall-detector.sh +151 -0
- package/tests/test-tool-retry-budget-guard.sh +75 -0
- package/tests/test-worktree-branch-pollution-detector.sh +50 -0
- 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
|
package/hooks/hooks.json
ADDED
|
@@ -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
|
-
|
|
196
|
-
Book: https://
|
|
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 + '
|
|
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 + '
|
|
5905
|
-
console.log(' ' + c.
|
|
5906
|
-
console.log(' ' + c.dim + '
|
|
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
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "29.
|
|
4
|
-
"description": "One command to make Claude Code safe.
|
|
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"
|