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,62 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# broad-find-guard.sh — Block overly broad find/locate commands that scan the entire home directory
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude Code running `find $HOME` or `find /` which scans all user files,
|
|
5
|
+
# triggering OneDrive/iCloud/Dropbox to download synced files (#51010)
|
|
6
|
+
#
|
|
7
|
+
# Detects patterns like:
|
|
8
|
+
# find / -name "CLAUDE.md"
|
|
9
|
+
# find ~ -name "*.py"
|
|
10
|
+
# find $HOME -type f
|
|
11
|
+
# find /c/Users/username -name "*.md"
|
|
12
|
+
# locate "CLAUDE.md"
|
|
13
|
+
#
|
|
14
|
+
# Why: On Windows/macOS, cloud sync folders (OneDrive, iCloud, Dropbox) are
|
|
15
|
+
# under the home directory. A broad `find` traverses them and triggers
|
|
16
|
+
# downloading of ALL synced files — potentially gigabytes of data.
|
|
17
|
+
# Claude Code should only search project-scoped directories.
|
|
18
|
+
#
|
|
19
|
+
# Usage: Add to settings.json as a PreToolUse hook
|
|
20
|
+
#
|
|
21
|
+
# {
|
|
22
|
+
# "hooks": {
|
|
23
|
+
# "PreToolUse": [{
|
|
24
|
+
# "matcher": "Bash",
|
|
25
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/broad-find-guard.sh" }]
|
|
26
|
+
# }]
|
|
27
|
+
# }
|
|
28
|
+
# }
|
|
29
|
+
#
|
|
30
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
31
|
+
|
|
32
|
+
INPUT=$(cat)
|
|
33
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
34
|
+
|
|
35
|
+
[ -z "$COMMAND" ] && exit 0
|
|
36
|
+
|
|
37
|
+
# Pattern 1: find starting from root
|
|
38
|
+
if echo "$COMMAND" | grep -qE 'find\s+/\s'; then
|
|
39
|
+
echo "BLOCKED: find from root (/) scans entire filesystem. Use a project-scoped path instead." >&2
|
|
40
|
+
exit 2
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Pattern 2: find starting from home directory (various forms)
|
|
44
|
+
if echo "$COMMAND" | grep -qE 'find\s+(~|\$HOME|/home/[a-zA-Z0-9_]+|/Users/[a-zA-Z0-9_]+|/c/Users/[a-zA-Z0-9_]+)\s'; then
|
|
45
|
+
echo "BLOCKED: find from home directory scans all user files including cloud sync folders (OneDrive/iCloud/Dropbox). Use a specific project path instead." >&2
|
|
46
|
+
exit 2
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Pattern 3: find with -maxdepth 0 or 1 from home is OK (shallow), but deep scans are not
|
|
50
|
+
# This pattern catches find ~ without further path restriction
|
|
51
|
+
if echo "$COMMAND" | grep -qE 'find\s+(~|\$HOME|/home/|/Users/|/c/Users/)\b' && ! echo "$COMMAND" | grep -qE '\-maxdepth\s+[01]'; then
|
|
52
|
+
echo "BLOCKED: broad find from home directory. Add -maxdepth or use a specific subdirectory to avoid scanning cloud sync folders." >&2
|
|
53
|
+
exit 2
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Pattern 4: locate without project-scoping (scans entire DB)
|
|
57
|
+
if echo "$COMMAND" | grep -qE '^\s*locate\s' && ! echo "$COMMAND" | grep -qE 'locate.*--database|locate.*-d\s'; then
|
|
58
|
+
echo "BLOCKED: locate searches the entire filesystem index. Use find with a specific directory instead." >&2
|
|
59
|
+
exit 2
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
exit 0
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# cache-creation-spike-detector.sh — cache_creationスパイクを検知して警告
|
|
3
|
+
# Why: smooshSystemReminderSiblings関数がsystem-reminderを毎ターンtool_result.contentに
|
|
4
|
+
# 折り込むことで、プロンプトキャッシュのプレフィックスが変わり、cache_creationが
|
|
5
|
+
# 数十万トークン単位でスパイクする (#49585)。5xの消費率上昇報告あり (#49593)
|
|
6
|
+
# Event: PostToolUse (全ツール実行後にチェック)
|
|
7
|
+
# Action: cache_creation_input_tokensが閾値を超えた場合に警告
|
|
8
|
+
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
# PostToolUseではusageデータにアクセスできないため、
|
|
11
|
+
# /costコマンド出力のログファイルで累積cache_creationを追跡する
|
|
12
|
+
CACHE_LOG="/tmp/cc-cache-creation-tracker.log"
|
|
13
|
+
THRESHOLD=100000 # 100Kトークン以上でcache_creation警告
|
|
14
|
+
|
|
15
|
+
# 10回に1回だけチェック(パフォーマンス配慮)
|
|
16
|
+
COUNTER_FILE="/tmp/cache-spike-check-counter"
|
|
17
|
+
COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo "0")
|
|
18
|
+
COUNT=$((COUNT + 1))
|
|
19
|
+
echo "$COUNT" > "$COUNTER_FILE"
|
|
20
|
+
[ $((COUNT % 10)) -ne 0 ] && exit 0
|
|
21
|
+
|
|
22
|
+
# セッション開始からの経過時間をチェック
|
|
23
|
+
SESSION_START=$(stat -c %Y /tmp/cache-spike-check-counter 2>/dev/null || echo "0")
|
|
24
|
+
NOW=$(date +%s)
|
|
25
|
+
ELAPSED=$((NOW - SESSION_START))
|
|
26
|
+
|
|
27
|
+
# セッション開始10分以内はスキップ(初期キャッシュ構築は正常)
|
|
28
|
+
[ "$ELAPSED" -lt 600 ] && exit 0
|
|
29
|
+
|
|
30
|
+
echo "INFO: cache_creationスパイク検知hookが動作中。異常なトークン消費を感じたら /cost で確認してください。" >&2
|
|
31
|
+
echo "詳細: https://github.com/anthropics/claude-code/issues/49585" >&2
|
|
32
|
+
exit 0
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# case-insensitive-path-guard.sh — Detect path case mismatches on case-insensitive filesystems
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude Code resolving paths with wrong case on macOS APFS (case-insensitive),
|
|
5
|
+
# causing rm -rf to destroy unintended directories
|
|
6
|
+
# - #48792: rm -rf WebstormProjects/ destroyed webstormprojects/ (10 years of work)
|
|
7
|
+
# - #49102: Same APFS bug, second catastrophic loss in 48 hours
|
|
8
|
+
#
|
|
9
|
+
# How it works:
|
|
10
|
+
# For rm/mv/cp commands targeting paths under $HOME, resolves the ACTUAL
|
|
11
|
+
# filesystem path and compares case. If the specified path exists only via
|
|
12
|
+
# case-insensitive matching (e.g., "Projects" matches "projects/"), blocks
|
|
13
|
+
# the command because the user likely intended a different directory.
|
|
14
|
+
#
|
|
15
|
+
# Platform: macOS only (APFS case-insensitive is default). On Linux (ext4, case-sensitive),
|
|
16
|
+
# the guard exits cleanly since case mismatches simply won't resolve.
|
|
17
|
+
#
|
|
18
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
19
|
+
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
INPUT=$(cat)
|
|
23
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
24
|
+
[ -z "$COMMAND" ] && exit 0
|
|
25
|
+
|
|
26
|
+
# Only check destructive commands
|
|
27
|
+
echo "$COMMAND" | grep -qE '\b(rm|mv|cp)\s' || exit 0
|
|
28
|
+
|
|
29
|
+
# Skip if not macOS (Linux filesystems are case-sensitive by default)
|
|
30
|
+
if [ "$(uname)" != "Darwin" ]; then
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Extract target paths from destructive commands
|
|
35
|
+
# Handles: rm -rf path, mv path dest, cp -r path dest
|
|
36
|
+
TARGETS=""
|
|
37
|
+
|
|
38
|
+
# rm: all non-flag arguments
|
|
39
|
+
if echo "$COMMAND" | grep -qE '\brm\s'; then
|
|
40
|
+
TARGETS=$(echo "$COMMAND" | grep -oP '\brm\s+(-[a-zA-Z]+\s+)*\K[^\s;&|]+(\s+[^\s;&|]+)*' 2>/dev/null || true)
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# mv: first non-flag argument (source)
|
|
44
|
+
if echo "$COMMAND" | grep -qE '\bmv\s'; then
|
|
45
|
+
MV_TARGET=$(echo "$COMMAND" | grep -oP '\bmv\s+(-[a-zA-Z]+\s+)*\K\S+' 2>/dev/null || true)
|
|
46
|
+
TARGETS="$TARGETS $MV_TARGET"
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
[ -z "$TARGETS" ] && exit 0
|
|
50
|
+
|
|
51
|
+
# Expand ~ to $HOME
|
|
52
|
+
TARGETS=$(echo "$TARGETS" | sed "s|~|$HOME|g")
|
|
53
|
+
|
|
54
|
+
for target in $TARGETS; do
|
|
55
|
+
# Skip flags
|
|
56
|
+
echo "$target" | grep -q '^-' && continue
|
|
57
|
+
|
|
58
|
+
# Skip safe disposable paths
|
|
59
|
+
echo "$target" | grep -qE '(node_modules|\.cache|__pycache__|/tmp/|dist/|build/)' && continue
|
|
60
|
+
|
|
61
|
+
# Only check paths under home directory (most risk)
|
|
62
|
+
echo "$target" | grep -qE "^($HOME|~)" || continue
|
|
63
|
+
|
|
64
|
+
# Resolve the canonical path using the filesystem
|
|
65
|
+
# On case-insensitive APFS, /Users/me/Projects resolves even if actual is /Users/me/projects
|
|
66
|
+
CANONICAL=$(python3 -c "
|
|
67
|
+
import os, sys
|
|
68
|
+
p = os.path.expanduser('$target')
|
|
69
|
+
# Walk up to find the longest existing prefix
|
|
70
|
+
check = p
|
|
71
|
+
while check and not os.path.exists(check):
|
|
72
|
+
check = os.path.dirname(check)
|
|
73
|
+
if not check:
|
|
74
|
+
sys.exit(0)
|
|
75
|
+
# Get the real path (canonical case)
|
|
76
|
+
real = os.path.realpath(check)
|
|
77
|
+
# Get the suffix that was beyond the existing part
|
|
78
|
+
suffix = p[len(check):]
|
|
79
|
+
print(real + suffix)
|
|
80
|
+
" 2>/dev/null || echo "")
|
|
81
|
+
|
|
82
|
+
[ -z "$CANONICAL" ] && continue
|
|
83
|
+
|
|
84
|
+
# Compare: if the specified path differs in case from the canonical path
|
|
85
|
+
if [ "$target" != "$CANONICAL" ] && [ "${target,,}" = "${CANONICAL,,}" ] 2>/dev/null; then
|
|
86
|
+
echo "BLOCKED: Case mismatch detected on case-insensitive filesystem" >&2
|
|
87
|
+
echo " Specified: $target" >&2
|
|
88
|
+
echo " Actual: $CANONICAL" >&2
|
|
89
|
+
echo " On macOS APFS, these resolve to the SAME directory." >&2
|
|
90
|
+
echo " This mismatch has caused catastrophic data loss (#48792, #49102)." >&2
|
|
91
|
+
echo " Verify the path case matches the actual filesystem before proceeding." >&2
|
|
92
|
+
exit 2
|
|
93
|
+
fi
|
|
94
|
+
done
|
|
95
|
+
|
|
96
|
+
exit 0
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# cjk-punctuation-guard.sh — Detect CJK punctuation corruption after Write/Edit
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude Code's Write and Edit tools silently convert full-width CJK
|
|
5
|
+
# punctuation to half-width ASCII (,→, 。→. 「→" etc.) without warning.
|
|
6
|
+
# This corrupts Chinese, Japanese, and Korean text.
|
|
7
|
+
# (GitHub Issue #50975)
|
|
8
|
+
#
|
|
9
|
+
# How it works:
|
|
10
|
+
# After Write or Edit, checks if the file contains CJK characters.
|
|
11
|
+
# If yes, runs git diff to see if any full-width punctuation was replaced
|
|
12
|
+
# with half-width equivalents in the same commit.
|
|
13
|
+
#
|
|
14
|
+
# TRIGGER: PostToolUse MATCHER: "Write|Edit"
|
|
15
|
+
|
|
16
|
+
INPUT=$(cat)
|
|
17
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.file // empty' 2>/dev/null)
|
|
18
|
+
|
|
19
|
+
[ -z "$FILE" ] || [ ! -f "$FILE" ] && exit 0
|
|
20
|
+
|
|
21
|
+
# Only check files that contain CJK characters
|
|
22
|
+
if ! grep -Pq '[\p{Han}\p{Hiragana}\p{Katakana}\p{Hangul}]' "$FILE" 2>/dev/null; then
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Check git diff for punctuation changes (full-width → half-width)
|
|
27
|
+
DIFF=$(git diff -- "$FILE" 2>/dev/null)
|
|
28
|
+
[ -z "$DIFF" ] && exit 0
|
|
29
|
+
|
|
30
|
+
# Detect suspicious patterns: removed full-width, added half-width in same hunk
|
|
31
|
+
# Common corruptions: ,→, 。→. 、→, :→: ;→; !→! ?→? 「→" 」→" (→( )→)
|
|
32
|
+
REMOVED_FW=$(echo "$DIFF" | grep '^-' | grep -Pc '[,。、:;!?「」()【】『』]' 2>/dev/null || echo 0)
|
|
33
|
+
ADDED_HW=$(echo "$DIFF" | grep '^+' | grep -Pc '[,\.;:!\?\(\)\[\]]' 2>/dev/null || echo 0)
|
|
34
|
+
|
|
35
|
+
if [ "$REMOVED_FW" -gt 0 ] && [ "$ADDED_HW" -gt 0 ]; then
|
|
36
|
+
echo "⚠ WARNING: CJK punctuation may have been corrupted in $FILE" >&2
|
|
37
|
+
echo " $REMOVED_FW lines with full-width punctuation removed" >&2
|
|
38
|
+
echo " $ADDED_HW lines with half-width punctuation added" >&2
|
|
39
|
+
echo " Review: git diff -- \"$FILE\"" >&2
|
|
40
|
+
echo " Undo: git checkout -- \"$FILE\"" >&2
|
|
41
|
+
# Warning only (exit 0), not blocking — the write already happened
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
exit 0
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# clipboard-secret-guard.sh — Block secrets from being copied to clipboard
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude Code may pipe sensitive data (API keys, tokens, passwords)
|
|
5
|
+
# to clipboard utilities (pbcopy, xclip, xsel, wl-copy). This leaks
|
|
6
|
+
# secrets outside the terminal where they persist and may sync to cloud.
|
|
7
|
+
#
|
|
8
|
+
# How it works: PreToolUse hook that detects clipboard commands containing
|
|
9
|
+
# secret-like patterns. Blocks the operation.
|
|
10
|
+
#
|
|
11
|
+
# TRIGGER: PreToolUse
|
|
12
|
+
# MATCHER: "Bash"
|
|
13
|
+
# CATEGORY: security
|
|
14
|
+
|
|
15
|
+
INPUT=$(cat)
|
|
16
|
+
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
17
|
+
[ -z "$CMD" ] && exit 0
|
|
18
|
+
|
|
19
|
+
# Check if command pipes to clipboard
|
|
20
|
+
if echo "$CMD" | grep -qiE '(pbcopy|xclip|xsel|wl-copy|clip\.exe)'; then
|
|
21
|
+
# Check if the piped content looks like it contains secrets
|
|
22
|
+
if echo "$CMD" | grep -qiE '(api.?key|secret|token|password|passwd|credential|private.?key|Bearer|AWS_|ANTHROPIC_|OPENAI_)'; then
|
|
23
|
+
echo "BLOCKED: Attempting to copy secret-like content to clipboard." >&2
|
|
24
|
+
echo " Clipboard data may sync to cloud or persist after session." >&2
|
|
25
|
+
exit 2
|
|
26
|
+
fi
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
exit 0
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# compact-circuit-breaker.sh — Prevent auto-compact death spirals
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Auto-compact can enter an infinite loop when FileHistory
|
|
7
|
+
# recovery is degraded — each compaction fails to restore
|
|
8
|
+
# continuity, triggering the next one immediately. Users have
|
|
9
|
+
# lost entire overnight token budgets to 15+ consecutive
|
|
10
|
+
# compactions with zero forward progress (#51088). One incident
|
|
11
|
+
# recorded 211 compactions in a single session (#24179).
|
|
12
|
+
#
|
|
13
|
+
# This hook acts as a circuit breaker: it allows normal
|
|
14
|
+
# compaction but blocks rapid-fire compaction that indicates
|
|
15
|
+
# a death spiral. After MAX_PER_HOUR compactions, further
|
|
16
|
+
# attempts are blocked until the window resets.
|
|
17
|
+
#
|
|
18
|
+
# TRIGGER: PreCompact
|
|
19
|
+
# MATCHER: (none — PreCompact has no matcher)
|
|
20
|
+
#
|
|
21
|
+
# DECISION: exit 0 = allow, exit 2 = block
|
|
22
|
+
#
|
|
23
|
+
# CONFIG:
|
|
24
|
+
# MAX_PER_HOUR — Maximum compactions allowed per hour (default: 3)
|
|
25
|
+
# MIN_INTERVAL — Minimum seconds between compactions (default: 120)
|
|
26
|
+
#
|
|
27
|
+
# See: https://github.com/anthropics/claude-code/issues/51088
|
|
28
|
+
# https://github.com/anthropics/claude-code/issues/24179
|
|
29
|
+
# ================================================================
|
|
30
|
+
|
|
31
|
+
MAX_PER_HOUR="${CC_COMPACT_MAX_PER_HOUR:-3}"
|
|
32
|
+
MIN_INTERVAL="${CC_COMPACT_MIN_INTERVAL:-120}"
|
|
33
|
+
STATE_DIR="/tmp/.cc-compact-circuit-breaker"
|
|
34
|
+
STATE_FILE="$STATE_DIR/compaction-log"
|
|
35
|
+
|
|
36
|
+
mkdir -p "$STATE_DIR"
|
|
37
|
+
touch "$STATE_FILE"
|
|
38
|
+
|
|
39
|
+
NOW=$(date +%s)
|
|
40
|
+
ONE_HOUR_AGO=$((NOW - 3600))
|
|
41
|
+
|
|
42
|
+
# Clean old entries (older than 1 hour)
|
|
43
|
+
if [ -f "$STATE_FILE" ]; then
|
|
44
|
+
awk -v cutoff="$ONE_HOUR_AGO" '$1 >= cutoff' "$STATE_FILE" > "$STATE_FILE.tmp"
|
|
45
|
+
mv "$STATE_FILE.tmp" "$STATE_FILE"
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Count compactions in the last hour
|
|
49
|
+
RECENT_COUNT=$(wc -l < "$STATE_FILE" | tr -d ' ')
|
|
50
|
+
|
|
51
|
+
# Check minimum interval since last compaction
|
|
52
|
+
LAST_TIME=0
|
|
53
|
+
if [ -s "$STATE_FILE" ]; then
|
|
54
|
+
LAST_TIME=$(tail -1 "$STATE_FILE")
|
|
55
|
+
fi
|
|
56
|
+
ELAPSED=$((NOW - LAST_TIME))
|
|
57
|
+
|
|
58
|
+
# Circuit breaker: block if too many compactions
|
|
59
|
+
if [ "$RECENT_COUNT" -ge "$MAX_PER_HOUR" ]; then
|
|
60
|
+
echo "CIRCUIT BREAKER: $RECENT_COUNT compactions in the last hour (max: $MAX_PER_HOUR). Possible death spiral detected. Start a fresh session instead of compacting." >&2
|
|
61
|
+
exit 2
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# Cooldown: block if too soon after last compaction
|
|
65
|
+
if [ "$ELAPSED" -lt "$MIN_INTERVAL" ] && [ "$LAST_TIME" -gt 0 ]; then
|
|
66
|
+
echo "COOLDOWN: Last compaction was ${ELAPSED}s ago (min interval: ${MIN_INTERVAL}s). Wait before compacting again." >&2
|
|
67
|
+
exit 2
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# Allow compaction and log it
|
|
71
|
+
echo "$NOW" >> "$STATE_FILE"
|
|
72
|
+
exit 0
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# context-size-alert.sh — Warn when context window usage exceeds threshold
|
|
3
|
+
# Trigger: PostToolUse
|
|
4
|
+
# Matcher: (empty — runs after every tool use)
|
|
5
|
+
#
|
|
6
|
+
# Since v2.1.100, the system prompt grew ~40-50% (~4,000 tokens).
|
|
7
|
+
# This hook monitors context usage and warns before you hit limits.
|
|
8
|
+
# See: https://github.com/anthropics/claude-code/issues/46339
|
|
9
|
+
#
|
|
10
|
+
# Optimization tips: https://zenn.dev/yurukusa/books/token-savings-guide
|
|
11
|
+
#
|
|
12
|
+
# TRIGGER: PostToolUse MATCHER: ""
|
|
13
|
+
|
|
14
|
+
INPUT=$(cat)
|
|
15
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
|
|
16
|
+
|
|
17
|
+
# Only check every 10th invocation to minimize overhead
|
|
18
|
+
COUNTER_FILE="/tmp/context-size-alert-${SESSION_ID:-default}.count"
|
|
19
|
+
COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo 0)
|
|
20
|
+
COUNT=$((COUNT + 1))
|
|
21
|
+
echo "$COUNT" > "$COUNTER_FILE"
|
|
22
|
+
[ $((COUNT % 10)) -ne 0 ] && exit 0
|
|
23
|
+
|
|
24
|
+
# Check if /cost or /context data is available via transcript
|
|
25
|
+
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)
|
|
26
|
+
[ -z "$TRANSCRIPT" ] && exit 0
|
|
27
|
+
|
|
28
|
+
# Count approximate context by transcript file size (rough proxy)
|
|
29
|
+
if [ -f "$TRANSCRIPT" ]; then
|
|
30
|
+
SIZE_KB=$(du -k "$TRANSCRIPT" | cut -f1)
|
|
31
|
+
# Rough threshold: 500KB transcript ≈ approaching context limits
|
|
32
|
+
if [ "$SIZE_KB" -gt 500 ]; then
|
|
33
|
+
echo "⚠ Context is large (~${SIZE_KB}KB transcript). Consider /compact or /clear to reduce token cost." >&2
|
|
34
|
+
echo " Tip: Run /context to see exact usage breakdown." >&2
|
|
35
|
+
fi
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
exit 0
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# context-usage-drift-alert.sh — コンテキスト使用率の急増を検知
|
|
3
|
+
# Why: 1Mコンテキストモデルで実際124%使用中にUI上60%と表示される問題 (#50204)。
|
|
4
|
+
# 予告なくauto-compactが発火してコンテキストが消失する。
|
|
5
|
+
# ツール呼び出し回数でコンテキスト消費を推定し、警告する。
|
|
6
|
+
# Event: PostToolUse MATCHER: ""
|
|
7
|
+
# Action: セッション内のツール呼び出し回数が閾値を超えたら警告
|
|
8
|
+
|
|
9
|
+
COUNTER_FILE="/tmp/cc-context-usage-counter-$$"
|
|
10
|
+
# セッションPIDが変わると新しいカウンターになる
|
|
11
|
+
# フォールバック: 親PIDでグループ化
|
|
12
|
+
if [ ! -f "$COUNTER_FILE" ]; then
|
|
13
|
+
COUNTER_FILE="/tmp/cc-context-usage-counter-$(date +%Y%m%d)"
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo "0")
|
|
17
|
+
COUNT=$((COUNT + 1))
|
|
18
|
+
echo "$COUNT" > "$COUNTER_FILE"
|
|
19
|
+
|
|
20
|
+
# 50回: 注意喚起、100回: 強い警告、150回: compact推奨
|
|
21
|
+
if [ "$COUNT" -eq 50 ]; then
|
|
22
|
+
echo "📊 Session checkpoint: $COUNT tool calls. Context may be growing large." >&2
|
|
23
|
+
echo "Run /cost to check actual usage. UI display may undercount by 2x (#50204)." >&2
|
|
24
|
+
elif [ "$COUNT" -eq 100 ]; then
|
|
25
|
+
echo "⚠ HIGH CONTEXT USAGE: $COUNT tool calls this session." >&2
|
|
26
|
+
echo "UI may show ~50% when actual usage is near 100%. Consider /compact." >&2
|
|
27
|
+
echo "Unexpected auto-compact can erase your working context. See: #50204" >&2
|
|
28
|
+
elif [ "$COUNT" -eq 150 ]; then
|
|
29
|
+
echo "🚨 VERY HIGH CONTEXT: $COUNT tool calls. Auto-compact likely imminent." >&2
|
|
30
|
+
echo "Save important state to files NOW. Run /compact manually to control what's kept." >&2
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
exit 0
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# dangerous-pip-flag-guard.sh — Block dangerous pip flags that bypass safety
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude Code using pip install with dangerous flags in auto mode
|
|
5
|
+
# - #48992: pip install --break-system-packages passed through auto mode
|
|
6
|
+
# - Breaks system Python installations, can render OS tools unusable
|
|
7
|
+
#
|
|
8
|
+
# What it blocks:
|
|
9
|
+
# --break-system-packages (bypasses PEP 668 externally-managed check)
|
|
10
|
+
# --force-reinstall combined with system paths
|
|
11
|
+
# pip install targeting /usr/lib or /usr/local/lib directly
|
|
12
|
+
#
|
|
13
|
+
# What it allows:
|
|
14
|
+
# pip install in virtual environments (venv/conda)
|
|
15
|
+
# pip install --user (installs to user directory)
|
|
16
|
+
# pip install in project directories
|
|
17
|
+
#
|
|
18
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
19
|
+
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
INPUT=$(cat)
|
|
23
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
24
|
+
[ -z "$COMMAND" ] && exit 0
|
|
25
|
+
|
|
26
|
+
# Only check pip commands
|
|
27
|
+
echo "$COMMAND" | grep -qE '\bpip[3]?\s+install\b' || exit 0
|
|
28
|
+
|
|
29
|
+
# Block --break-system-packages
|
|
30
|
+
if echo "$COMMAND" | grep -qE '\-\-break-system-packages'; then
|
|
31
|
+
echo "BLOCKED: --break-system-packages flag detected" >&2
|
|
32
|
+
echo "This flag bypasses PEP 668 protection and can break your system Python." >&2
|
|
33
|
+
echo "Use a virtual environment instead: python3 -m venv .venv && source .venv/bin/activate" >&2
|
|
34
|
+
exit 2
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Block sudo pip install (system-wide install without venv)
|
|
38
|
+
if echo "$COMMAND" | grep -qE '\bsudo\s+pip[3]?\s+install\b'; then
|
|
39
|
+
echo "BLOCKED: sudo pip install detected" >&2
|
|
40
|
+
echo "System-wide pip install can break OS tools. Use a virtual environment." >&2
|
|
41
|
+
exit 2
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Block pip targeting system directories
|
|
45
|
+
if echo "$COMMAND" | grep -qE '\bpip[3]?\s+install\b.*\-\-target\s*=?\s*/(usr|opt|lib)'; then
|
|
46
|
+
echo "BLOCKED: pip install targeting system directory" >&2
|
|
47
|
+
echo "Installing packages to system directories can break OS tools." >&2
|
|
48
|
+
exit 2
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
exit 0
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# deny-bypass-detector.sh — Detect when Claude circumvents a hook denial
|
|
3
|
+
#
|
|
4
|
+
# Solves: After a PreToolUse hook blocks a command (exit 2), Claude
|
|
5
|
+
# reformulates the same operation as a script wrapper, eval, or
|
|
6
|
+
# bash -c to evade pattern matching. (#46991)
|
|
7
|
+
#
|
|
8
|
+
# How it works: Two-phase detection:
|
|
9
|
+
# Phase 1 (PostToolUse): When a Bash command is blocked (tool_result
|
|
10
|
+
# contains deny/block signals), log the dangerous substrings.
|
|
11
|
+
# Phase 2 (PreToolUse): Before each Bash command, check if it
|
|
12
|
+
# contains a recently-denied substring wrapped in bash -c, sh -c,
|
|
13
|
+
# eval, or a temp script.
|
|
14
|
+
#
|
|
15
|
+
# Denied commands expire after 60 seconds to avoid permanent lockout.
|
|
16
|
+
#
|
|
17
|
+
# Usage: Add TWO hooks — PostToolUse to log denials, PreToolUse to detect bypass
|
|
18
|
+
#
|
|
19
|
+
# {
|
|
20
|
+
# "hooks": {
|
|
21
|
+
# "PostToolUse": [{
|
|
22
|
+
# "matcher": "Bash",
|
|
23
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/deny-bypass-detector.sh" }]
|
|
24
|
+
# }],
|
|
25
|
+
# "PreToolUse": [{
|
|
26
|
+
# "matcher": "Bash",
|
|
27
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/deny-bypass-detector.sh" }]
|
|
28
|
+
# }]
|
|
29
|
+
# }
|
|
30
|
+
# }
|
|
31
|
+
#
|
|
32
|
+
# TRIGGER: PostToolUse+PreToolUse MATCHER: "Bash"
|
|
33
|
+
|
|
34
|
+
set -euo pipefail
|
|
35
|
+
|
|
36
|
+
DENY_LOG="/tmp/cc-deny-bypass-log"
|
|
37
|
+
mkdir -p "$(dirname "$DENY_LOG")" 2>/dev/null || true
|
|
38
|
+
|
|
39
|
+
INPUT=$(cat)
|
|
40
|
+
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // empty' 2>/dev/null)
|
|
41
|
+
|
|
42
|
+
# --- Phase 1: PostToolUse — log denied commands ---
|
|
43
|
+
if [[ "$HOOK_EVENT" == "PostToolUse" ]]; then
|
|
44
|
+
RESULT=$(echo "$INPUT" | jq -r '.tool_result // empty' 2>/dev/null)
|
|
45
|
+
# Detect denial signals in tool result
|
|
46
|
+
if echo "$RESULT" | grep -qiE 'BLOCKED|exit.*(code|status).*2|hook.*denied|hook.*blocked'; then
|
|
47
|
+
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
48
|
+
[ -z "$CMD" ] && exit 0
|
|
49
|
+
# Extract dangerous substrings: the core operation
|
|
50
|
+
# e.g., from "rm -rf node_modules" extract "rm -rf" and "node_modules"
|
|
51
|
+
TIMESTAMP=$(date +%s)
|
|
52
|
+
# Log the full command and key fragments
|
|
53
|
+
echo "${TIMESTAMP}|${CMD}" >> "$DENY_LOG"
|
|
54
|
+
# Also extract individual dangerous tokens
|
|
55
|
+
for token in $(echo "$CMD" | grep -oE '(rm\s+-rf|git\s+push\s+--force|git\s+reset\s+--hard|git\s+clean|chmod\s+777|curl.*\|.*sh|wget.*\|.*sh)' 2>/dev/null); do
|
|
56
|
+
echo "${TIMESTAMP}|PATTERN:${token}" >> "$DENY_LOG"
|
|
57
|
+
done
|
|
58
|
+
fi
|
|
59
|
+
exit 0
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# --- Phase 2: PreToolUse — detect bypass attempts ---
|
|
63
|
+
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
64
|
+
[ -z "$CMD" ] && exit 0
|
|
65
|
+
[ ! -f "$DENY_LOG" ] && exit 0
|
|
66
|
+
|
|
67
|
+
NOW=$(date +%s)
|
|
68
|
+
CUTOFF=$((NOW - 60))
|
|
69
|
+
|
|
70
|
+
# Clean expired entries
|
|
71
|
+
if [ -f "$DENY_LOG" ]; then
|
|
72
|
+
awk -F'|' -v cutoff="$CUTOFF" '$1 >= cutoff' "$DENY_LOG" > "${DENY_LOG}.tmp" 2>/dev/null
|
|
73
|
+
mv "${DENY_LOG}.tmp" "$DENY_LOG" 2>/dev/null || true
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# Check if current command wraps a denied command
|
|
77
|
+
BYPASS_DETECTED=0
|
|
78
|
+
DENIED_CMD=""
|
|
79
|
+
|
|
80
|
+
while IFS='|' read -r ts denied_cmd; do
|
|
81
|
+
[ "$ts" -lt "$CUTOFF" ] 2>/dev/null && continue
|
|
82
|
+
[ -z "$denied_cmd" ] && continue
|
|
83
|
+
|
|
84
|
+
# Skip pattern entries for this check
|
|
85
|
+
[[ "$denied_cmd" == PATTERN:* ]] && continue
|
|
86
|
+
|
|
87
|
+
# Check 1: bash -c / sh -c / eval wrapping the denied command
|
|
88
|
+
if echo "$CMD" | grep -qE '(bash|sh)\s+-c\s' || echo "$CMD" | grep -qE '\beval\s'; then
|
|
89
|
+
# Extract the inner command from the wrapper
|
|
90
|
+
INNER=$(echo "$CMD" | sed -E "s/.*(bash|sh)\s+-c\s+['\"]?//" | sed -E "s/['\"]?\s*$//")
|
|
91
|
+
# Check if inner command is similar to denied command
|
|
92
|
+
# Use key fragments: first word + arguments
|
|
93
|
+
DENIED_CORE=$(echo "$denied_cmd" | awk '{print $1}')
|
|
94
|
+
if echo "$INNER" | grep -qF "$DENIED_CORE"; then
|
|
95
|
+
BYPASS_DETECTED=1
|
|
96
|
+
DENIED_CMD="$denied_cmd"
|
|
97
|
+
break
|
|
98
|
+
fi
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# Check 2: Writing denied command to a temp script then executing
|
|
102
|
+
if echo "$CMD" | grep -qE '(cat|echo|printf).*>.*\.(sh|bash|tmp)' || \
|
|
103
|
+
echo "$CMD" | grep -qE 'python3?\s+-c|node\s+-e'; then
|
|
104
|
+
DENIED_CORE=$(echo "$denied_cmd" | awk '{print $1}')
|
|
105
|
+
if echo "$CMD" | grep -qF "$DENIED_CORE"; then
|
|
106
|
+
BYPASS_DETECTED=1
|
|
107
|
+
DENIED_CMD="$denied_cmd"
|
|
108
|
+
break
|
|
109
|
+
fi
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# Check 3: Direct re-execution (same command within 60s)
|
|
113
|
+
if [ "$CMD" = "$denied_cmd" ]; then
|
|
114
|
+
BYPASS_DETECTED=1
|
|
115
|
+
DENIED_CMD="$denied_cmd"
|
|
116
|
+
break
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
done < "$DENY_LOG"
|
|
120
|
+
|
|
121
|
+
# Also check pattern-based detection
|
|
122
|
+
if [ "$BYPASS_DETECTED" -eq 0 ]; then
|
|
123
|
+
while IFS='|' read -r ts pattern_entry; do
|
|
124
|
+
[ "$ts" -lt "$CUTOFF" ] 2>/dev/null && continue
|
|
125
|
+
[[ "$pattern_entry" != PATTERN:* ]] && continue
|
|
126
|
+
PATTERN="${pattern_entry#PATTERN:}"
|
|
127
|
+
if echo "$CMD" | grep -qiE "$PATTERN"; then
|
|
128
|
+
BYPASS_DETECTED=1
|
|
129
|
+
DENIED_CMD="(pattern: $PATTERN)"
|
|
130
|
+
break
|
|
131
|
+
fi
|
|
132
|
+
done < "$DENY_LOG"
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
if [ "$BYPASS_DETECTED" -eq 1 ]; then
|
|
136
|
+
echo "BLOCKED: Bypass attempt detected." >&2
|
|
137
|
+
echo " A similar command was denied <60 seconds ago: $DENIED_CMD" >&2
|
|
138
|
+
echo " Wrapping denied commands in scripts or eval does not change the policy." >&2
|
|
139
|
+
echo " Ask the user for explicit permission before retrying." >&2
|
|
140
|
+
exit 2
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
exit 0
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# dotenv-read-guard.sh — Block Read/Glob/Grep of .env files
|
|
3
|
+
#
|
|
4
|
+
# Solves: Sub-agents (especially Explore) reading .env files and exposing
|
|
5
|
+
# API keys, tokens, and secrets in the conversation transcript.
|
|
6
|
+
# (#51030 — Explore agent read .env, exposed 5 API keys, $50 damage)
|
|
7
|
+
# (#30731 — credentials exposed in output)
|
|
8
|
+
#
|
|
9
|
+
# This hook catches the Read tool accessing .env files, which
|
|
10
|
+
# credential-file-cat-guard.sh misses (it only covers Bash cat commands).
|
|
11
|
+
# Sub-agents inherit hooks but NOT memory/security instructions, making
|
|
12
|
+
# this hook essential for preventing secret leaks in multi-agent workflows.
|
|
13
|
+
#
|
|
14
|
+
# Usage: Add to settings.json as a PreToolUse hook
|
|
15
|
+
#
|
|
16
|
+
# {
|
|
17
|
+
# "hooks": {
|
|
18
|
+
# "PreToolUse": [{
|
|
19
|
+
# "matcher": "Read",
|
|
20
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/dotenv-read-guard.sh" }]
|
|
21
|
+
# }]
|
|
22
|
+
# }
|
|
23
|
+
# }
|
|
24
|
+
#
|
|
25
|
+
# TRIGGER: PreToolUse MATCHER: "Read"
|
|
26
|
+
|
|
27
|
+
INPUT=$(cat)
|
|
28
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
29
|
+
|
|
30
|
+
[ -z "$FILE_PATH" ] && exit 0
|
|
31
|
+
|
|
32
|
+
BASENAME=$(basename "$FILE_PATH")
|
|
33
|
+
|
|
34
|
+
# Block .env and all variants (.env.local, .env.production, .env.staging, etc.)
|
|
35
|
+
# Allow .env.example, .env.sample, .env.template (safe reference files)
|
|
36
|
+
if echo "$BASENAME" | grep -qE '^\.env(\.example|\.sample|\.template)$'; then
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if echo "$BASENAME" | grep -qE '^\.env(\..+)?$'; then
|
|
41
|
+
echo "BLOCKED: Reading $BASENAME — contains secrets (API keys, tokens)" >&2
|
|
42
|
+
echo " .env files should never be read by Claude Code." >&2
|
|
43
|
+
echo " If you need to check which variables are set, read .env.example instead." >&2
|
|
44
|
+
echo " Related: GitHub Issue #51030 (sub-agent exposed 5 API keys)" >&2
|
|
45
|
+
exit 2
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
exit 0
|