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,30 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# quota-reset-cycle-monitor.sh — quotaリセット周期の変更を検知
|
|
3
|
+
# Why: ユーザーのquotaリセット周期が予告なく月曜→金曜に変更された (#49599, 2r/4c)。
|
|
4
|
+
# リセット日を追跡し、周期変更時に警告する。
|
|
5
|
+
# 突然のquota枯渇の原因究明に役立つ。
|
|
6
|
+
# Event: Notification MATCHER: ""
|
|
7
|
+
# Action: 日次でquotaリセット日を記録、周期変更を検知
|
|
8
|
+
|
|
9
|
+
RESET_LOG="/tmp/cc-quota-reset-history"
|
|
10
|
+
TODAY=$(date +%u) # 1=Monday, 7=Sunday
|
|
11
|
+
TODAY_DATE=$(date +%Y-%m-%d)
|
|
12
|
+
|
|
13
|
+
# 1日1回だけチェック(日付で制御)
|
|
14
|
+
LAST_CHECK=$(head -1 "$RESET_LOG" 2>/dev/null | cut -d'|' -f1)
|
|
15
|
+
[ "$LAST_CHECK" = "$TODAY_DATE" ] && exit 0
|
|
16
|
+
|
|
17
|
+
# /costの出力からリセット情報を取得する方法の案内
|
|
18
|
+
# 実際のリセット検知は手動確認が必要だが、ログを残すことで追跡可能
|
|
19
|
+
echo "$TODAY_DATE|$TODAY" >> "$RESET_LOG"
|
|
20
|
+
|
|
21
|
+
# リセット履歴が2件以上ある場合、周期を分析
|
|
22
|
+
ENTRIES=$(wc -l < "$RESET_LOG" 2>/dev/null || echo "0")
|
|
23
|
+
if [ "$ENTRIES" -ge 7 ]; then
|
|
24
|
+
# 過去7日の曜日パターンを表示(週末にquotaが増えたら次週リセット=正常)
|
|
25
|
+
echo "📊 Quota tracking: $ENTRIES days logged. Run '/cost' to check current reset day." >&2
|
|
26
|
+
echo "Known issue: reset cycle may change without notice (#49599)." >&2
|
|
27
|
+
echo "If your quota resets on a different day than expected, report to Anthropic support." >&2
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
exit 0
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# repo-visibility-guard.sh — Block repository visibility changes
|
|
3
|
+
# Prevents Claude Code from making private repos public (or vice versa).
|
|
4
|
+
# Incident: #50353 — Opus 4.7 ran `gh repo edit --visibility public` autonomously,
|
|
5
|
+
# exposing a hardcoded private key. Wallet drained $413 in 60-90 seconds.
|
|
6
|
+
#
|
|
7
|
+
# Hook config (settings.json):
|
|
8
|
+
# {
|
|
9
|
+
# "hooks": {
|
|
10
|
+
# "PreToolUse": [{
|
|
11
|
+
# "matcher": "Bash",
|
|
12
|
+
# "hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/repo-visibility-guard.sh" }]
|
|
13
|
+
# }]
|
|
14
|
+
# }
|
|
15
|
+
# }
|
|
16
|
+
|
|
17
|
+
INPUT=$(cat)
|
|
18
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
19
|
+
[ -z "$COMMAND" ] && exit 0
|
|
20
|
+
|
|
21
|
+
# Block gh repo edit --visibility (public/private/internal)
|
|
22
|
+
if echo "$COMMAND" | grep -qE 'gh\s+repo\s+edit\s+--visibility'; then
|
|
23
|
+
echo "BLOCKED: repository visibility change requires manual confirmation. See #50353." >&2
|
|
24
|
+
exit 2
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Block git push with --set-upstream to unknown remotes (potential exfiltration)
|
|
28
|
+
if echo "$COMMAND" | grep -qE 'git\s+remote\s+add\s' && echo "$COMMAND" | grep -qE 'git\s+push'; then
|
|
29
|
+
echo "BLOCKED: adding remote and pushing in one command. Review the remote URL first." >&2
|
|
30
|
+
exit 2
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
exit 0
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# sandbox-relative-path-audit.sh — Detect relative paths in sandbox settings that are silently ignored
|
|
3
|
+
#
|
|
4
|
+
# CRITICAL: denyWrite, denyRead, and allowWrite in settings.json only work
|
|
5
|
+
# with ABSOLUTE paths. Relative paths are SILENTLY IGNORED — no error, no
|
|
6
|
+
# warning, and zero protection. Users who think they've protected sensitive
|
|
7
|
+
# directories may have no actual protection.
|
|
8
|
+
#
|
|
9
|
+
# Born from: https://github.com/anthropics/claude-code/issues/50454
|
|
10
|
+
#
|
|
11
|
+
# TRIGGER: PreToolUse MATCHER: "Bash|Write|Edit"
|
|
12
|
+
# Best used as a Notification hook (exit 0 always) to alert without blocking.
|
|
13
|
+
|
|
14
|
+
INPUT=$(cat)
|
|
15
|
+
# Only run once per session (check marker file)
|
|
16
|
+
MARKER="/tmp/cc-sandbox-audit-$$"
|
|
17
|
+
[ -f "$MARKER" ] && exit 0
|
|
18
|
+
touch "$MARKER"
|
|
19
|
+
|
|
20
|
+
# Find settings.json locations
|
|
21
|
+
SETTINGS_FILES=""
|
|
22
|
+
[ -f "$HOME/.claude/settings.json" ] && SETTINGS_FILES="$HOME/.claude/settings.json"
|
|
23
|
+
[ -f ".claude/settings.json" ] && SETTINGS_FILES="$SETTINGS_FILES .claude/settings.json"
|
|
24
|
+
[ -f "$HOME/.claude/settings.local.json" ] && SETTINGS_FILES="$SETTINGS_FILES $HOME/.claude/settings.local.json"
|
|
25
|
+
|
|
26
|
+
[ -z "$SETTINGS_FILES" ] && exit 0
|
|
27
|
+
|
|
28
|
+
FOUND_RELATIVE=0
|
|
29
|
+
for SFILE in $SETTINGS_FILES; do
|
|
30
|
+
for KEY in denyWrite denyRead allowWrite; do
|
|
31
|
+
PATHS=$(jq -r ".permissions.${KEY}[]? // empty" "$SFILE" 2>/dev/null)
|
|
32
|
+
[ -z "$PATHS" ] && continue
|
|
33
|
+
while IFS= read -r P; do
|
|
34
|
+
[ -z "$P" ] && continue
|
|
35
|
+
if [[ "$P" != /* ]] && [[ "$P" != "~"* ]]; then
|
|
36
|
+
echo "⚠ SANDBOX WARNING: Relative path in ${KEY} is SILENTLY IGNORED" >&2
|
|
37
|
+
echo " File: $SFILE" >&2
|
|
38
|
+
echo " Path: \"$P\" → has NO effect" >&2
|
|
39
|
+
echo " Fix: Use absolute path: \"$(realpath -m "$P" 2>/dev/null || echo "$PWD/$P")\"" >&2
|
|
40
|
+
FOUND_RELATIVE=1
|
|
41
|
+
fi
|
|
42
|
+
done <<< "$PATHS"
|
|
43
|
+
done
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
if [ "$FOUND_RELATIVE" -eq 1 ]; then
|
|
47
|
+
echo "" >&2
|
|
48
|
+
echo "See: https://github.com/anthropics/claude-code/issues/50454" >&2
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
exit 0
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# session-agent-cost-limiter.sh — Cap total subagent spawns per session
|
|
3
|
+
#
|
|
4
|
+
# Solves: #47049 — User lost £140 overnight when Claude spawned 16+
|
|
5
|
+
# subagents. Each agent gets its own context window = 16x token cost.
|
|
6
|
+
# Existing max-concurrent-agents limits simultaneous agents, but not
|
|
7
|
+
# total spawns over a session. This hook limits the cumulative count.
|
|
8
|
+
#
|
|
9
|
+
# How it works: Tracks every Agent spawn in a session-scoped counter.
|
|
10
|
+
# After CC_MAX_SESSION_AGENTS total spawns, blocks further agents.
|
|
11
|
+
# Counter resets when the session ends (file keyed by PPID).
|
|
12
|
+
#
|
|
13
|
+
# CONFIG:
|
|
14
|
+
# CC_MAX_SESSION_AGENTS=10 (default: 10 total agents per session)
|
|
15
|
+
#
|
|
16
|
+
# TRIGGER: PreToolUse
|
|
17
|
+
# MATCHER: "Agent"
|
|
18
|
+
# CATEGORY: cost-control
|
|
19
|
+
|
|
20
|
+
INPUT=$(cat)
|
|
21
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
22
|
+
[ "$TOOL" != "Agent" ] && exit 0
|
|
23
|
+
|
|
24
|
+
MAX_TOTAL=${CC_MAX_SESSION_AGENTS:-10}
|
|
25
|
+
# Use PPID to track the parent Claude Code process, not this subshell
|
|
26
|
+
COUNTER_FILE="/tmp/cc-session-agents-${PPID}"
|
|
27
|
+
|
|
28
|
+
# Initialize if missing
|
|
29
|
+
[ -f "$COUNTER_FILE" ] || echo "0" > "$COUNTER_FILE"
|
|
30
|
+
|
|
31
|
+
CURRENT=$(cat "$COUNTER_FILE" 2>/dev/null || echo 0)
|
|
32
|
+
|
|
33
|
+
if [ "$CURRENT" -ge "$MAX_TOTAL" ]; then
|
|
34
|
+
echo "BLOCKED: Session agent limit reached (${CURRENT}/${MAX_TOTAL} total spawns)." >&2
|
|
35
|
+
echo " Each subagent opens a new context window and consumes tokens independently." >&2
|
|
36
|
+
echo " Consider completing existing work before spawning more agents." >&2
|
|
37
|
+
echo " Override: CC_MAX_SESSION_AGENTS=$((MAX_TOTAL + 5))" >&2
|
|
38
|
+
exit 2
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Increment
|
|
42
|
+
echo $((CURRENT + 1)) > "$COUNTER_FILE"
|
|
43
|
+
exit 0
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# session-cost-alert.sh — Alert when estimated session cost exceeds threshold
|
|
3
|
+
#
|
|
4
|
+
# Solves: #47049 — User lost £140 overnight without realizing costs were
|
|
5
|
+
# accumulating. This hook estimates token cost per tool call and warns
|
|
6
|
+
# when the session total exceeds a configurable threshold.
|
|
7
|
+
#
|
|
8
|
+
# How it works: PostToolUse hook that parses session_tokens from tool
|
|
9
|
+
# results (when available) and estimates cost using Anthropic pricing.
|
|
10
|
+
# Warns at $1 and blocks at $5 (configurable).
|
|
11
|
+
#
|
|
12
|
+
# CONFIG:
|
|
13
|
+
# CC_COST_WARN=1 (warn at $1, default)
|
|
14
|
+
# CC_COST_BLOCK=5 (block at $5, default)
|
|
15
|
+
# CC_MODEL_COST=5 ($/M input tokens for Opus, default)
|
|
16
|
+
#
|
|
17
|
+
# TRIGGER: PostToolUse
|
|
18
|
+
# MATCHER: ""
|
|
19
|
+
# CATEGORY: cost-control
|
|
20
|
+
|
|
21
|
+
INPUT=$(cat)
|
|
22
|
+
|
|
23
|
+
WARN_THRESHOLD=${CC_COST_WARN:-1}
|
|
24
|
+
BLOCK_THRESHOLD=${CC_COST_BLOCK:-5}
|
|
25
|
+
COST_PER_M=${CC_MODEL_COST:-5}
|
|
26
|
+
|
|
27
|
+
COST_FILE="/tmp/cc-session-cost-${PPID}"
|
|
28
|
+
|
|
29
|
+
# Initialize
|
|
30
|
+
if [ ! -f "$COST_FILE" ]; then
|
|
31
|
+
echo "0" > "$COST_FILE"
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Try to extract token count from tool result
|
|
35
|
+
# Note: Not all tool results contain token info. This is a best-effort estimate.
|
|
36
|
+
TOKENS=$(echo "$INPUT" | jq -r '.tool_result // empty' 2>/dev/null | wc -c)
|
|
37
|
+
# Rough estimate: 1 char ≈ 0.3 tokens (for tool output going into context)
|
|
38
|
+
EST_TOKENS=$((TOKENS * 3 / 10))
|
|
39
|
+
|
|
40
|
+
# Add to running total
|
|
41
|
+
CURRENT=$(cat "$COST_FILE" 2>/dev/null || echo 0)
|
|
42
|
+
TOTAL=$((CURRENT + EST_TOKENS))
|
|
43
|
+
echo "$TOTAL" > "$COST_FILE"
|
|
44
|
+
|
|
45
|
+
# Estimate cost
|
|
46
|
+
COST=$(echo "scale=4; $TOTAL * $COST_PER_M / 1000000" | bc 2>/dev/null || echo "0")
|
|
47
|
+
COST_CENTS=$(echo "scale=0; $TOTAL * $COST_PER_M / 10000" | bc 2>/dev/null || echo "0")
|
|
48
|
+
|
|
49
|
+
# Check thresholds
|
|
50
|
+
BLOCK_CENTS=$(echo "scale=0; $BLOCK_THRESHOLD * 100" | bc 2>/dev/null || echo "500")
|
|
51
|
+
WARN_CENTS=$(echo "scale=0; $WARN_THRESHOLD * 100" | bc 2>/dev/null || echo "100")
|
|
52
|
+
|
|
53
|
+
if [ "$COST_CENTS" -ge "$BLOCK_CENTS" ] 2>/dev/null; then
|
|
54
|
+
echo "BLOCKED: Estimated session cost \$${COST} exceeds \$${BLOCK_THRESHOLD} limit." >&2
|
|
55
|
+
echo " Estimated tokens used: ${TOTAL}" >&2
|
|
56
|
+
echo " Override: CC_COST_BLOCK=$((BLOCK_THRESHOLD * 2))" >&2
|
|
57
|
+
exit 2
|
|
58
|
+
elif [ "$COST_CENTS" -ge "$WARN_CENTS" ] 2>/dev/null; then
|
|
59
|
+
echo "WARNING: Estimated session cost \$${COST} approaching \$${BLOCK_THRESHOLD} limit." >&2
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
exit 0
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
MAX_RSS_MB="${CC_MAX_RSS_MB:-4096}"
|
|
2
2
|
CHECK_INTERVAL=300
|
|
3
|
+
PID_FILE="/tmp/cc-memory-watchdog.pid"
|
|
4
|
+
|
|
5
|
+
# Prevent duplicate instances — only one watchdog should run at a time
|
|
6
|
+
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
|
7
|
+
exit 0
|
|
8
|
+
fi
|
|
9
|
+
|
|
3
10
|
(
|
|
11
|
+
echo $$ > "$PID_FILE"
|
|
12
|
+
trap 'rm -f "$PID_FILE"' EXIT
|
|
4
13
|
while true; do
|
|
5
14
|
sleep "$CHECK_INTERVAL"
|
|
6
15
|
pgrep -f "claude" | while read pid; do
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# settings-integrity-monitor.sh — Detect unexpected settings.json changes
|
|
3
|
+
# Trigger: PreToolUse
|
|
4
|
+
# Matcher: (empty — runs on every tool use)
|
|
5
|
+
#
|
|
6
|
+
# The /model command silently rewrites settings.json from scratch,
|
|
7
|
+
# removing sandbox restrictions and hook configurations.
|
|
8
|
+
# See: https://github.com/anthropics/claude-code/issues/44791
|
|
9
|
+
#
|
|
10
|
+
# This hook maintains a checksum of settings.json and warns when
|
|
11
|
+
# it changes outside your control. It also creates automatic backups
|
|
12
|
+
# so you can restore your configuration.
|
|
13
|
+
#
|
|
14
|
+
# TRIGGER: PreToolUse MATCHER: ""
|
|
15
|
+
|
|
16
|
+
SETTINGS="${CLAUDE_SETTINGS_FILE:-$HOME/.claude/settings.json}"
|
|
17
|
+
BACKUP_DIR="$HOME/.claude/settings-backups"
|
|
18
|
+
CHECKSUM_FILE="$BACKUP_DIR/.checksum"
|
|
19
|
+
|
|
20
|
+
# Exit silently if settings.json doesn't exist
|
|
21
|
+
[ -f "$SETTINGS" ] || exit 0
|
|
22
|
+
|
|
23
|
+
mkdir -p "$BACKUP_DIR"
|
|
24
|
+
|
|
25
|
+
CURRENT_HASH=$(sha256sum "$SETTINGS" 2>/dev/null | cut -d' ' -f1)
|
|
26
|
+
|
|
27
|
+
if [ ! -f "$CHECKSUM_FILE" ]; then
|
|
28
|
+
# First run: save baseline
|
|
29
|
+
echo "$CURRENT_HASH" > "$CHECKSUM_FILE"
|
|
30
|
+
cp "$SETTINGS" "$BACKUP_DIR/settings.json.baseline"
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
SAVED_HASH=$(cat "$CHECKSUM_FILE" 2>/dev/null)
|
|
35
|
+
|
|
36
|
+
if [ "$CURRENT_HASH" != "$SAVED_HASH" ]; then
|
|
37
|
+
# Settings changed — create timestamped backup of PREVIOUS version
|
|
38
|
+
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
|
39
|
+
if [ -f "$BACKUP_DIR/settings.json.latest" ]; then
|
|
40
|
+
cp "$BACKUP_DIR/settings.json.latest" "$BACKUP_DIR/settings.json.$TIMESTAMP"
|
|
41
|
+
fi
|
|
42
|
+
# Save current as latest
|
|
43
|
+
cp "$SETTINGS" "$BACKUP_DIR/settings.json.latest"
|
|
44
|
+
echo "$CURRENT_HASH" > "$CHECKSUM_FILE"
|
|
45
|
+
|
|
46
|
+
# Count hooks in old vs new
|
|
47
|
+
OLD_HOOKS=$(jq '[.hooks | to_entries[].value[].hooks[]?] | length' "$BACKUP_DIR/settings.json.$TIMESTAMP" 2>/dev/null || echo "?")
|
|
48
|
+
NEW_HOOKS=$(jq '[.hooks | to_entries[].value[].hooks[]?] | length' "$SETTINGS" 2>/dev/null || echo "?")
|
|
49
|
+
|
|
50
|
+
echo "⚠ settings.json was modified (hooks: $OLD_HOOKS → $NEW_HOOKS)" >&2
|
|
51
|
+
echo " Backup saved: $BACKUP_DIR/settings.json.$TIMESTAMP" >&2
|
|
52
|
+
echo " Restore: cp $BACKUP_DIR/settings.json.$TIMESTAMP $SETTINGS" >&2
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
exit 0
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# settings-json-model-guard.sh — Backup settings.json before /model changes
|
|
3
|
+
#
|
|
4
|
+
# Solves: The /model slash command rewrites settings.json completely,
|
|
5
|
+
# wiping all hook configurations and custom settings. (#46921)
|
|
6
|
+
#
|
|
7
|
+
# How it works: PreToolUse(Bash) detects commands that modify
|
|
8
|
+
# settings.json (especially from /model). Creates a timestamped
|
|
9
|
+
# backup before allowing the write. If hooks are lost after the
|
|
10
|
+
# write, the PostToolUse phase restores them.
|
|
11
|
+
#
|
|
12
|
+
# Usage: Add TWO hooks
|
|
13
|
+
#
|
|
14
|
+
# {
|
|
15
|
+
# "hooks": {
|
|
16
|
+
# "PreToolUse": [{
|
|
17
|
+
# "matcher": "Edit|Write",
|
|
18
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/settings-json-model-guard.sh" }]
|
|
19
|
+
# }],
|
|
20
|
+
# "PostToolUse": [{
|
|
21
|
+
# "matcher": "Edit|Write",
|
|
22
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/settings-json-model-guard.sh" }]
|
|
23
|
+
# }]
|
|
24
|
+
# }
|
|
25
|
+
# }
|
|
26
|
+
#
|
|
27
|
+
# TRIGGER: PreToolUse+PostToolUse MATCHER: "Edit|Write"
|
|
28
|
+
|
|
29
|
+
set -euo pipefail
|
|
30
|
+
|
|
31
|
+
INPUT=$(cat)
|
|
32
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
33
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
34
|
+
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // empty' 2>/dev/null)
|
|
35
|
+
|
|
36
|
+
# Only care about settings.json writes
|
|
37
|
+
case "$FILE" in
|
|
38
|
+
*/.claude/settings.json|*/.claude/settings.local.json) ;;
|
|
39
|
+
*) exit 0 ;;
|
|
40
|
+
esac
|
|
41
|
+
|
|
42
|
+
BACKUP_DIR="$HOME/.claude/settings-backups"
|
|
43
|
+
mkdir -p "$BACKUP_DIR"
|
|
44
|
+
|
|
45
|
+
SETTINGS_FILE="$FILE"
|
|
46
|
+
[ ! -f "$SETTINGS_FILE" ] && exit 0
|
|
47
|
+
|
|
48
|
+
# --- PreToolUse: backup before modification ---
|
|
49
|
+
if [[ "$HOOK_EVENT" == "PreToolUse" ]]; then
|
|
50
|
+
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
|
51
|
+
BASENAME=$(basename "$SETTINGS_FILE" .json)
|
|
52
|
+
BACKUP="$BACKUP_DIR/${BASENAME}-pre-model-${TIMESTAMP}.json"
|
|
53
|
+
cp "$SETTINGS_FILE" "$BACKUP"
|
|
54
|
+
|
|
55
|
+
# Count hooks in current settings
|
|
56
|
+
HOOK_COUNT=$(jq '[.hooks // {} | to_entries[] | .value[] | .hooks // [] | length] | add // 0' "$SETTINGS_FILE" 2>/dev/null || echo 0)
|
|
57
|
+
if [ "$HOOK_COUNT" -gt 0 ]; then
|
|
58
|
+
echo "Settings backup created: $BACKUP ($HOOK_COUNT hooks preserved)" >&2
|
|
59
|
+
# Store hook count for PostToolUse comparison
|
|
60
|
+
echo "$HOOK_COUNT" > "/tmp/cc-settings-hook-count-pre"
|
|
61
|
+
fi
|
|
62
|
+
exit 0
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# --- PostToolUse: verify hooks survived ---
|
|
66
|
+
if [[ "$HOOK_EVENT" == "PostToolUse" ]]; then
|
|
67
|
+
PRE_COUNT_FILE="/tmp/cc-settings-hook-count-pre"
|
|
68
|
+
[ ! -f "$PRE_COUNT_FILE" ] && exit 0
|
|
69
|
+
|
|
70
|
+
PRE_COUNT=$(cat "$PRE_COUNT_FILE" 2>/dev/null || echo 0)
|
|
71
|
+
rm -f "$PRE_COUNT_FILE"
|
|
72
|
+
|
|
73
|
+
[ "$PRE_COUNT" -eq 0 ] && exit 0
|
|
74
|
+
|
|
75
|
+
# Count hooks after modification
|
|
76
|
+
POST_COUNT=$(jq '[.hooks // {} | to_entries[] | .value[] | .hooks // [] | length] | add // 0' "$SETTINGS_FILE" 2>/dev/null || echo 0)
|
|
77
|
+
|
|
78
|
+
if [ "$POST_COUNT" -lt "$PRE_COUNT" ]; then
|
|
79
|
+
LATEST_BACKUP=$(ls -t "$BACKUP_DIR"/settings-pre-model-*.json 2>/dev/null | head -1)
|
|
80
|
+
echo "WARNING: Hook count dropped from $PRE_COUNT to $POST_COUNT after settings modification!" >&2
|
|
81
|
+
echo " This typically happens when /model rewrites settings.json." >&2
|
|
82
|
+
if [ -n "$LATEST_BACKUP" ]; then
|
|
83
|
+
echo " Restore hooks: jq -s '.[0] * {hooks: .[1].hooks}' '$SETTINGS_FILE' '$LATEST_BACKUP' > /tmp/merged.json && mv /tmp/merged.json '$SETTINGS_FILE'" >&2
|
|
84
|
+
fi
|
|
85
|
+
fi
|
|
86
|
+
exit 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
exit 0
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# shell-config-truncation-guard.sh — Block writes that would truncate shell config files
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude Code installer auto-update truncating ~/.bash_profile and
|
|
5
|
+
# ~/.zshrc to 0 bytes, destroying all user shell configuration (#49615).
|
|
6
|
+
# Also catches Claude itself attempting to overwrite these files with
|
|
7
|
+
# minimal or empty content.
|
|
8
|
+
#
|
|
9
|
+
# How it works: PreToolUse hook intercepts Write/Bash operations targeting
|
|
10
|
+
# shell config files. If the new content would be significantly shorter
|
|
11
|
+
# than the existing file (>60% reduction), the operation is blocked.
|
|
12
|
+
# Empty/near-empty writes are always blocked.
|
|
13
|
+
#
|
|
14
|
+
# TRIGGER: PreToolUse
|
|
15
|
+
# MATCHER: "Bash|Write"
|
|
16
|
+
#
|
|
17
|
+
# Usage:
|
|
18
|
+
# {
|
|
19
|
+
# "hooks": {
|
|
20
|
+
# "PreToolUse": [{
|
|
21
|
+
# "matcher": "Bash|Write",
|
|
22
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/shell-config-truncation-guard.sh" }]
|
|
23
|
+
# }]
|
|
24
|
+
# }
|
|
25
|
+
# }
|
|
26
|
+
|
|
27
|
+
INPUT=$(cat)
|
|
28
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
29
|
+
|
|
30
|
+
# Shell config files to protect
|
|
31
|
+
PROTECTED_FILES=(
|
|
32
|
+
"$HOME/.bashrc"
|
|
33
|
+
"$HOME/.bash_profile"
|
|
34
|
+
"$HOME/.zshrc"
|
|
35
|
+
"$HOME/.zprofile"
|
|
36
|
+
"$HOME/.profile"
|
|
37
|
+
"$HOME/.zshenv"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
check_file_truncation() {
|
|
41
|
+
local target_file="$1"
|
|
42
|
+
local new_size="$2"
|
|
43
|
+
|
|
44
|
+
for protected in "${PROTECTED_FILES[@]}"; do
|
|
45
|
+
if [ "$target_file" = "$protected" ] || [ "$(realpath "$target_file" 2>/dev/null)" = "$(realpath "$protected" 2>/dev/null)" ]; then
|
|
46
|
+
if [ ! -f "$protected" ]; then
|
|
47
|
+
return 0
|
|
48
|
+
fi
|
|
49
|
+
local current_size
|
|
50
|
+
current_size=$(wc -c < "$protected" 2>/dev/null || echo 0)
|
|
51
|
+
|
|
52
|
+
# Block if writing 0 bytes or near-empty (< 10 bytes)
|
|
53
|
+
if [ "$new_size" -lt 10 ] && [ "$current_size" -gt 50 ]; then
|
|
54
|
+
echo "BLOCKED: Attempted to truncate $protected to $new_size bytes (current: $current_size bytes)" >&2
|
|
55
|
+
echo "This would destroy your shell configuration. See: github.com/anthropics/claude-code/issues/49615" >&2
|
|
56
|
+
exit 2
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Block if >60% size reduction
|
|
60
|
+
if [ "$current_size" -gt 100 ]; then
|
|
61
|
+
local threshold=$((current_size * 40 / 100))
|
|
62
|
+
if [ "$new_size" -lt "$threshold" ]; then
|
|
63
|
+
echo "BLOCKED: Write to $protected would reduce size by >60% ($current_size → $new_size bytes)" >&2
|
|
64
|
+
echo "If intentional, back up first: cp $protected ${protected}.bak" >&2
|
|
65
|
+
exit 2
|
|
66
|
+
fi
|
|
67
|
+
fi
|
|
68
|
+
return 0
|
|
69
|
+
fi
|
|
70
|
+
done
|
|
71
|
+
return 0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if [ "$TOOL" = "Write" ]; then
|
|
75
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
76
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty' 2>/dev/null)
|
|
77
|
+
if [ -n "$FILE_PATH" ]; then
|
|
78
|
+
NEW_SIZE=${#CONTENT}
|
|
79
|
+
check_file_truncation "$FILE_PATH" "$NEW_SIZE"
|
|
80
|
+
fi
|
|
81
|
+
elif [ "$TOOL" = "Bash" ]; then
|
|
82
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
83
|
+
[ -z "$COMMAND" ] && exit 0
|
|
84
|
+
|
|
85
|
+
# Detect redirect truncation: > ~/.bashrc, >~/.zshrc, etc.
|
|
86
|
+
for protected in "${PROTECTED_FILES[@]}"; do
|
|
87
|
+
base=$(basename "$protected")
|
|
88
|
+
# Match: > file, >file, truncate file, : > file, echo "" > file
|
|
89
|
+
if echo "$COMMAND" | grep -qE "(^|[;&|])\s*(>|truncate\s+-s\s*0|:\s*>)\s*~?(/[^;]*)?${base}"; then
|
|
90
|
+
echo "BLOCKED: Command would truncate $protected" >&2
|
|
91
|
+
echo "If intentional, back up first: cp $protected ${protected}.bak" >&2
|
|
92
|
+
exit 2
|
|
93
|
+
fi
|
|
94
|
+
done
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
exit 0
|
|
@@ -34,8 +34,8 @@ if echo "$COMMAND" | grep -qE '(sh|bash|zsh|dash)\s+-c\s+'; then
|
|
|
34
34
|
fi
|
|
35
35
|
|
|
36
36
|
# === Check 2: Python one-liners ===
|
|
37
|
-
if echo "$COMMAND" | grep -qE 'python[23]?\s+-c\s+'; then
|
|
38
|
-
INNER=$(echo "$COMMAND" | sed -E "s/.*python[23]?\s+-c\s+['\"]?//" | sed "s/['\"]?\s*$//")
|
|
37
|
+
if echo "$COMMAND" | grep -qE 'python[23]?(\.[0-9]+)?\s+-c\s+'; then
|
|
38
|
+
INNER=$(echo "$COMMAND" | sed -E "s/.*python[23]?(\.[0-9]+)?\s+-c\s+['\"]?//" | sed "s/['\"]?\s*$//")
|
|
39
39
|
if echo "$INNER" | grep -qiE "os\.system\(.*($DESTRUCT_PATTERN)|subprocess\.(run|call|Popen)\(.*($DESTRUCT_PATTERN)|shutil\.rmtree\s*\(\s*['\"/~]"; then
|
|
40
40
|
echo "BLOCKED: Destructive command in Python one-liner" >&2
|
|
41
41
|
exit 2
|
|
@@ -69,9 +69,9 @@ if echo "$COMMAND" | grep -qE '(sh|bash)\s+-c\s+.*(sh|bash)\s+-c'; then
|
|
|
69
69
|
fi
|
|
70
70
|
|
|
71
71
|
# === Check 6: Pipe to shell (echo "rm -rf /" | sh) ===
|
|
72
|
-
if echo "$COMMAND" | grep -qE '\|\s*(sh|bash|zsh)\s
|
|
72
|
+
if echo "$COMMAND" | grep -qE '\|\s*(sh|bash|zsh)(\s|$)'; then
|
|
73
73
|
# Extract the piped content
|
|
74
|
-
PIPED=$(echo "$COMMAND" | sed -E 's/\s*\|\s*(sh|bash|zsh)\s
|
|
74
|
+
PIPED=$(echo "$COMMAND" | sed -E 's/\s*\|\s*(sh|bash|zsh)(\s.*)?$//')
|
|
75
75
|
if echo "$PIPED" | grep -qE "$DESTRUCT_PATTERN"; then
|
|
76
76
|
echo "BLOCKED: Destructive command piped to shell" >&2
|
|
77
77
|
exit 2
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# subagent-spawn-rate-monitor.sh — サブエージェントの過剰spawn検知
|
|
3
|
+
# Why: サブエージェントは毎回spawnされるたびに~4.7Kトークンがcache_creation
|
|
4
|
+
# (1.25xコスト)として課金される。spawn-heavyなワークフローでは線形に増大し、
|
|
5
|
+
# ユーザーが気づかないうちにquotaを消耗する (#50213, #46968)
|
|
6
|
+
# Event: PreToolUse MATCHER: Agent
|
|
7
|
+
# Action: 短時間に多数のAgent spawnがあれば警告
|
|
8
|
+
|
|
9
|
+
COUNTER_FILE="/tmp/cc-subagent-spawn-counter"
|
|
10
|
+
WINDOW_FILE="/tmp/cc-subagent-spawn-window"
|
|
11
|
+
THRESHOLD=5 # この回数を超えたら警告
|
|
12
|
+
WINDOW_SECS=300 # 5分間のウィンドウ
|
|
13
|
+
|
|
14
|
+
NOW=$(date +%s)
|
|
15
|
+
WINDOW_START=$(cat "$WINDOW_FILE" 2>/dev/null || echo "$NOW")
|
|
16
|
+
COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo "0")
|
|
17
|
+
|
|
18
|
+
# ウィンドウ期限切れならリセット
|
|
19
|
+
ELAPSED=$((NOW - WINDOW_START))
|
|
20
|
+
if [ "$ELAPSED" -gt "$WINDOW_SECS" ]; then
|
|
21
|
+
COUNT=0
|
|
22
|
+
echo "$NOW" > "$WINDOW_FILE"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
COUNT=$((COUNT + 1))
|
|
26
|
+
echo "$COUNT" > "$COUNTER_FILE"
|
|
27
|
+
|
|
28
|
+
if [ "$COUNT" -gt "$THRESHOLD" ]; then
|
|
29
|
+
echo "⚠ HIGH SUBAGENT SPAWN RATE: $COUNT agents spawned in ${ELAPSED}s" >&2
|
|
30
|
+
echo "Each spawn costs ~4.7K tokens at 1.25x rate (no cache_control)." >&2
|
|
31
|
+
echo "Consider batching tasks or using fewer parallel agents. See: #50213" >&2
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
exit 0
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# subcommand-chain-guard.sh — Block commands with excessive subcommand chains
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude Code silently ignores deny rules when a command contains
|
|
5
|
+
# 50+ subcommands (MAX_SUBCOMMANDS_FOR_SECURITY_CHECK = 50).
|
|
6
|
+
# Attackers chain 50 no-op "true" commands before a dangerous command
|
|
7
|
+
# to bypass all security checks. (Adversa AI / CVE disclosure, April 2026)
|
|
8
|
+
#
|
|
9
|
+
# How it works: Counts semicolon-separated and &&/|| chained subcommands.
|
|
10
|
+
# If the count exceeds a threshold (default: 20), blocks execution.
|
|
11
|
+
# This catches the exploit well before the 50-command limit.
|
|
12
|
+
#
|
|
13
|
+
# TRIGGER: PreToolUse
|
|
14
|
+
# MATCHER: "Bash"
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
THRESHOLD=${CC_SUBCOMMAND_LIMIT:-20}
|
|
19
|
+
|
|
20
|
+
INPUT=$(cat)
|
|
21
|
+
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
22
|
+
[ -z "$CMD" ] && exit 0
|
|
23
|
+
|
|
24
|
+
# Count subcommands: split on ; && ||
|
|
25
|
+
# Use tr to normalize separators, then count
|
|
26
|
+
SUBCOMMAND_COUNT=$(echo "$CMD" | tr ';' '\n' | tr '&' '\n' | tr '|' '\n' | grep -c '[^ ]' 2>/dev/null || echo 1)
|
|
27
|
+
|
|
28
|
+
if [ "$SUBCOMMAND_COUNT" -gt "$THRESHOLD" ]; then
|
|
29
|
+
echo "BLOCKED: Command contains $SUBCOMMAND_COUNT subcommands (limit: $THRESHOLD)." >&2
|
|
30
|
+
echo " Claude Code ignores deny rules after 50 subcommands (CVE disclosure)." >&2
|
|
31
|
+
echo " This hook blocks at $THRESHOLD to prevent security bypass." >&2
|
|
32
|
+
echo " Override: CC_SUBCOMMAND_LIMIT=100 (not recommended)" >&2
|
|
33
|
+
exit 2
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Also detect the specific attack pattern: many "true" or ":" no-ops
|
|
37
|
+
NOOP_COUNT=$(echo "$CMD" | grep -oE '\btrue\b|^:|;\s*:' | wc -l 2>/dev/null || echo 0)
|
|
38
|
+
if [ "$NOOP_COUNT" -gt 10 ]; then
|
|
39
|
+
echo "BLOCKED: Suspicious pattern — $NOOP_COUNT no-op commands detected." >&2
|
|
40
|
+
echo " This resembles the subcommand-chain attack (50x true + dangerous cmd)." >&2
|
|
41
|
+
exit 2
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
exit 0
|