cc-safe-setup 29.6.39 → 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 +133 -12
- package/SETTINGS_REFERENCE.md +2 -0
- package/SKILL.md +47 -0
- package/TROUBLESHOOTING.md +26 -0
- package/examples/README.md +11 -1
- package/examples/activity-logger.sh +58 -0
- package/examples/allow-claude-settings.sh +3 -2
- package/examples/allow-git-hooks-dir.sh +3 -2
- package/examples/allow-protected-dirs.sh +3 -2
- 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/bash-heuristic-approver.sh +1 -1
- 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/decision-warn.sh +59 -0
- package/examples/deny-bypass-detector.sh +143 -0
- package/examples/direnv-auto-reload.sh +9 -2
- package/examples/dotenv-commit-guard.sh +11 -5
- 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/proof-log-session.sh +62 -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 +108 -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,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
|
|
@@ -24,8 +24,15 @@ OLD_CWD=$(echo "$INPUT" | jq -r '.old_cwd // empty' 2>/dev/null)
|
|
|
24
24
|
if [ -f "${NEW_CWD}/.envrc" ]; then
|
|
25
25
|
echo "📂 Directory changed: found .envrc in ${NEW_CWD}" >&2
|
|
26
26
|
if command -v direnv &>/dev/null; then
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
# Write exports to CLAUDE_ENV_FILE so Claude Code picks them up
|
|
28
|
+
# (eval in a subshell would be lost — CLAUDE_ENV_FILE persists to BashTool)
|
|
29
|
+
if [ -n "$CLAUDE_ENV_FILE" ]; then
|
|
30
|
+
echo " direnv: auto-allowing and writing to CLAUDE_ENV_FILE" >&2
|
|
31
|
+
cd "$NEW_CWD" && direnv allow . 2>/dev/null && \
|
|
32
|
+
direnv export bash > "$CLAUDE_ENV_FILE" 2>/dev/null
|
|
33
|
+
else
|
|
34
|
+
echo " ⚠ CLAUDE_ENV_FILE not set — direnv exports won't persist" >&2
|
|
35
|
+
fi
|
|
29
36
|
else
|
|
30
37
|
echo " ⚠ direnv not installed — .envrc found but not loaded" >&2
|
|
31
38
|
fi
|
|
@@ -28,15 +28,21 @@ if echo "$COMMAND" | grep -qE 'git\s+add\s+.*\.env'; then
|
|
|
28
28
|
exit 2
|
|
29
29
|
fi
|
|
30
30
|
|
|
31
|
-
# Check for git add -
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
# Check for git add -f/--force (bypasses .gitignore — can stage secrets)
|
|
32
|
+
# GitHub Issue anthropics/claude-code#44730: auto-mode used git add -f to
|
|
33
|
+
# force-add .gitignore'd secret files, exposing credentials in a commit.
|
|
34
|
+
if echo "$COMMAND" | grep -qE 'git\s+add\s+.*(-f|--force)\b'; then
|
|
35
|
+
echo "BLOCKED: 'git add --force' bypasses .gitignore and can stage secret files." >&2
|
|
36
|
+
echo " Use 'git add <specific-file>' without --force instead." >&2
|
|
37
|
+
exit 2
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Check for git add -A/--all that might include .env
|
|
41
|
+
if echo "$COMMAND" | grep -qE 'git\s+add\s+(-A|--all)\b'; then
|
|
34
42
|
if [ -f ".env" ] || [ -f ".env.local" ] || [ -f ".env.production" ]; then
|
|
35
|
-
# Check if .gitignore excludes it
|
|
36
43
|
if ! git check-ignore -q .env 2>/dev/null; then
|
|
37
44
|
echo "WARNING: 'git add -A' with .env file not in .gitignore." >&2
|
|
38
45
|
echo " Add .env to .gitignore before staging all files." >&2
|
|
39
|
-
# Warning only for git add -A
|
|
40
46
|
fi
|
|
41
47
|
fi
|
|
42
48
|
fi
|
|
@@ -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
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# effort-tracking-logger.sh — ツール使用ごとのエフォートログを記録
|
|
3
|
+
# Why: OTEL互換のエフォート追跡への要望が急増 (#49893, 18👍)。
|
|
4
|
+
# 公式対応を待たずに、hookでツール呼び出しごとのログを残す。
|
|
5
|
+
# コスト分析・セッション振り返り・ボトルネック特定に使える。
|
|
6
|
+
# Event: PostToolUse MATCHER: ""
|
|
7
|
+
# Output: ~/.claude/effort-log/YYYY-MM-DD.jsonl
|
|
8
|
+
|
|
9
|
+
LOG_DIR="${HOME}/.claude/effort-log"
|
|
10
|
+
mkdir -p "$LOG_DIR"
|
|
11
|
+
LOG_FILE="$LOG_DIR/$(date +%Y-%m-%d).jsonl"
|
|
12
|
+
|
|
13
|
+
# stdinからツール情報を取得
|
|
14
|
+
TOOL_INPUT=$(cat)
|
|
15
|
+
TOOL_NAME=$(echo "$TOOL_INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_name','unknown'))" 2>/dev/null)
|
|
16
|
+
TOOL_STATUS=$(echo "$TOOL_INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('was_error','false'))" 2>/dev/null)
|
|
17
|
+
|
|
18
|
+
# JSONLログに追記
|
|
19
|
+
python3 -c "
|
|
20
|
+
import json, datetime
|
|
21
|
+
entry = {
|
|
22
|
+
'timestamp': datetime.datetime.now().isoformat(),
|
|
23
|
+
'tool': '$TOOL_NAME',
|
|
24
|
+
'error': '$TOOL_STATUS' == 'true',
|
|
25
|
+
'session_pid': $(echo $$)
|
|
26
|
+
}
|
|
27
|
+
print(json.dumps(entry))
|
|
28
|
+
" >> "$LOG_FILE"
|
|
29
|
+
|
|
30
|
+
exit 0
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# financial-operation-guard.sh — Block unauthorized financial operations
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude Code transferred $1,446 from spot to futures without
|
|
5
|
+
# authorization when told to "close a position". Financial APIs
|
|
6
|
+
# should never be called without explicit per-transaction approval. (#46828)
|
|
7
|
+
#
|
|
8
|
+
# How it works: Detects commands that interact with exchange APIs,
|
|
9
|
+
# wallet transfers, payment processors, or any operation involving
|
|
10
|
+
# fund movement. Blocks with exit 2 and requires explicit user
|
|
11
|
+
# confirmation.
|
|
12
|
+
#
|
|
13
|
+
# TRIGGER: PreToolUse
|
|
14
|
+
# MATCHER: "Bash"
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
INPUT=$(cat)
|
|
19
|
+
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
20
|
+
[ -z "$CMD" ] && exit 0
|
|
21
|
+
|
|
22
|
+
# Detect financial API calls
|
|
23
|
+
# Exchange APIs
|
|
24
|
+
if echo "$CMD" | grep -qiE '(binance|bitget|bybit|kraken|coinbase|ftx|okx|kucoin|gate\.io|huobi).*(transfer|withdraw|swap|order|trade|margin|futures|spot|deposit)'; then
|
|
25
|
+
echo "BLOCKED: Financial exchange operation detected." >&2
|
|
26
|
+
echo " Command: $(echo "$CMD" | head -c 200)" >&2
|
|
27
|
+
echo " Fund transfers require explicit user approval for EACH transaction." >&2
|
|
28
|
+
exit 2
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Generic payment/transfer patterns
|
|
32
|
+
if echo "$CMD" | grep -qiE '(transfer|withdraw|send|swap|bridge)[^a-z].*\b(usdt|usdc|eth|btc|sol|bnb|funds|balance|wallet)\b'; then
|
|
33
|
+
echo "BLOCKED: Cryptocurrency transfer operation detected." >&2
|
|
34
|
+
echo " Command: $(echo "$CMD" | head -c 200)" >&2
|
|
35
|
+
echo " Wallet/fund operations require explicit user approval." >&2
|
|
36
|
+
exit 2
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Payment processor APIs
|
|
40
|
+
if echo "$CMD" | grep -qiE 'stripe.*(charges?|transfers?|payouts?)|paypal.*(payments?|send|transfers?)|square.*(payments?|charges?)'; then
|
|
41
|
+
echo "BLOCKED: Payment processor operation detected." >&2
|
|
42
|
+
echo " Command: $(echo "$CMD" | head -c 200)" >&2
|
|
43
|
+
echo " Payment operations require explicit user approval." >&2
|
|
44
|
+
exit 2
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
exit 0
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# full-rewrite-detector.sh — Warn on full-file rewrites
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude Code sometimes rewrites entire files when a small edit
|
|
7
|
+
# would suffice. AMD's analysis of 6,852 sessions found this
|
|
8
|
+
# pattern increasing over time — a sign of quality degradation.
|
|
9
|
+
# This hook detects when >80% of a file's lines were changed
|
|
10
|
+
# and warns the user.
|
|
11
|
+
#
|
|
12
|
+
# TRIGGER: PostToolUse
|
|
13
|
+
# MATCHER: "Write"
|
|
14
|
+
#
|
|
15
|
+
# HOW IT WORKS:
|
|
16
|
+
# After a Write operation, checks git diff for the target file.
|
|
17
|
+
# If the ratio of changed lines to total lines exceeds the
|
|
18
|
+
# threshold (default 80%), emits a warning.
|
|
19
|
+
#
|
|
20
|
+
# CONFIGURATION:
|
|
21
|
+
# CC_REWRITE_THRESHOLD=80 — percentage threshold (default 80)
|
|
22
|
+
#
|
|
23
|
+
# NOTE: Only works in git repositories. No-op outside git repos.
|
|
24
|
+
# ================================================================
|
|
25
|
+
|
|
26
|
+
INPUT=$(cat)
|
|
27
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
28
|
+
|
|
29
|
+
# Skip if no file path
|
|
30
|
+
[ -z "$FILE" ] && exit 0
|
|
31
|
+
|
|
32
|
+
# Skip if not in a git repo
|
|
33
|
+
git rev-parse --is-inside-work-tree &>/dev/null || exit 0
|
|
34
|
+
|
|
35
|
+
# Skip if file doesn't exist (new file creation is fine)
|
|
36
|
+
[ -f "$FILE" ] || exit 0
|
|
37
|
+
|
|
38
|
+
# Get change stats from git
|
|
39
|
+
STATS=$(git diff --numstat -- "$FILE" 2>/dev/null)
|
|
40
|
+
[ -z "$STATS" ] && exit 0
|
|
41
|
+
|
|
42
|
+
ADDED=$(echo "$STATS" | awk '{print $1}')
|
|
43
|
+
DELETED=$(echo "$STATS" | awk '{print $2}')
|
|
44
|
+
|
|
45
|
+
# Handle binary files (git outputs "-" for binary)
|
|
46
|
+
[ "$ADDED" = "-" ] && exit 0
|
|
47
|
+
|
|
48
|
+
CHANGED=$((ADDED + DELETED))
|
|
49
|
+
TOTAL=$(wc -l < "$FILE" 2>/dev/null || echo 0)
|
|
50
|
+
|
|
51
|
+
# Avoid division by zero; skip very small files
|
|
52
|
+
[ "$TOTAL" -lt 5 ] && exit 0
|
|
53
|
+
|
|
54
|
+
RATIO=$((CHANGED * 100 / TOTAL))
|
|
55
|
+
THRESHOLD=${CC_REWRITE_THRESHOLD:-80}
|
|
56
|
+
|
|
57
|
+
if [ "$RATIO" -gt "$THRESHOLD" ]; then
|
|
58
|
+
echo "WARNING: Full rewrite detected on $(basename "$FILE")" >&2
|
|
59
|
+
echo " ${RATIO}% of lines changed (${CHANGED} lines changed / ${TOTAL} total)" >&2
|
|
60
|
+
echo " Consider: was a partial edit sufficient?" >&2
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
exit 0
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# home-critical-bash-guard.sh — Block Bash commands that delete/modify critical home files
|
|
3
|
+
#
|
|
4
|
+
# Solves: Bash commands that rm/mv/truncate critical dotfiles and directories
|
|
5
|
+
# - #49554: auto mode approved ~/.ssh directory deletion
|
|
6
|
+
# - #49539: ~/.git-credentials PATs deleted without confirmation
|
|
7
|
+
# - #49464: ./~ misinterpreted as ~/ leading to home directory deletion attempt
|
|
8
|
+
#
|
|
9
|
+
# Complements dotfile-protection-guard.sh (which covers Write/Edit tools).
|
|
10
|
+
# This hook covers the Bash tool path — rm, mv, truncate, > redirect on dotfiles.
|
|
11
|
+
#
|
|
12
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
13
|
+
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
INPUT=$(cat)
|
|
17
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
18
|
+
[ -z "$COMMAND" ] && exit 0
|
|
19
|
+
|
|
20
|
+
HOME_DIR="$HOME"
|
|
21
|
+
|
|
22
|
+
# Critical paths (regex patterns)
|
|
23
|
+
CRITICAL="(${HOME_DIR}|\~)/\.(bashrc|bash_profile|zshrc|zshenv|profile|login|logout|ssh|git-credentials|gitconfig|gnupg|npmrc|netrc|docker|kube|aws)"
|
|
24
|
+
|
|
25
|
+
# Check for rm/unlink targeting critical paths
|
|
26
|
+
if echo "$COMMAND" | grep -qE "(rm|unlink)\s" && echo "$COMMAND" | grep -qE "$CRITICAL"; then
|
|
27
|
+
echo "BLOCKED: Deleting critical home directory file" >&2
|
|
28
|
+
echo "Command: $COMMAND" >&2
|
|
29
|
+
exit 2
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Check for mv (rename/move) of critical paths
|
|
33
|
+
if echo "$COMMAND" | grep -qE "mv\s" && echo "$COMMAND" | grep -qE "$CRITICAL"; then
|
|
34
|
+
echo "BLOCKED: Moving/renaming critical home directory file" >&2
|
|
35
|
+
echo "Command: $COMMAND" >&2
|
|
36
|
+
exit 2
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Check for truncation via redirect (> ~/.bashrc or : > ~/.bashrc)
|
|
40
|
+
if echo "$COMMAND" | grep -qE ">\s*(${HOME_DIR}|\~)/\."; then
|
|
41
|
+
TARGET=$(echo "$COMMAND" | grep -oP ">\s*\K(${HOME_DIR}|~)/\.[^\s;|&]+")
|
|
42
|
+
if echo "$TARGET" | grep -qE "$CRITICAL"; then
|
|
43
|
+
echo "BLOCKED: Truncating critical home directory file" >&2
|
|
44
|
+
echo "Command: $COMMAND" >&2
|
|
45
|
+
exit 2
|
|
46
|
+
fi
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Check for chmod on critical credential files
|
|
50
|
+
if echo "$COMMAND" | grep -qE "chmod\s.*777" && echo "$COMMAND" | grep -qE "$CRITICAL"; then
|
|
51
|
+
echo "BLOCKED: Removing permissions on critical file" >&2
|
|
52
|
+
echo "Command: $COMMAND" >&2
|
|
53
|
+
exit 2
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
exit 0
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# idle-session-cost-alert.sh — Warn when session has been idle too long
|
|
3
|
+
# An idle session can still consume tokens via background processes.
|
|
4
|
+
# Incident: #50389 — Idle session consumed 18% usage limit over 2 hours with zero user input.
|
|
5
|
+
#
|
|
6
|
+
# This hook runs on Notification events and warns if the session has been
|
|
7
|
+
# idle for more than 5 minutes, reminding the user to exit if not actively working.
|
|
8
|
+
#
|
|
9
|
+
# Hook config (settings.json):
|
|
10
|
+
# {
|
|
11
|
+
# "hooks": {
|
|
12
|
+
# "Notification": [{
|
|
13
|
+
# "matcher": "",
|
|
14
|
+
# "hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/idle-session-cost-alert.sh" }]
|
|
15
|
+
# }]
|
|
16
|
+
# }
|
|
17
|
+
# }
|
|
18
|
+
|
|
19
|
+
INPUT=$(cat)
|
|
20
|
+
|
|
21
|
+
# Track last activity timestamp
|
|
22
|
+
IDLE_FILE="/tmp/claude-idle-tracker-$$"
|
|
23
|
+
CURRENT_TIME=$(date +%s)
|
|
24
|
+
|
|
25
|
+
if [ -f "$IDLE_FILE" ]; then
|
|
26
|
+
LAST_ACTIVE=$(cat "$IDLE_FILE")
|
|
27
|
+
IDLE_SECONDS=$((CURRENT_TIME - LAST_ACTIVE))
|
|
28
|
+
|
|
29
|
+
if [ "$IDLE_SECONDS" -gt 300 ]; then
|
|
30
|
+
IDLE_MINUTES=$((IDLE_SECONDS / 60))
|
|
31
|
+
echo "WARNING: Session idle for ${IDLE_MINUTES} minutes. Idle sessions can consume tokens via background processes (#50389). Consider exiting if not actively working." >&2
|
|
32
|
+
fi
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
echo "$CURRENT_TIME" > "$IDLE_FILE"
|
|
36
|
+
exit 0
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
INPUT=$(cat)
|
|
2
|
+
COUNTER_FILE="/tmp/.cc-model-check-counter"
|
|
3
|
+
COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo 0)
|
|
4
|
+
COUNT=$((COUNT + 1))
|
|
5
|
+
echo "$COUNT" > "$COUNTER_FILE"
|
|
6
|
+
if [ $((COUNT % 50)) -ne 0 ]; then
|
|
7
|
+
exit 0
|
|
8
|
+
fi
|
|
9
|
+
SESSION_FILE=$(ls -t ~/.claude/projects/*/session.jsonl 2>/dev/null | head -1)
|
|
10
|
+
if [ -n "$SESSION_FILE" ]; then
|
|
11
|
+
MODEL=$(grep -o '"model":"[^"]*"' "$SESSION_FILE" 2>/dev/null | tail -1 | cut -d'"' -f4)
|
|
12
|
+
if echo "$MODEL" | grep -qi "opus-4-7\|opus-4.7"; then
|
|
13
|
+
echo "⚠ Model alert: You're using $MODEL which may consume 3x more tokens than Opus 4.6."
|
|
14
|
+
echo "Consider: claude --model claude-opus-4-6 or add \"model\": \"claude-opus-4-6\" to settings.json"
|
|
15
|
+
echo "See: https://github.com/anthropics/claude-code/issues/49601"
|
|
16
|
+
fi
|
|
17
|
+
fi
|
|
18
|
+
exit 0
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# model-version-change-alert.sh — モデルバージョン変更を検知して警告
|
|
3
|
+
# Why: Opus 4.6がモデルピッカーから突然削除された (#49689, 14👍)。
|
|
4
|
+
# ユーザーが意図せず別モデルに切り替えられるケースが多発。
|
|
5
|
+
# モデルが変わるとhookの挙動・トークン消費・品質が全て変わる。
|
|
6
|
+
# Event: Notification MATCHER: ""
|
|
7
|
+
# Action: 前回のモデルと現在のモデルを比較し、変更時に警告
|
|
8
|
+
|
|
9
|
+
MODEL_HISTORY="/tmp/cc-model-version-history"
|
|
10
|
+
CURRENT_MODEL="${CLAUDE_MODEL:-unknown}"
|
|
11
|
+
|
|
12
|
+
# Notificationイベントのbodyからモデル情報を取得試行
|
|
13
|
+
if [ -n "$1" ]; then
|
|
14
|
+
BODY_MODEL=$(echo "$1" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('model',''))" 2>/dev/null)
|
|
15
|
+
[ -n "$BODY_MODEL" ] && CURRENT_MODEL="$BODY_MODEL"
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# 前回のモデルを読み取り
|
|
19
|
+
PREV_MODEL=$(cat "$MODEL_HISTORY" 2>/dev/null || echo "")
|
|
20
|
+
|
|
21
|
+
if [ -n "$PREV_MODEL" ] && [ "$PREV_MODEL" != "$CURRENT_MODEL" ] && [ "$CURRENT_MODEL" != "unknown" ]; then
|
|
22
|
+
echo "⚠ MODEL CHANGED: $PREV_MODEL → $CURRENT_MODEL" >&2
|
|
23
|
+
echo "Your model was switched. This affects token consumption, quality, and hook behavior." >&2
|
|
24
|
+
echo "If unintended, check your settings: claude --model $PREV_MODEL" >&2
|
|
25
|
+
echo "Known issue: Opus 4.6 was removed from the Desktop picker (#49689)" >&2
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# 現在のモデルを記録
|
|
29
|
+
[ "$CURRENT_MODEL" != "unknown" ] && echo "$CURRENT_MODEL" > "$MODEL_HISTORY"
|
|
30
|
+
|
|
31
|
+
exit 0
|