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,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,77 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# exploration-budget-guard.sh — Stop excessive exploration from draining tokens
|
|
3
|
+
#
|
|
4
|
+
# Solves: #51054 — Claude wastes 20% of weekly allowance exploring files
|
|
5
|
+
# on a simple task instead of acting. Read/Glob/Grep loops with
|
|
6
|
+
# no Edit/Write progress.
|
|
7
|
+
#
|
|
8
|
+
# Tracks read-only tool calls (Read, Glob, Grep) per session. After 25
|
|
9
|
+
# consecutive reads without a write, warns. After 40, blocks.
|
|
10
|
+
# Resets when an Edit/Write/Bash(write) occurs.
|
|
11
|
+
#
|
|
12
|
+
# TRIGGER: PreToolUse MATCHER: "Read|Glob|Grep|Edit|Write"
|
|
13
|
+
|
|
14
|
+
INPUT=$(cat)
|
|
15
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
16
|
+
|
|
17
|
+
STATE_DIR="/tmp/.cc-exploration-budget"
|
|
18
|
+
mkdir -p "$STATE_DIR"
|
|
19
|
+
|
|
20
|
+
# Session-scoped state file (keyed by parent PID)
|
|
21
|
+
STATE_FILE="$STATE_DIR/session-$$"
|
|
22
|
+
# Fall back to a shared file if $$ changes between hook calls
|
|
23
|
+
STATE_FILE="$STATE_DIR/exploration-count"
|
|
24
|
+
|
|
25
|
+
NOW=$(date +%s)
|
|
26
|
+
|
|
27
|
+
case "$TOOL" in
|
|
28
|
+
Edit|Write)
|
|
29
|
+
# Write operation = progress. Reset exploration counter.
|
|
30
|
+
echo "0 $NOW" > "$STATE_FILE"
|
|
31
|
+
exit 0
|
|
32
|
+
;;
|
|
33
|
+
Read|Glob|Grep)
|
|
34
|
+
# Read operation = exploration. Increment counter.
|
|
35
|
+
;;
|
|
36
|
+
*)
|
|
37
|
+
exit 0
|
|
38
|
+
;;
|
|
39
|
+
esac
|
|
40
|
+
|
|
41
|
+
# Read current count
|
|
42
|
+
COUNT=0
|
|
43
|
+
LAST_TIME=0
|
|
44
|
+
if [ -f "$STATE_FILE" ]; then
|
|
45
|
+
read -r COUNT LAST_TIME < "$STATE_FILE" 2>/dev/null || true
|
|
46
|
+
# Reset if more than 10 minutes since last call (new task)
|
|
47
|
+
ELAPSED=$(( NOW - LAST_TIME ))
|
|
48
|
+
if [ "$ELAPSED" -gt 600 ]; then
|
|
49
|
+
COUNT=0
|
|
50
|
+
fi
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
COUNT=$(( COUNT + 1 ))
|
|
54
|
+
echo "$COUNT $NOW" > "$STATE_FILE"
|
|
55
|
+
|
|
56
|
+
WARN_THRESHOLD=25
|
|
57
|
+
BLOCK_THRESHOLD=40
|
|
58
|
+
|
|
59
|
+
if [ "$COUNT" -ge "$BLOCK_THRESHOLD" ]; then
|
|
60
|
+
echo "BLOCKED: $COUNT consecutive read operations without writing anything." >&2
|
|
61
|
+
echo " You're stuck in an exploration loop — this wastes tokens." >&2
|
|
62
|
+
echo " Take action NOW:" >&2
|
|
63
|
+
echo " 1. Write your solution based on what you've already read" >&2
|
|
64
|
+
echo " 2. If unsure, make a small change and test it" >&2
|
|
65
|
+
echo " 3. Ask the user for clarification instead of reading more files" >&2
|
|
66
|
+
echo "" >&2
|
|
67
|
+
echo " Exploration budget: $COUNT/$BLOCK_THRESHOLD (EXCEEDED)" >&2
|
|
68
|
+
exit 2
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
if [ "$COUNT" -ge "$WARN_THRESHOLD" ]; then
|
|
72
|
+
echo "WARNING: $COUNT consecutive reads without any write." >&2
|
|
73
|
+
echo " You may be over-exploring. Consider acting on what you know." >&2
|
|
74
|
+
echo " Budget: $COUNT/$BLOCK_THRESHOLD reads before block." >&2
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
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
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# move-delete-sequence-guard.sh — Detect move+delete sequences that cause data loss
|
|
3
|
+
#
|
|
4
|
+
# Solves: Agent moves files to a temp location, then deletes the parent directory,
|
|
5
|
+
# effectively destroying the moved files' original context and siblings.
|
|
6
|
+
# - #49129: mv files to /tmp && rm -rf parent/ — lost 50GB of data
|
|
7
|
+
# - #49792: Opus 4.7 moves files, then deletes the source directory
|
|
8
|
+
#
|
|
9
|
+
# Pattern detected:
|
|
10
|
+
# mv <source> <dest> && rm -rf <source_parent>
|
|
11
|
+
# mv <source> <dest> ; rm -rf <source_parent>
|
|
12
|
+
# mv <source> <dest> || rm -rf <source_parent>
|
|
13
|
+
# Any compound command containing both mv and rm -r on related paths
|
|
14
|
+
#
|
|
15
|
+
# This is distinct from rm-safety-net.sh (blocks rm on critical paths)
|
|
16
|
+
# and bulk-file-delete-guard.sh (blocks large recursive deletes).
|
|
17
|
+
# This hook specifically targets the mv+rm compound pattern.
|
|
18
|
+
#
|
|
19
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
20
|
+
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
INPUT=$(cat)
|
|
24
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
25
|
+
[ -z "$COMMAND" ] && exit 0
|
|
26
|
+
|
|
27
|
+
# Only check compound commands that contain both mv and rm
|
|
28
|
+
if ! echo "$COMMAND" | grep -qE '\bmv\s' || ! echo "$COMMAND" | grep -qE '\brm\s'; then
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Extract mv source directory and rm target, check for overlap
|
|
33
|
+
# Pattern: mv <src> <dst> [&&;||] rm [-rf] <target>
|
|
34
|
+
# We check if the rm target is a parent of, or the same as, the mv source
|
|
35
|
+
|
|
36
|
+
# Get all mv source paths (first arg after mv and optional flags)
|
|
37
|
+
MV_SOURCES=$(echo "$COMMAND" | grep -oP '\bmv\s+(-[a-zA-Z]+\s+)*\K\S+' 2>/dev/null || true)
|
|
38
|
+
# Get all rm targets
|
|
39
|
+
RM_TARGETS=$(echo "$COMMAND" | grep -oP '\brm\s+(-[a-zA-Z]+\s+)*\K\S+' 2>/dev/null || true)
|
|
40
|
+
|
|
41
|
+
[ -z "$MV_SOURCES" ] && exit 0
|
|
42
|
+
[ -z "$RM_TARGETS" ] && exit 0
|
|
43
|
+
|
|
44
|
+
# Normalize: get parent directory of mv source
|
|
45
|
+
for mv_src in $MV_SOURCES; do
|
|
46
|
+
mv_parent=$(dirname "$mv_src" 2>/dev/null || echo "")
|
|
47
|
+
[ -z "$mv_parent" ] && continue
|
|
48
|
+
|
|
49
|
+
for rm_target in $RM_TARGETS; do
|
|
50
|
+
# Check if rm target matches the mv source's parent or the mv source itself
|
|
51
|
+
# Normalize trailing slashes
|
|
52
|
+
rm_clean="${rm_target%/}"
|
|
53
|
+
mv_parent_clean="${mv_parent%/}"
|
|
54
|
+
mv_src_clean="${mv_src%/}"
|
|
55
|
+
|
|
56
|
+
# Case 1: rm deletes the parent of the moved file
|
|
57
|
+
if [ "$rm_clean" = "$mv_parent_clean" ]; then
|
|
58
|
+
echo "BLOCKED: Move+delete sequence detected — rm target ($rm_target) is parent of mv source ($mv_src)" >&2
|
|
59
|
+
echo "This pattern destroys sibling files. Move files individually instead." >&2
|
|
60
|
+
echo "Command: $COMMAND" >&2
|
|
61
|
+
echo "" >&2
|
|
62
|
+
echo "See: https://github.com/anthropics/claude-code/issues/49129" >&2
|
|
63
|
+
exit 2
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Case 2: rm deletes the exact path that was moved from
|
|
67
|
+
if [ "$rm_clean" = "$mv_src_clean" ]; then
|
|
68
|
+
# mv a dir then rm -rf the same dir — likely the user moved the dir,
|
|
69
|
+
# but rm -rf on the source after mv is suspicious if recursive
|
|
70
|
+
if echo "$COMMAND" | grep -qE "rm\s+.*-[rRf]*[rR].*$rm_target|rm\s+.*-[rRf]*[rR]\s+$rm_target"; then
|
|
71
|
+
echo "BLOCKED: Move+delete sequence detected — rm -r on the same path as mv source ($mv_src)" >&2
|
|
72
|
+
echo "If the directory was already moved, rm -r should not be needed." >&2
|
|
73
|
+
echo "Command: $COMMAND" >&2
|
|
74
|
+
echo "" >&2
|
|
75
|
+
echo "See: https://github.com/anthropics/claude-code/issues/49129" >&2
|
|
76
|
+
exit 2
|
|
77
|
+
fi
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# Case 3: rm deletes an ancestor directory of the mv source
|
|
81
|
+
if echo "$mv_src_clean" | grep -qE "^${rm_clean}/"; then
|
|
82
|
+
echo "BLOCKED: Move+delete sequence detected — rm target ($rm_target) is ancestor of mv source ($mv_src)" >&2
|
|
83
|
+
echo "This pattern destroys the source tree. Use targeted operations instead." >&2
|
|
84
|
+
echo "Command: $COMMAND" >&2
|
|
85
|
+
echo "" >&2
|
|
86
|
+
echo "See: https://github.com/anthropics/claude-code/issues/49129" >&2
|
|
87
|
+
exit 2
|
|
88
|
+
fi
|
|
89
|
+
done
|
|
90
|
+
done
|
|
91
|
+
|
|
92
|
+
exit 0
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# pii-upload-guard.sh — Detect PII in outbound data before upload
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude uploaded physical coordinates to a public website
|
|
5
|
+
# despite CLAUDE.md stating "no PII" for 17 sessions. CLAUDE.md
|
|
6
|
+
# instructions are suggestions; hooks are enforcement. (#46910)
|
|
7
|
+
#
|
|
8
|
+
# How it works: Scans Bash commands for outbound data operations
|
|
9
|
+
# (curl POST/PUT, scp, rsync to remote, git push with config files)
|
|
10
|
+
# and checks if the data being sent contains PII patterns:
|
|
11
|
+
# coordinates, emails, phone numbers, API keys, addresses.
|
|
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
|
+
# Only check outbound data commands
|
|
23
|
+
IS_OUTBOUND=0
|
|
24
|
+
if echo "$CMD" | grep -qiE 'curl\s+.*-X\s*(POST|PUT|PATCH)|curl\s+.*--data|curl\s+.*-d\s'; then
|
|
25
|
+
IS_OUTBOUND=1
|
|
26
|
+
elif echo "$CMD" | grep -qiE '\bscp\b.*:|\brsync\b.*:|\bsftp\b'; then
|
|
27
|
+
IS_OUTBOUND=1
|
|
28
|
+
elif echo "$CMD" | grep -qiE 'curl\s+.*upload|wget\s+.*--post'; then
|
|
29
|
+
IS_OUTBOUND=1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
[ "$IS_OUTBOUND" -eq 0 ] && exit 0
|
|
33
|
+
|
|
34
|
+
# Check for PII patterns in the command
|
|
35
|
+
PII_FOUND=""
|
|
36
|
+
|
|
37
|
+
# GPS coordinates (latitude/longitude pairs)
|
|
38
|
+
if echo "$CMD" | grep -qE '[-]?[0-9]{1,3}\.[0-9]{4,}.*[-]?[0-9]{1,3}\.[0-9]{4,}'; then
|
|
39
|
+
PII_FOUND="GPS coordinates"
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Email addresses
|
|
43
|
+
if echo "$CMD" | grep -qiE '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'; then
|
|
44
|
+
PII_FOUND="${PII_FOUND:+$PII_FOUND, }email address"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Phone numbers (various formats)
|
|
48
|
+
if echo "$CMD" | grep -qE '\+?[0-9]{1,4}[-. ]?\(?[0-9]{1,4}\)?[-. ]?[0-9]{3,4}[-. ]?[0-9]{3,4}'; then
|
|
49
|
+
PII_FOUND="${PII_FOUND:+$PII_FOUND, }phone number"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# API keys / tokens (long hex or base64 strings in data)
|
|
53
|
+
if echo "$CMD" | grep -qE '(key|token|secret|password|api_key|apikey)=[A-Za-z0-9+/=_-]{20,}'; then
|
|
54
|
+
PII_FOUND="${PII_FOUND:+$PII_FOUND, }API key/token"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# Physical addresses (street patterns)
|
|
58
|
+
if echo "$CMD" | grep -qiE '[0-9]+\s+(street|st|avenue|ave|road|rd|boulevard|blvd|drive|dr|lane|ln)\b'; then
|
|
59
|
+
PII_FOUND="${PII_FOUND:+$PII_FOUND, }physical address"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
if [ -n "$PII_FOUND" ]; then
|
|
63
|
+
echo "WARNING: Possible PII detected in outbound data: $PII_FOUND" >&2
|
|
64
|
+
echo " Command: $(echo "$CMD" | head -c 200)" >&2
|
|
65
|
+
echo " Review the data being sent before proceeding." >&2
|
|
66
|
+
echo " If this is intentional, acknowledge the PII and re-run." >&2
|
|
67
|
+
# exit 1 = warning (allow with notice), not exit 2 (block)
|
|
68
|
+
# Some legitimate uses send coordinates/emails
|
|
69
|
+
exit 1
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
exit 0
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
2
|
+
[ -z "$COMMAND" ] && exit 0
|
|
3
|
+
echo "$COMMAND" | grep -qE '\bgh\s+pr\s+create\b' || exit 0
|
|
4
|
+
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
|
5
|
+
[ -z "$BRANCH" ] && exit 0
|
|
6
|
+
EXISTING=$(gh pr list --head "$BRANCH" --state open --json number,title --jq '.[0].number' 2>/dev/null)
|
|
7
|
+
if [ -n "$EXISTING" ]; then
|
|
8
|
+
TITLE=$(gh pr list --head "$BRANCH" --state open --json title --jq '.[0].title' 2>/dev/null)
|
|
9
|
+
echo "BLOCKED: An open PR already exists for branch '$BRANCH'." >&2
|
|
10
|
+
echo " PR #$EXISTING: $TITLE" >&2
|
|
11
|
+
echo " Update the existing PR instead of creating a new one." >&2
|
|
12
|
+
exit 2
|
|
13
|
+
fi
|
|
14
|
+
exit 0
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# production-port-kill-guard.sh — Block commands that kill processes by port number
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude Code killing production services running on ports without
|
|
5
|
+
# understanding their purpose. Real incident: user's CLAUDE.md said
|
|
6
|
+
# port 7000, but Claude killed the process on port 8000 — $1,000 loss.
|
|
7
|
+
# (GitHub Issue #50971)
|
|
8
|
+
#
|
|
9
|
+
# Detects:
|
|
10
|
+
# lsof -ti :PORT | xargs kill (find process by port, then kill)
|
|
11
|
+
# lsof -t -i :PORT | kill (same, different flag style)
|
|
12
|
+
# fuser -k PORT/tcp (directly kill process on port)
|
|
13
|
+
# fuser --kill PORT/tcp (same, long flag)
|
|
14
|
+
# kill $(lsof -ti :PORT) (subshell variant)
|
|
15
|
+
#
|
|
16
|
+
# Does NOT block:
|
|
17
|
+
# lsof -i :PORT (just listing, no kill)
|
|
18
|
+
# fuser PORT/tcp (just checking, no kill)
|
|
19
|
+
# netstat / ss (read-only port inspection)
|
|
20
|
+
#
|
|
21
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
22
|
+
|
|
23
|
+
INPUT=$(cat)
|
|
24
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
25
|
+
|
|
26
|
+
[ -z "$COMMAND" ] && exit 0
|
|
27
|
+
|
|
28
|
+
# Block lsof-to-kill pipeline (lsof -ti :PORT piped to kill/xargs kill)
|
|
29
|
+
# Covers: -ti :PORT, -t -i :PORT, -i :PORT -t, and combined flags like -sti
|
|
30
|
+
if echo "$COMMAND" | grep -qE 'lsof\s.*-[a-zA-Z]*t.*:'; then
|
|
31
|
+
if echo "$COMMAND" | grep -qE '\|\s*(xargs\s+)?kill|kill\s+\$\('; then
|
|
32
|
+
PORT=$(echo "$COMMAND" | grep -oP ':\K\d+' | head -1)
|
|
33
|
+
echo "BLOCKED: Killing process by port number is dangerous." >&2
|
|
34
|
+
echo " Port ${PORT} may be running a production service." >&2
|
|
35
|
+
echo " First check what's running: lsof -i :${PORT}" >&2
|
|
36
|
+
echo " Then decide manually whether to stop it." >&2
|
|
37
|
+
echo " Command: $COMMAND" >&2
|
|
38
|
+
exit 2
|
|
39
|
+
fi
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Block kill $(lsof ...) subshell pattern
|
|
43
|
+
if echo "$COMMAND" | grep -qE 'kill\s+\$\(lsof\s'; then
|
|
44
|
+
echo "BLOCKED: Killing process found by lsof is dangerous." >&2
|
|
45
|
+
echo " Verify the process identity before terminating." >&2
|
|
46
|
+
echo " Command: $COMMAND" >&2
|
|
47
|
+
exit 2
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Block fuser -k (directly kills process on port)
|
|
51
|
+
if echo "$COMMAND" | grep -qE '\bfuser\s+(-[a-zA-Z]*k|--kill)\s'; then
|
|
52
|
+
PORT=$(echo "$COMMAND" | grep -oP '\d+(?=/tcp)' | head -1)
|
|
53
|
+
echo "BLOCKED: fuser -k kills the process on port ${PORT:-unknown} immediately." >&2
|
|
54
|
+
echo " First check: fuser ${PORT:-PORT}/tcp" >&2
|
|
55
|
+
echo " Then stop the service gracefully if needed." >&2
|
|
56
|
+
echo " Command: $COMMAND" >&2
|
|
57
|
+
exit 2
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
exit 0
|