cc-safe-setup 29.6.40 → 29.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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/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/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-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-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-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,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
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# dotfile-protection-guard.sh — Block writes to critical dotfiles
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude Code modifying or truncating critical user dotfiles
|
|
5
|
+
# - #49615: Installer auto-update zeroed ~/.bash_profile and ~/.zshrc
|
|
6
|
+
# - #49539: ~/.git-credentials PATs deleted without confirmation
|
|
7
|
+
# - #49554: auto mode approved ~/.ssh deletion
|
|
8
|
+
#
|
|
9
|
+
# What it blocks (Write/Edit tool):
|
|
10
|
+
# ~/.bash_profile, ~/.bashrc, ~/.zshrc, ~/.profile
|
|
11
|
+
# ~/.ssh/*, ~/.git-credentials, ~/.gitconfig
|
|
12
|
+
# ~/.gnupg/*, ~/.npmrc (may contain auth tokens)
|
|
13
|
+
# ~/.aws/credentials, ~/.config/gh/hosts.yml
|
|
14
|
+
#
|
|
15
|
+
# What it allows:
|
|
16
|
+
# Files in project directories (not under ~/ root)
|
|
17
|
+
# ~/.claude/* (Claude Code's own config)
|
|
18
|
+
#
|
|
19
|
+
# TRIGGER: PreToolUse MATCHER: "Write|Edit"
|
|
20
|
+
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
INPUT=$(cat)
|
|
24
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
25
|
+
[ -z "$FILE" ] && exit 0
|
|
26
|
+
|
|
27
|
+
# Expand ~ to actual home directory
|
|
28
|
+
HOME_DIR="$HOME"
|
|
29
|
+
RESOLVED=$(echo "$FILE" | sed "s|^~|$HOME_DIR|")
|
|
30
|
+
|
|
31
|
+
# Allow Claude Code's own config
|
|
32
|
+
if echo "$RESOLVED" | grep -qE "^${HOME_DIR}/\.claude(/|$)"; then
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Critical dotfiles — block any modification
|
|
37
|
+
CRITICAL_PATTERNS=(
|
|
38
|
+
"^${HOME_DIR}/\.(bash_profile|bashrc|zshrc|zshenv|profile|login|logout)$"
|
|
39
|
+
"^${HOME_DIR}/\.ssh(/|$)"
|
|
40
|
+
"^${HOME_DIR}/\.git-credentials$"
|
|
41
|
+
"^${HOME_DIR}/\.gitconfig$"
|
|
42
|
+
"^${HOME_DIR}/\.gnupg(/|$)"
|
|
43
|
+
"^${HOME_DIR}/\.npmrc$"
|
|
44
|
+
"^${HOME_DIR}/\.aws/(credentials|config)$"
|
|
45
|
+
"^${HOME_DIR}/\.config/gh/hosts\.yml$"
|
|
46
|
+
"^${HOME_DIR}/\.netrc$"
|
|
47
|
+
"^${HOME_DIR}/\.docker/config\.json$"
|
|
48
|
+
"^${HOME_DIR}/\.kube/config$"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
for PATTERN in "${CRITICAL_PATTERNS[@]}"; do
|
|
52
|
+
if echo "$RESOLVED" | grep -qE "$PATTERN"; then
|
|
53
|
+
echo "BLOCKED: Modifying critical dotfile: $FILE" >&2
|
|
54
|
+
echo "This file contains shell config or credentials that should not be altered by AI." >&2
|
|
55
|
+
echo "If you need to modify this file, do it manually." >&2
|
|
56
|
+
exit 2
|
|
57
|
+
fi
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
exit 0
|