cc-safe-setup 29.6.40 → 29.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +66 -0
- package/.claude-plugin/plugin.json +11 -0
- package/README.md +123 -12
- package/SETTINGS_REFERENCE.md +2 -0
- package/SKILL.md +47 -0
- package/examples/README.md +11 -1
- package/examples/auto-approve-compound-git.sh +3 -0
- package/examples/auto-compact-context-monitor.sh +35 -0
- package/examples/auto-mode-safety-enforcer.sh +57 -0
- package/examples/background-task-guard.sh +57 -0
- package/examples/broad-find-guard.sh +62 -0
- package/examples/cache-creation-spike-detector.sh +32 -0
- package/examples/case-insensitive-path-guard.sh +96 -0
- package/examples/cjk-punctuation-guard.sh +44 -0
- package/examples/clipboard-secret-guard.sh +29 -0
- package/examples/context-size-alert.sh +38 -0
- package/examples/context-usage-drift-alert.sh +33 -0
- package/examples/dangerous-pip-flag-guard.sh +51 -0
- package/examples/deny-bypass-detector.sh +143 -0
- package/examples/dotenv-read-guard.sh +48 -0
- package/examples/dotfile-protection-guard.sh +60 -0
- package/examples/effort-tracking-logger.sh +30 -0
- package/examples/financial-operation-guard.sh +47 -0
- package/examples/full-rewrite-detector.sh +63 -0
- package/examples/home-critical-bash-guard.sh +56 -0
- package/examples/idle-session-cost-alert.sh +36 -0
- package/examples/model-version-alert.sh +18 -0
- package/examples/model-version-change-alert.sh +31 -0
- package/examples/move-delete-sequence-guard.sh +92 -0
- package/examples/pii-upload-guard.sh +72 -0
- package/examples/pr-duplicate-guard.sh +14 -0
- package/examples/production-port-kill-guard.sh +60 -0
- package/examples/quota-reset-cycle-monitor.sh +30 -0
- package/examples/repo-visibility-guard.sh +33 -0
- package/examples/sandbox-relative-path-audit.sh +51 -0
- package/examples/session-agent-cost-limiter.sh +43 -0
- package/examples/session-cost-alert.sh +62 -0
- package/examples/session-memory-watchdog.sh +9 -0
- package/examples/settings-integrity-monitor.sh +55 -0
- package/examples/settings-json-model-guard.sh +89 -0
- package/examples/shell-config-truncation-guard.sh +97 -0
- package/examples/shell-wrapper-guard.sh +4 -4
- package/examples/subagent-spawn-rate-monitor.sh +34 -0
- package/examples/subcommand-chain-guard.sh +44 -0
- package/examples/system-dir-protection-guard.sh +100 -0
- package/examples/thinking-display-enforcer.sh +25 -0
- package/examples/tool-retry-budget-guard.sh +59 -0
- package/examples/worktree-branch-pollution-detector.sh +35 -0
- package/examples/worktree-create-log.sh +6 -0
- package/examples/worktree-hook-linker.sh +72 -0
- package/examples/worktree-remove-uncommitted-guard.sh +20 -0
- package/hooks/hooks.json +60 -0
- package/index.mjs +92 -6
- package/memory/market-anthropic-japan-strategy-2026-04-13.md +4 -0
- package/package.json +2 -2
- package/plugins/credential-guard/.claude-plugin/plugin.json +58 -0
- package/plugins/git-protection/.claude-plugin/plugin.json +58 -0
- package/plugins/safety-essentials/.claude-plugin/plugin.json +58 -0
- package/plugins/token-guard/.claude-plugin/plugin.json +51 -0
- package/skills/safety-setup/SKILL.md +47 -0
- package/tests/dotenv-read-guard.test.sh +65 -0
- package/tests/test-auto-mode-safety-enforcer.sh +55 -0
- package/tests/test-case-insensitive-path-guard.sh +78 -0
- package/tests/test-context-usage-drift-alert.sh +52 -0
- package/tests/test-dangerous-pip-flag-guard.sh +56 -0
- package/tests/test-dotfile-protection-guard.sh +68 -0
- package/tests/test-effort-tracking-logger.sh +55 -0
- package/tests/test-financial-operation-guard.sh +59 -0
- package/tests/test-home-critical-bash-guard.sh +59 -0
- package/tests/test-model-version-change-alert.sh +55 -0
- package/tests/test-move-delete-sequence-guard.sh +63 -0
- package/tests/test-pr-duplicate-guard.sh +29 -0
- package/tests/test-quota-reset-cycle-monitor.sh +52 -0
- package/tests/test-shell-config-truncation-guard.sh +104 -0
- package/tests/test-subagent-spawn-rate-monitor.sh +43 -0
- package/tests/test-system-dir-protection-guard.sh +81 -0
- package/tests/test-tool-retry-budget-guard.sh +75 -0
- package/tests/test-worktree-branch-pollution-detector.sh +50 -0
- package/tests/test-worktree-lifecycle-hooks.sh +29 -0
|
@@ -0,0 +1,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
|
|
@@ -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
|
|
@@ -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
|