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,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,62 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# proof-log-session.sh — Generate session summary on Stop event
|
|
3
|
+
#
|
|
4
|
+
# Solves: "What did the AI do last week?" — activity logs exist but are unreadable
|
|
5
|
+
# Creates a human-readable 5W1H summary from the activity log at session end.
|
|
6
|
+
#
|
|
7
|
+
# Usage: Add to settings.json as a Stop hook
|
|
8
|
+
#
|
|
9
|
+
# {
|
|
10
|
+
# "hooks": {
|
|
11
|
+
# "Stop": [{
|
|
12
|
+
# "matcher": "",
|
|
13
|
+
# "hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/proof-log-session.sh" }]
|
|
14
|
+
# }]
|
|
15
|
+
# }
|
|
16
|
+
# }
|
|
17
|
+
#
|
|
18
|
+
# Output: ~/ops/proof-log/YYYY-MM-DD.md (appended)
|
|
19
|
+
# Requires: activity-logger.sh to be running as a PostToolUse hook
|
|
20
|
+
|
|
21
|
+
set -u
|
|
22
|
+
|
|
23
|
+
LOG_FILE="${HOME}/.claude/activity-log.jsonl"
|
|
24
|
+
DATE=$(date +"%Y-%m-%d")
|
|
25
|
+
PROOF_DIR="${HOME}/ops/proof-log"
|
|
26
|
+
PROOF_FILE="${PROOF_DIR}/${DATE}.md"
|
|
27
|
+
|
|
28
|
+
mkdir -p "$PROOF_DIR"
|
|
29
|
+
|
|
30
|
+
[ ! -f "$LOG_FILE" ] && exit 0
|
|
31
|
+
|
|
32
|
+
# Count today's activity
|
|
33
|
+
TODAY_START=$(date -u -d "today 00:00:00" +"%Y-%m-%dT" 2>/dev/null || date -u +"%Y-%m-%dT")
|
|
34
|
+
EDIT_COUNT=$(grep -c '"tool":"Edit"' "$LOG_FILE" 2>/dev/null) || EDIT_COUNT=0
|
|
35
|
+
WRITE_COUNT=$(grep -c '"tool":"Write"' "$LOG_FILE" 2>/dev/null) || WRITE_COUNT=0
|
|
36
|
+
BASH_COUNT=$(grep -c '"tool":"Bash"' "$LOG_FILE" 2>/dev/null) || BASH_COUNT=0
|
|
37
|
+
READ_COUNT=$(grep -c '"tool":"Read"' "$LOG_FILE" 2>/dev/null) || READ_COUNT=0
|
|
38
|
+
ERROR_COUNT=$(grep -c '"error_pattern":"[^"]*[a-zA-Z]' "$LOG_FILE" 2>/dev/null) || ERROR_COUNT=0
|
|
39
|
+
|
|
40
|
+
# Get edited files
|
|
41
|
+
FILES=$(grep '"tool":"Edit\|Write"' "$LOG_FILE" 2>/dev/null | jq -r '.file // empty' 2>/dev/null | sort -u | head -10)
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
echo ""
|
|
45
|
+
echo "## Session $(date +"%H:%M")"
|
|
46
|
+
echo "- Edit: ${EDIT_COUNT}, Write: ${WRITE_COUNT}, Bash: ${BASH_COUNT}, Read: ${READ_COUNT}"
|
|
47
|
+
[ "$ERROR_COUNT" -gt 0 ] && echo "- Errors detected: ${ERROR_COUNT}"
|
|
48
|
+
if [ -n "$FILES" ]; then
|
|
49
|
+
echo "- Files touched:"
|
|
50
|
+
echo "$FILES" | while read -r f; do
|
|
51
|
+
[ -n "$f" ] && echo " - $f"
|
|
52
|
+
done
|
|
53
|
+
fi
|
|
54
|
+
} >> "$PROOF_FILE"
|
|
55
|
+
|
|
56
|
+
# Rotate activity log (keep last 1000 lines)
|
|
57
|
+
LINE_COUNT=$(wc -l < "$LOG_FILE" 2>/dev/null) || LINE_COUNT=0
|
|
58
|
+
if [ "$LINE_COUNT" -gt 1000 ]; then
|
|
59
|
+
tail -1000 "$LOG_FILE" > "${LOG_FILE}.tmp" && mv "${LOG_FILE}.tmp" "$LOG_FILE"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
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
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# session-agent-cost-limiter.sh — Cap total subagent spawns per session
|
|
3
|
+
#
|
|
4
|
+
# Solves: #47049 — User lost £140 overnight when Claude spawned 16+
|
|
5
|
+
# subagents. Each agent gets its own context window = 16x token cost.
|
|
6
|
+
# Existing max-concurrent-agents limits simultaneous agents, but not
|
|
7
|
+
# total spawns over a session. This hook limits the cumulative count.
|
|
8
|
+
#
|
|
9
|
+
# How it works: Tracks every Agent spawn in a session-scoped counter.
|
|
10
|
+
# After CC_MAX_SESSION_AGENTS total spawns, blocks further agents.
|
|
11
|
+
# Counter resets when the session ends (file keyed by PPID).
|
|
12
|
+
#
|
|
13
|
+
# CONFIG:
|
|
14
|
+
# CC_MAX_SESSION_AGENTS=10 (default: 10 total agents per session)
|
|
15
|
+
#
|
|
16
|
+
# TRIGGER: PreToolUse
|
|
17
|
+
# MATCHER: "Agent"
|
|
18
|
+
# CATEGORY: cost-control
|
|
19
|
+
|
|
20
|
+
INPUT=$(cat)
|
|
21
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
22
|
+
[ "$TOOL" != "Agent" ] && exit 0
|
|
23
|
+
|
|
24
|
+
MAX_TOTAL=${CC_MAX_SESSION_AGENTS:-10}
|
|
25
|
+
# Use PPID to track the parent Claude Code process, not this subshell
|
|
26
|
+
COUNTER_FILE="/tmp/cc-session-agents-${PPID}"
|
|
27
|
+
|
|
28
|
+
# Initialize if missing
|
|
29
|
+
[ -f "$COUNTER_FILE" ] || echo "0" > "$COUNTER_FILE"
|
|
30
|
+
|
|
31
|
+
CURRENT=$(cat "$COUNTER_FILE" 2>/dev/null || echo 0)
|
|
32
|
+
|
|
33
|
+
if [ "$CURRENT" -ge "$MAX_TOTAL" ]; then
|
|
34
|
+
echo "BLOCKED: Session agent limit reached (${CURRENT}/${MAX_TOTAL} total spawns)." >&2
|
|
35
|
+
echo " Each subagent opens a new context window and consumes tokens independently." >&2
|
|
36
|
+
echo " Consider completing existing work before spawning more agents." >&2
|
|
37
|
+
echo " Override: CC_MAX_SESSION_AGENTS=$((MAX_TOTAL + 5))" >&2
|
|
38
|
+
exit 2
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Increment
|
|
42
|
+
echo $((CURRENT + 1)) > "$COUNTER_FILE"
|
|
43
|
+
exit 0
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# session-cost-alert.sh — Alert when estimated session cost exceeds threshold
|
|
3
|
+
#
|
|
4
|
+
# Solves: #47049 — User lost £140 overnight without realizing costs were
|
|
5
|
+
# accumulating. This hook estimates token cost per tool call and warns
|
|
6
|
+
# when the session total exceeds a configurable threshold.
|
|
7
|
+
#
|
|
8
|
+
# How it works: PostToolUse hook that parses session_tokens from tool
|
|
9
|
+
# results (when available) and estimates cost using Anthropic pricing.
|
|
10
|
+
# Warns at $1 and blocks at $5 (configurable).
|
|
11
|
+
#
|
|
12
|
+
# CONFIG:
|
|
13
|
+
# CC_COST_WARN=1 (warn at $1, default)
|
|
14
|
+
# CC_COST_BLOCK=5 (block at $5, default)
|
|
15
|
+
# CC_MODEL_COST=5 ($/M input tokens for Opus, default)
|
|
16
|
+
#
|
|
17
|
+
# TRIGGER: PostToolUse
|
|
18
|
+
# MATCHER: ""
|
|
19
|
+
# CATEGORY: cost-control
|
|
20
|
+
|
|
21
|
+
INPUT=$(cat)
|
|
22
|
+
|
|
23
|
+
WARN_THRESHOLD=${CC_COST_WARN:-1}
|
|
24
|
+
BLOCK_THRESHOLD=${CC_COST_BLOCK:-5}
|
|
25
|
+
COST_PER_M=${CC_MODEL_COST:-5}
|
|
26
|
+
|
|
27
|
+
COST_FILE="/tmp/cc-session-cost-${PPID}"
|
|
28
|
+
|
|
29
|
+
# Initialize
|
|
30
|
+
if [ ! -f "$COST_FILE" ]; then
|
|
31
|
+
echo "0" > "$COST_FILE"
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Try to extract token count from tool result
|
|
35
|
+
# Note: Not all tool results contain token info. This is a best-effort estimate.
|
|
36
|
+
TOKENS=$(echo "$INPUT" | jq -r '.tool_result // empty' 2>/dev/null | wc -c)
|
|
37
|
+
# Rough estimate: 1 char ≈ 0.3 tokens (for tool output going into context)
|
|
38
|
+
EST_TOKENS=$((TOKENS * 3 / 10))
|
|
39
|
+
|
|
40
|
+
# Add to running total
|
|
41
|
+
CURRENT=$(cat "$COST_FILE" 2>/dev/null || echo 0)
|
|
42
|
+
TOTAL=$((CURRENT + EST_TOKENS))
|
|
43
|
+
echo "$TOTAL" > "$COST_FILE"
|
|
44
|
+
|
|
45
|
+
# Estimate cost
|
|
46
|
+
COST=$(echo "scale=4; $TOTAL * $COST_PER_M / 1000000" | bc 2>/dev/null || echo "0")
|
|
47
|
+
COST_CENTS=$(echo "scale=0; $TOTAL * $COST_PER_M / 10000" | bc 2>/dev/null || echo "0")
|
|
48
|
+
|
|
49
|
+
# Check thresholds
|
|
50
|
+
BLOCK_CENTS=$(echo "scale=0; $BLOCK_THRESHOLD * 100" | bc 2>/dev/null || echo "500")
|
|
51
|
+
WARN_CENTS=$(echo "scale=0; $WARN_THRESHOLD * 100" | bc 2>/dev/null || echo "100")
|
|
52
|
+
|
|
53
|
+
if [ "$COST_CENTS" -ge "$BLOCK_CENTS" ] 2>/dev/null; then
|
|
54
|
+
echo "BLOCKED: Estimated session cost \$${COST} exceeds \$${BLOCK_THRESHOLD} limit." >&2
|
|
55
|
+
echo " Estimated tokens used: ${TOTAL}" >&2
|
|
56
|
+
echo " Override: CC_COST_BLOCK=$((BLOCK_THRESHOLD * 2))" >&2
|
|
57
|
+
exit 2
|
|
58
|
+
elif [ "$COST_CENTS" -ge "$WARN_CENTS" ] 2>/dev/null; then
|
|
59
|
+
echo "WARNING: Estimated session cost \$${COST} approaching \$${BLOCK_THRESHOLD} limit." >&2
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
exit 0
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
MAX_RSS_MB="${CC_MAX_RSS_MB:-4096}"
|
|
2
2
|
CHECK_INTERVAL=300
|
|
3
|
+
PID_FILE="/tmp/cc-memory-watchdog.pid"
|
|
4
|
+
|
|
5
|
+
# Prevent duplicate instances — only one watchdog should run at a time
|
|
6
|
+
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
|
7
|
+
exit 0
|
|
8
|
+
fi
|
|
9
|
+
|
|
3
10
|
(
|
|
11
|
+
echo $$ > "$PID_FILE"
|
|
12
|
+
trap 'rm -f "$PID_FILE"' EXIT
|
|
4
13
|
while true; do
|
|
5
14
|
sleep "$CHECK_INTERVAL"
|
|
6
15
|
pgrep -f "claude" | while read pid; do
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# settings-integrity-monitor.sh — Detect unexpected settings.json changes
|
|
3
|
+
# Trigger: PreToolUse
|
|
4
|
+
# Matcher: (empty — runs on every tool use)
|
|
5
|
+
#
|
|
6
|
+
# The /model command silently rewrites settings.json from scratch,
|
|
7
|
+
# removing sandbox restrictions and hook configurations.
|
|
8
|
+
# See: https://github.com/anthropics/claude-code/issues/44791
|
|
9
|
+
#
|
|
10
|
+
# This hook maintains a checksum of settings.json and warns when
|
|
11
|
+
# it changes outside your control. It also creates automatic backups
|
|
12
|
+
# so you can restore your configuration.
|
|
13
|
+
#
|
|
14
|
+
# TRIGGER: PreToolUse MATCHER: ""
|
|
15
|
+
|
|
16
|
+
SETTINGS="${CLAUDE_SETTINGS_FILE:-$HOME/.claude/settings.json}"
|
|
17
|
+
BACKUP_DIR="$HOME/.claude/settings-backups"
|
|
18
|
+
CHECKSUM_FILE="$BACKUP_DIR/.checksum"
|
|
19
|
+
|
|
20
|
+
# Exit silently if settings.json doesn't exist
|
|
21
|
+
[ -f "$SETTINGS" ] || exit 0
|
|
22
|
+
|
|
23
|
+
mkdir -p "$BACKUP_DIR"
|
|
24
|
+
|
|
25
|
+
CURRENT_HASH=$(sha256sum "$SETTINGS" 2>/dev/null | cut -d' ' -f1)
|
|
26
|
+
|
|
27
|
+
if [ ! -f "$CHECKSUM_FILE" ]; then
|
|
28
|
+
# First run: save baseline
|
|
29
|
+
echo "$CURRENT_HASH" > "$CHECKSUM_FILE"
|
|
30
|
+
cp "$SETTINGS" "$BACKUP_DIR/settings.json.baseline"
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
SAVED_HASH=$(cat "$CHECKSUM_FILE" 2>/dev/null)
|
|
35
|
+
|
|
36
|
+
if [ "$CURRENT_HASH" != "$SAVED_HASH" ]; then
|
|
37
|
+
# Settings changed — create timestamped backup of PREVIOUS version
|
|
38
|
+
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
|
39
|
+
if [ -f "$BACKUP_DIR/settings.json.latest" ]; then
|
|
40
|
+
cp "$BACKUP_DIR/settings.json.latest" "$BACKUP_DIR/settings.json.$TIMESTAMP"
|
|
41
|
+
fi
|
|
42
|
+
# Save current as latest
|
|
43
|
+
cp "$SETTINGS" "$BACKUP_DIR/settings.json.latest"
|
|
44
|
+
echo "$CURRENT_HASH" > "$CHECKSUM_FILE"
|
|
45
|
+
|
|
46
|
+
# Count hooks in old vs new
|
|
47
|
+
OLD_HOOKS=$(jq '[.hooks | to_entries[].value[].hooks[]?] | length' "$BACKUP_DIR/settings.json.$TIMESTAMP" 2>/dev/null || echo "?")
|
|
48
|
+
NEW_HOOKS=$(jq '[.hooks | to_entries[].value[].hooks[]?] | length' "$SETTINGS" 2>/dev/null || echo "?")
|
|
49
|
+
|
|
50
|
+
echo "⚠ settings.json was modified (hooks: $OLD_HOOKS → $NEW_HOOKS)" >&2
|
|
51
|
+
echo " Backup saved: $BACKUP_DIR/settings.json.$TIMESTAMP" >&2
|
|
52
|
+
echo " Restore: cp $BACKUP_DIR/settings.json.$TIMESTAMP $SETTINGS" >&2
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
exit 0
|