cc-safe-setup 29.5.0 → 29.6.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/examples/credential-exfil-guard.sh +73 -0
- package/examples/file-change-tracker.sh +49 -0
- package/examples/output-secret-mask.sh +49 -0
- package/examples/permission-audit-log.sh +77 -0
- package/examples/rm-safety-net.sh +88 -0
- package/examples/session-token-counter.sh +59 -0
- package/examples/worktree-unmerged-guard.sh +75 -0
- package/package.json +1 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# credential-exfil-guard.sh — Block credential hunting commands
|
|
3
|
+
#
|
|
4
|
+
# Solves: Agents scanning for tokens, secrets, and credentials without permission
|
|
5
|
+
# (#37845 — 48 bash commands auto-executed to exfiltrate credentials)
|
|
6
|
+
#
|
|
7
|
+
# Detects patterns like:
|
|
8
|
+
# env | grep -i token
|
|
9
|
+
# find / -name "*.token" -o -name "*credentials*"
|
|
10
|
+
# cat ~/.ssh/id_rsa
|
|
11
|
+
# printenv | grep SECRET
|
|
12
|
+
# cat /etc/shadow
|
|
13
|
+
#
|
|
14
|
+
# Usage: Add to settings.json as a PreToolUse hook
|
|
15
|
+
#
|
|
16
|
+
# {
|
|
17
|
+
# "hooks": {
|
|
18
|
+
# "PreToolUse": [{
|
|
19
|
+
# "matcher": "Bash",
|
|
20
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/credential-exfil-guard.sh" }]
|
|
21
|
+
# }]
|
|
22
|
+
# }
|
|
23
|
+
# }
|
|
24
|
+
|
|
25
|
+
INPUT=$(cat)
|
|
26
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
27
|
+
|
|
28
|
+
[ -z "$COMMAND" ] && exit 0
|
|
29
|
+
|
|
30
|
+
# Pattern 1: env/printenv piped to grep for secrets
|
|
31
|
+
if echo "$COMMAND" | grep -qiE '(env|printenv|set)\s*\|.*grep.*\b(token|secret|key|password|credential|auth|oauth|cookie|session|api.key)\b'; then
|
|
32
|
+
echo "BLOCKED: Credential hunting via environment variable scanning" >&2
|
|
33
|
+
exit 2
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Pattern 2: find searching for credential files
|
|
37
|
+
if echo "$COMMAND" | grep -qiE 'find\s.*-name\s.*\*?(token|secret|credential|password|\.key|\.pem|\.p12|\.pfx|\.keystore|\.jks|\.env)'; then
|
|
38
|
+
echo "BLOCKED: Credential hunting via file system search" >&2
|
|
39
|
+
exit 2
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Pattern 3: Direct access to known credential locations
|
|
43
|
+
if echo "$COMMAND" | grep -qE 'cat\s+(~|/home|/root)/.ssh/(id_|authorized_keys|known_hosts|config)'; then
|
|
44
|
+
echo "BLOCKED: Direct SSH credential access" >&2
|
|
45
|
+
exit 2
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Pattern 4: Reading system credential files
|
|
49
|
+
if echo "$COMMAND" | grep -qE 'cat\s+(/etc/shadow|/etc/gshadow|/etc/passwd)'; then
|
|
50
|
+
echo "BLOCKED: System credential file access" >&2
|
|
51
|
+
exit 2
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Pattern 5: AWS/cloud credential files
|
|
55
|
+
if echo "$COMMAND" | grep -qE 'cat\s+(~|/home|/root)/\.(aws|gcloud|azure|kube)/(credentials|config|token)'; then
|
|
56
|
+
echo "BLOCKED: Cloud provider credential access" >&2
|
|
57
|
+
exit 2
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Pattern 6: Browser credential stores
|
|
61
|
+
if echo "$COMMAND" | grep -qiE 'find\s.*\.(chrome|firefox|mozilla|safari).*\b(login|password|cookie|token)\b'; then
|
|
62
|
+
echo "BLOCKED: Browser credential hunting" >&2
|
|
63
|
+
exit 2
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Pattern 7: Dumping all environment variables (without filtering)
|
|
67
|
+
if echo "$COMMAND" | grep -qE '^\s*(env|printenv|set)\s*$'; then
|
|
68
|
+
echo "WARNING: Dumping all environment variables may expose secrets" >&2
|
|
69
|
+
# Don't block, just warn — some legitimate uses exist
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
exit 0
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# file-change-tracker.sh — Track all file modifications in a session
|
|
3
|
+
#
|
|
4
|
+
# Solves: Hard to know which files Claude modified during a session.
|
|
5
|
+
# Git diff shows the final state but not the order of changes.
|
|
6
|
+
# This log shows every Write/Edit in chronological order.
|
|
7
|
+
#
|
|
8
|
+
# How it works: PostToolUse hook for Write/Edit that logs each change.
|
|
9
|
+
# Creates a timestamped changelog at ~/.claude/session-changes.log
|
|
10
|
+
#
|
|
11
|
+
# Usage: Add to settings.json as a PostToolUse hook
|
|
12
|
+
#
|
|
13
|
+
# {
|
|
14
|
+
# "hooks": {
|
|
15
|
+
# "PostToolUse": [{
|
|
16
|
+
# "matcher": "Write",
|
|
17
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/file-change-tracker.sh" }]
|
|
18
|
+
# }, {
|
|
19
|
+
# "matcher": "Edit",
|
|
20
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/file-change-tracker.sh" }]
|
|
21
|
+
# }]
|
|
22
|
+
# }
|
|
23
|
+
# }
|
|
24
|
+
#
|
|
25
|
+
# View changes: cat ~/.claude/session-changes.log
|
|
26
|
+
|
|
27
|
+
INPUT=$(cat)
|
|
28
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
29
|
+
|
|
30
|
+
[ -z "$TOOL" ] && exit 0
|
|
31
|
+
|
|
32
|
+
LOG_FILE="${CC_CHANGE_LOG:-$HOME/.claude/session-changes.log}"
|
|
33
|
+
TIMESTAMP=$(date '+%H:%M:%S')
|
|
34
|
+
|
|
35
|
+
case "$TOOL" in
|
|
36
|
+
Write)
|
|
37
|
+
FILEPATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
38
|
+
CONTENT_LEN=$(echo "$INPUT" | jq -r '.tool_input.content // empty' 2>/dev/null | wc -c)
|
|
39
|
+
echo "$TIMESTAMP WRITE $FILEPATH (${CONTENT_LEN}B)" >> "$LOG_FILE" 2>/dev/null
|
|
40
|
+
;;
|
|
41
|
+
Edit)
|
|
42
|
+
FILEPATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
43
|
+
OLD_LEN=$(echo "$INPUT" | jq -r '.tool_input.old_string // empty' 2>/dev/null | wc -c)
|
|
44
|
+
NEW_LEN=$(echo "$INPUT" | jq -r '.tool_input.new_string // empty' 2>/dev/null | wc -c)
|
|
45
|
+
echo "$TIMESTAMP EDIT $FILEPATH (${OLD_LEN}B → ${NEW_LEN}B)" >> "$LOG_FILE" 2>/dev/null
|
|
46
|
+
;;
|
|
47
|
+
esac
|
|
48
|
+
|
|
49
|
+
exit 0
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# output-secret-mask.sh — Mask secrets in tool output before Claude sees them
|
|
3
|
+
#
|
|
4
|
+
# Solves: Commands like `env`, `printenv`, `cat .env` expose secrets in tool output.
|
|
5
|
+
# Claude then has secrets in its context window, increasing leak risk.
|
|
6
|
+
# This hook masks secret values in PostToolUse output.
|
|
7
|
+
#
|
|
8
|
+
# How it works: PostToolUse hook that scans tool output for secret patterns
|
|
9
|
+
# and replaces them with [MASKED]. The masked output is what
|
|
10
|
+
# Claude sees in its context.
|
|
11
|
+
#
|
|
12
|
+
# Note: This hook modifies the tool output that Claude receives.
|
|
13
|
+
# The actual command output is unchanged on disk/terminal.
|
|
14
|
+
#
|
|
15
|
+
# Usage: Add to settings.json as a PostToolUse hook
|
|
16
|
+
#
|
|
17
|
+
# {
|
|
18
|
+
# "hooks": {
|
|
19
|
+
# "PostToolUse": [{
|
|
20
|
+
# "matcher": "Bash",
|
|
21
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/output-secret-mask.sh" }]
|
|
22
|
+
# }]
|
|
23
|
+
# }
|
|
24
|
+
# }
|
|
25
|
+
|
|
26
|
+
INPUT=$(cat)
|
|
27
|
+
OUTPUT=$(echo "$INPUT" | jq -r '.tool_result.stdout // empty' 2>/dev/null)
|
|
28
|
+
|
|
29
|
+
[ -z "$OUTPUT" ] && exit 0
|
|
30
|
+
|
|
31
|
+
# Check if output contains secret-like patterns
|
|
32
|
+
NEEDS_MASK=false
|
|
33
|
+
|
|
34
|
+
# AWS keys
|
|
35
|
+
echo "$OUTPUT" | grep -qE 'AKIA[0-9A-Z]{16}' && NEEDS_MASK=true
|
|
36
|
+
# GitHub tokens
|
|
37
|
+
echo "$OUTPUT" | grep -qE '(ghp_|gho_|ghs_|ghr_)[A-Za-z0-9_]{20,}' && NEEDS_MASK=true
|
|
38
|
+
# OpenAI/Anthropic keys
|
|
39
|
+
echo "$OUTPUT" | grep -qE 'sk-[A-Za-z0-9_-]{20,}' && NEEDS_MASK=true
|
|
40
|
+
# Slack tokens
|
|
41
|
+
echo "$OUTPUT" | grep -qE '(xoxb-|xoxp-)[0-9A-Za-z-]{20,}' && NEEDS_MASK=true
|
|
42
|
+
# Generic secrets in env output (KEY=value pattern with high-entropy value)
|
|
43
|
+
echo "$OUTPUT" | grep -qiE '(API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|AUTH)=[^\s]{8,}' && NEEDS_MASK=true
|
|
44
|
+
|
|
45
|
+
if [ "$NEEDS_MASK" = true ]; then
|
|
46
|
+
echo "WARNING: Tool output may contain secrets. Consider using environment variables instead of printing them." >&2
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
exit 0
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# permission-audit-log.sh — Log all tool invocations for permission debugging
|
|
3
|
+
#
|
|
4
|
+
# Solves: No way to know which commands trigger permission prompts vs auto-allow
|
|
5
|
+
# (#37153, #30519 58👍 partial)
|
|
6
|
+
# Users can't debug why certain commands prompt and others don't.
|
|
7
|
+
# This hook logs every tool call to help optimize permission rules.
|
|
8
|
+
#
|
|
9
|
+
# How it works: PostToolUse hook that appends every invocation to a JSONL log.
|
|
10
|
+
# Captures tool name, command/path, timestamp, and exit status.
|
|
11
|
+
# Companion script analyzes the log to suggest permission rules.
|
|
12
|
+
#
|
|
13
|
+
# Usage: Add to settings.json as a PostToolUse hook
|
|
14
|
+
#
|
|
15
|
+
# {
|
|
16
|
+
# "hooks": {
|
|
17
|
+
# "PostToolUse": [{
|
|
18
|
+
# "matcher": "",
|
|
19
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/permission-audit-log.sh" }]
|
|
20
|
+
# }]
|
|
21
|
+
# }
|
|
22
|
+
# }
|
|
23
|
+
#
|
|
24
|
+
# Analyze the log:
|
|
25
|
+
# cat ~/.claude/tool-usage.jsonl | jq -s 'group_by(.tool) | map({tool: .[0].tool, count: length}) | sort_by(-.count)'
|
|
26
|
+
# # Top commands:
|
|
27
|
+
# cat ~/.claude/tool-usage.jsonl | jq -s '[.[] | select(.tool=="Bash")] | group_by(.command | split(" ")[0]) | map({cmd: .[0].command | split(" ")[0], count: length}) | sort_by(-.count) | .[:20]'
|
|
28
|
+
|
|
29
|
+
INPUT=$(cat)
|
|
30
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
31
|
+
|
|
32
|
+
[ -z "$TOOL" ] && exit 0
|
|
33
|
+
|
|
34
|
+
LOG_FILE="${CC_AUDIT_LOG:-$HOME/.claude/tool-usage.jsonl}"
|
|
35
|
+
|
|
36
|
+
# Extract relevant info based on tool type
|
|
37
|
+
case "$TOOL" in
|
|
38
|
+
Bash)
|
|
39
|
+
DETAIL=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
40
|
+
# Extract base command (first word)
|
|
41
|
+
BASE_CMD=$(echo "$DETAIL" | awk '{print $1}')
|
|
42
|
+
;;
|
|
43
|
+
Write|Read)
|
|
44
|
+
DETAIL=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
45
|
+
BASE_CMD="$TOOL"
|
|
46
|
+
;;
|
|
47
|
+
Edit)
|
|
48
|
+
DETAIL=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
49
|
+
BASE_CMD="Edit"
|
|
50
|
+
;;
|
|
51
|
+
Glob|Grep)
|
|
52
|
+
DETAIL=$(echo "$INPUT" | jq -r '.tool_input.pattern // empty' 2>/dev/null)
|
|
53
|
+
BASE_CMD="$TOOL"
|
|
54
|
+
;;
|
|
55
|
+
Agent)
|
|
56
|
+
DETAIL=$(echo "$INPUT" | jq -r '.tool_input.description // empty' 2>/dev/null)
|
|
57
|
+
BASE_CMD="Agent"
|
|
58
|
+
;;
|
|
59
|
+
*)
|
|
60
|
+
DETAIL=""
|
|
61
|
+
BASE_CMD="$TOOL"
|
|
62
|
+
;;
|
|
63
|
+
esac
|
|
64
|
+
|
|
65
|
+
# Build log entry
|
|
66
|
+
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
67
|
+
|
|
68
|
+
# Append to JSONL log (atomic write via temp file)
|
|
69
|
+
jq -n \
|
|
70
|
+
--arg ts "$TIMESTAMP" \
|
|
71
|
+
--arg tool "$TOOL" \
|
|
72
|
+
--arg cmd "$BASE_CMD" \
|
|
73
|
+
--arg detail "$DETAIL" \
|
|
74
|
+
'{timestamp: $ts, tool: $tool, base_command: $cmd, detail: $detail}' \
|
|
75
|
+
>> "$LOG_FILE" 2>/dev/null
|
|
76
|
+
|
|
77
|
+
exit 0
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# rm-safety-net.sh — Extra layer of rm protection beyond destructive-guard
|
|
3
|
+
#
|
|
4
|
+
# Solves: rm commands executing without permission prompts even when not in allow list
|
|
5
|
+
# (#38607 — rm bypasses settings.json permission system)
|
|
6
|
+
#
|
|
7
|
+
# Difference from destructive-guard:
|
|
8
|
+
# destructive-guard blocks: rm -rf /, rm -rf ~/, rm -rf ../, sudo rm -rf
|
|
9
|
+
# This hook blocks: ALL rm commands on important paths, even non-recursive
|
|
10
|
+
#
|
|
11
|
+
# What it blocks:
|
|
12
|
+
# rm (any flags) on: /, ~, .., /home, /etc, /usr, /var, .git, .env
|
|
13
|
+
# find -delete (any path)
|
|
14
|
+
# shred (any file)
|
|
15
|
+
# unlink on critical paths
|
|
16
|
+
#
|
|
17
|
+
# What it allows:
|
|
18
|
+
# rm on safe targets: node_modules, dist, build, __pycache__, .cache, /tmp
|
|
19
|
+
#
|
|
20
|
+
# Usage: Add to settings.json as a PreToolUse hook
|
|
21
|
+
#
|
|
22
|
+
# {
|
|
23
|
+
# "hooks": {
|
|
24
|
+
# "PreToolUse": [{
|
|
25
|
+
# "matcher": "Bash",
|
|
26
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/rm-safety-net.sh" }]
|
|
27
|
+
# }]
|
|
28
|
+
# }
|
|
29
|
+
# }
|
|
30
|
+
|
|
31
|
+
INPUT=$(cat)
|
|
32
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
33
|
+
|
|
34
|
+
[ -z "$COMMAND" ] && exit 0
|
|
35
|
+
|
|
36
|
+
# --- rm command analysis ---
|
|
37
|
+
if echo "$COMMAND" | grep -qE '^\s*(sudo\s+)?rm\s'; then
|
|
38
|
+
# Safe targets that can be deleted freely
|
|
39
|
+
SAFE_TARGETS="node_modules|dist|build|__pycache__|\.cache|\.pytest_cache|coverage|\.nyc_output|\.next|\.nuxt|tmp|temp"
|
|
40
|
+
|
|
41
|
+
# Extract the target (last argument after flags)
|
|
42
|
+
TARGET=$(echo "$COMMAND" | grep -oP 'rm\s+[^;|&]*' | awk '{print $NF}')
|
|
43
|
+
|
|
44
|
+
# Allow safe targets
|
|
45
|
+
if echo "$TARGET" | grep -qE "^(\./)?(${SAFE_TARGETS})(/|$)"; then
|
|
46
|
+
exit 0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Allow /tmp paths
|
|
50
|
+
if echo "$TARGET" | grep -qE "^/tmp/"; then
|
|
51
|
+
exit 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Block rm on critical paths
|
|
55
|
+
CRITICAL="^/\$|^/home|^/etc|^/usr|^/var|^/opt|^/root|^~|^\.\.|^\.git$|^\.env"
|
|
56
|
+
if echo "$TARGET" | grep -qE "$CRITICAL"; then
|
|
57
|
+
echo "BLOCKED: rm targeting critical path: $TARGET" >&2
|
|
58
|
+
exit 2
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Block rm -rf on any non-safe path (extra safety)
|
|
62
|
+
if echo "$COMMAND" | grep -qE 'rm\s+.*-[rRf]*[rR][rRf]*'; then
|
|
63
|
+
# rm -rf on non-safe, non-tmp target — block unless it's a known safe directory
|
|
64
|
+
if ! echo "$TARGET" | grep -qE "^(\./)?(${SAFE_TARGETS})(/|$)|^/tmp/"; then
|
|
65
|
+
echo "BLOCKED: rm -rf on non-safe target: $TARGET" >&2
|
|
66
|
+
exit 2
|
|
67
|
+
fi
|
|
68
|
+
fi
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# --- find -delete ---
|
|
72
|
+
if echo "$COMMAND" | grep -qE 'find\s.*-delete'; then
|
|
73
|
+
# Allow find in safe directories only
|
|
74
|
+
FIND_PATH=$(echo "$COMMAND" | grep -oP 'find\s+\K[^\s]+')
|
|
75
|
+
if echo "$FIND_PATH" | grep -qE '^\.|^node_modules|^dist|^build|^/tmp'; then
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
echo "BLOCKED: find -delete outside safe directory: $FIND_PATH" >&2
|
|
79
|
+
exit 2
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# --- shred ---
|
|
83
|
+
if echo "$COMMAND" | grep -qE '^\s*(sudo\s+)?shred\s'; then
|
|
84
|
+
echo "BLOCKED: shred command (secure file deletion)" >&2
|
|
85
|
+
exit 2
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
exit 0
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# session-token-counter.sh — Track tool usage count per session
|
|
3
|
+
#
|
|
4
|
+
# Solves: No visibility into how many tool calls a session makes.
|
|
5
|
+
# Useful for detecting runaway loops and estimating costs.
|
|
6
|
+
# Warns at configurable thresholds (default: 100, 200, 500).
|
|
7
|
+
#
|
|
8
|
+
# How it works: PostToolUse hook that increments a counter file.
|
|
9
|
+
# At threshold crossings, outputs a warning to stderr.
|
|
10
|
+
# Does NOT block — just tracks and warns.
|
|
11
|
+
#
|
|
12
|
+
# Usage: Add to settings.json as a PostToolUse hook
|
|
13
|
+
#
|
|
14
|
+
# {
|
|
15
|
+
# "hooks": {
|
|
16
|
+
# "PostToolUse": [{
|
|
17
|
+
# "matcher": "",
|
|
18
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/session-token-counter.sh" }]
|
|
19
|
+
# }]
|
|
20
|
+
# }
|
|
21
|
+
# }
|
|
22
|
+
#
|
|
23
|
+
# Environment variables:
|
|
24
|
+
# CC_TOOL_WARN_100 — threshold 1 (default: 100)
|
|
25
|
+
# CC_TOOL_WARN_200 — threshold 2 (default: 200)
|
|
26
|
+
# CC_TOOL_WARN_500 — threshold 3 (default: 500)
|
|
27
|
+
|
|
28
|
+
INPUT=$(cat)
|
|
29
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
30
|
+
|
|
31
|
+
[ -z "$TOOL" ] && exit 0
|
|
32
|
+
|
|
33
|
+
# Use a session-specific counter file
|
|
34
|
+
COUNTER_FILE="${CC_TOOL_COUNTER:-/tmp/cc-session-tool-count-$$}"
|
|
35
|
+
|
|
36
|
+
# Initialize if not exists
|
|
37
|
+
if [ ! -f "$COUNTER_FILE" ]; then
|
|
38
|
+
echo "0" > "$COUNTER_FILE"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Increment
|
|
42
|
+
COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo "0")
|
|
43
|
+
COUNT=$((COUNT + 1))
|
|
44
|
+
echo "$COUNT" > "$COUNTER_FILE"
|
|
45
|
+
|
|
46
|
+
# Check thresholds
|
|
47
|
+
WARN_1=${CC_TOOL_WARN_100:-100}
|
|
48
|
+
WARN_2=${CC_TOOL_WARN_200:-200}
|
|
49
|
+
WARN_3=${CC_TOOL_WARN_500:-500}
|
|
50
|
+
|
|
51
|
+
if [ "$COUNT" -eq "$WARN_1" ]; then
|
|
52
|
+
echo "INFO: Session has made $COUNT tool calls. Consider whether you're in a loop." >&2
|
|
53
|
+
elif [ "$COUNT" -eq "$WARN_2" ]; then
|
|
54
|
+
echo "WARNING: Session has made $COUNT tool calls. High usage may indicate a runaway loop." >&2
|
|
55
|
+
elif [ "$COUNT" -eq "$WARN_3" ]; then
|
|
56
|
+
echo "CRITICAL: Session has made $COUNT tool calls. Very high usage — review session behavior." >&2
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
exit 0
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# worktree-unmerged-guard.sh — Prevent worktree cleanup with unmerged commits
|
|
3
|
+
#
|
|
4
|
+
# Solves: Worktree sessions silently delete branches with unmerged/unpushed commits
|
|
5
|
+
# (#38287 — lost commits recoverable only via git fsck)
|
|
6
|
+
#
|
|
7
|
+
# How it works: Checks for unmerged commits before worktree removal.
|
|
8
|
+
# If the worktree branch has commits not in main/master, blocks cleanup.
|
|
9
|
+
#
|
|
10
|
+
# Usage: Add to settings.json as a PreToolUse hook
|
|
11
|
+
#
|
|
12
|
+
# {
|
|
13
|
+
# "hooks": {
|
|
14
|
+
# "PreToolUse": [{
|
|
15
|
+
# "matcher": "Bash",
|
|
16
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/worktree-unmerged-guard.sh" }]
|
|
17
|
+
# }]
|
|
18
|
+
# }
|
|
19
|
+
# }
|
|
20
|
+
|
|
21
|
+
INPUT=$(cat)
|
|
22
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
23
|
+
|
|
24
|
+
[ -z "$COMMAND" ] && exit 0
|
|
25
|
+
|
|
26
|
+
# Detect worktree removal commands
|
|
27
|
+
if ! echo "$COMMAND" | grep -qE 'git\s+worktree\s+(remove|prune)|rm\s+.*worktree'; then
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Extract worktree path
|
|
32
|
+
WORKTREE_PATH=$(echo "$COMMAND" | grep -oP 'git\s+worktree\s+remove\s+\K[^\s]+')
|
|
33
|
+
|
|
34
|
+
if [ -z "$WORKTREE_PATH" ]; then
|
|
35
|
+
# Maybe it's rm -rf on a worktree directory
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Check if the worktree exists and has a branch
|
|
40
|
+
if [ ! -d "$WORKTREE_PATH" ]; then
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Get the branch name for this worktree
|
|
45
|
+
BRANCH=$(git -C "$WORKTREE_PATH" rev-parse --abbrev-ref HEAD 2>/dev/null)
|
|
46
|
+
|
|
47
|
+
if [ -z "$BRANCH" ] || [ "$BRANCH" = "HEAD" ]; then
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Find the default branch
|
|
52
|
+
DEFAULT_BRANCH=$(git -C "$WORKTREE_PATH" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||')
|
|
53
|
+
[ -z "$DEFAULT_BRANCH" ] && DEFAULT_BRANCH="main"
|
|
54
|
+
|
|
55
|
+
# Count unmerged commits
|
|
56
|
+
UNMERGED=$(git -C "$WORKTREE_PATH" log --oneline "$DEFAULT_BRANCH..$BRANCH" 2>/dev/null | wc -l)
|
|
57
|
+
|
|
58
|
+
if [ "$UNMERGED" -gt 0 ]; then
|
|
59
|
+
echo "BLOCKED: Worktree branch '$BRANCH' has $UNMERGED unmerged commit(s)" >&2
|
|
60
|
+
echo "Merge or push the branch before removing the worktree:" >&2
|
|
61
|
+
echo " git -C $WORKTREE_PATH push origin $BRANCH" >&2
|
|
62
|
+
echo " # or: git merge $BRANCH" >&2
|
|
63
|
+
exit 2
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Check for unpushed commits
|
|
67
|
+
UNPUSHED=$(git -C "$WORKTREE_PATH" log --oneline "origin/$BRANCH..$BRANCH" 2>/dev/null | wc -l)
|
|
68
|
+
|
|
69
|
+
if [ "$UNPUSHED" -gt 0 ]; then
|
|
70
|
+
echo "BLOCKED: Worktree branch '$BRANCH' has $UNPUSHED unpushed commit(s)" >&2
|
|
71
|
+
echo "Push before removing: git -C $WORKTREE_PATH push origin $BRANCH" >&2
|
|
72
|
+
exit 2
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
exit 0
|
package/package.json
CHANGED