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,52 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for quota-reset-cycle-monitor.sh
|
|
3
|
+
HOOK="examples/quota-reset-cycle-monitor.sh"
|
|
4
|
+
PASS=0 FAIL=0
|
|
5
|
+
|
|
6
|
+
assert_contains() { if echo "$2" | grep -q "$3"; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1 (expected '$3')"; fi; }
|
|
7
|
+
assert_not_contains() { if ! echo "$2" | grep -q "$3"; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1 (unexpected '$3')"; fi; }
|
|
8
|
+
assert_exit() { if [ "$2" -eq "$3" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1 (exit $2, expected $3)"; fi; }
|
|
9
|
+
|
|
10
|
+
HISTORY="/tmp/cc-quota-reset-history"
|
|
11
|
+
rm -f "$HISTORY"
|
|
12
|
+
|
|
13
|
+
# Test 1: First run creates history
|
|
14
|
+
OUT=$(bash "$HOOK" 2>&1)
|
|
15
|
+
RC=$?
|
|
16
|
+
assert_exit "exit 0" "$RC" 0
|
|
17
|
+
if [ -f "$HISTORY" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: history not created"; fi
|
|
18
|
+
|
|
19
|
+
# Test 2: History contains today's date
|
|
20
|
+
TODAY=$(date +%Y-%m-%d)
|
|
21
|
+
CONTENT=$(cat "$HISTORY")
|
|
22
|
+
assert_contains "has today's date" "$CONTENT" "$TODAY"
|
|
23
|
+
|
|
24
|
+
# Test 3: Second run same day — no duplicate entry
|
|
25
|
+
bash "$HOOK" 2>/dev/null
|
|
26
|
+
LINES=$(wc -l < "$HISTORY")
|
|
27
|
+
if [ "$LINES" -eq 1 ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: duplicate entry ($LINES lines)"; fi
|
|
28
|
+
|
|
29
|
+
# Test 4: History format is correct (date|weekday)
|
|
30
|
+
WEEKDAY=$(date +%u)
|
|
31
|
+
assert_contains "correct format" "$CONTENT" "$TODAY|$WEEKDAY"
|
|
32
|
+
|
|
33
|
+
# Test 5: With 7+ entries, should give info message
|
|
34
|
+
rm -f "$HISTORY"
|
|
35
|
+
for i in $(seq 1 7); do
|
|
36
|
+
echo "2026-04-$(printf '%02d' $((i+10)))|$i" >> "$HISTORY"
|
|
37
|
+
done
|
|
38
|
+
# Remove today's entry so the hook will run
|
|
39
|
+
sed -i "/$TODAY/d" "$HISTORY"
|
|
40
|
+
OUT=$(bash "$HOOK" 2>&1)
|
|
41
|
+
assert_contains "7+ entries shows info" "$OUT" "tracking"
|
|
42
|
+
assert_contains "references issue" "$OUT" "#49599"
|
|
43
|
+
|
|
44
|
+
# Test 6: Exit code always 0
|
|
45
|
+
RC=$?
|
|
46
|
+
assert_exit "always exit 0" "$RC" 0
|
|
47
|
+
|
|
48
|
+
# Cleanup
|
|
49
|
+
rm -f "$HISTORY"
|
|
50
|
+
|
|
51
|
+
echo "quota-reset-cycle-monitor: $PASS passed, $FAIL failed"
|
|
52
|
+
exit $FAIL
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for shell-config-truncation-guard.sh
|
|
3
|
+
# Run: bash tests/test-shell-config-truncation-guard.sh
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
PASS=0
|
|
7
|
+
FAIL=0
|
|
8
|
+
HOOK="$(dirname "$0")/../examples/shell-config-truncation-guard.sh"
|
|
9
|
+
|
|
10
|
+
# Create temp dir for test files
|
|
11
|
+
TEST_DIR=$(mktemp -d)
|
|
12
|
+
trap 'rm -rf "$TEST_DIR"' EXIT
|
|
13
|
+
|
|
14
|
+
# Create a fake home with shell config files
|
|
15
|
+
export HOME="$TEST_DIR"
|
|
16
|
+
echo '# My bash profile
|
|
17
|
+
export PATH="$HOME/bin:$PATH"
|
|
18
|
+
export EDITOR=vim
|
|
19
|
+
alias ll="ls -la"
|
|
20
|
+
eval "$(pyenv init -)"
|
|
21
|
+
source ~/.bash_completion
|
|
22
|
+
' > "$TEST_DIR/.bash_profile"
|
|
23
|
+
|
|
24
|
+
echo '# My zshrc
|
|
25
|
+
export PATH="$HOME/bin:$PATH"
|
|
26
|
+
autoload -Uz compinit
|
|
27
|
+
compinit
|
|
28
|
+
source /usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh
|
|
29
|
+
' > "$TEST_DIR/.zshrc"
|
|
30
|
+
|
|
31
|
+
echo '# My bashrc
|
|
32
|
+
if [ -f /etc/bashrc ]; then . /etc/bashrc; fi
|
|
33
|
+
export PATH="$HOME/bin:$PATH"
|
|
34
|
+
' > "$TEST_DIR/.bashrc"
|
|
35
|
+
|
|
36
|
+
test_hook() {
|
|
37
|
+
local input="$1" expected_exit="$2" desc="$3"
|
|
38
|
+
local actual_exit=0
|
|
39
|
+
echo "$input" | bash "$HOOK" > /dev/null 2>/dev/null || actual_exit=$?
|
|
40
|
+
if [ "$actual_exit" -eq "$expected_exit" ]; then
|
|
41
|
+
echo " PASS: $desc"
|
|
42
|
+
PASS=$((PASS + 1))
|
|
43
|
+
else
|
|
44
|
+
echo " FAIL: $desc (expected exit $expected_exit, got $actual_exit)"
|
|
45
|
+
FAIL=$((FAIL + 1))
|
|
46
|
+
fi
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
echo "shell-config-truncation-guard.sh tests"
|
|
50
|
+
echo ""
|
|
51
|
+
|
|
52
|
+
# --- Block: Write tool truncating to empty ---
|
|
53
|
+
echo "= Write tool: truncation blocking ="
|
|
54
|
+
test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$TEST_DIR/.bash_profile\",\"content\":\"\"}}" 2 "Block empty Write to .bash_profile"
|
|
55
|
+
test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$TEST_DIR/.zshrc\",\"content\":\"\"}}" 2 "Block empty Write to .zshrc"
|
|
56
|
+
test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$TEST_DIR/.bashrc\",\"content\":\"\"}}" 2 "Block empty Write to .bashrc"
|
|
57
|
+
|
|
58
|
+
# --- Block: Write tool with near-empty content ---
|
|
59
|
+
test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$TEST_DIR/.bash_profile\",\"content\":\"#\n\"}}" 2 "Block near-empty Write to .bash_profile"
|
|
60
|
+
|
|
61
|
+
# --- Block: Write tool with >60% size reduction ---
|
|
62
|
+
test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$TEST_DIR/.bash_profile\",\"content\":\"export PATH\"}}" 2 "Block >60% reduction of .bash_profile"
|
|
63
|
+
|
|
64
|
+
# --- Allow: Write tool with reasonable content ---
|
|
65
|
+
test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$TEST_DIR/.bash_profile\",\"content\":\"# Updated bash profile\nexport PATH=\\\"$TEST_DIR/bin:\$PATH\\\"\nexport EDITOR=vim\nalias ll=\\\"ls -la\\\"\neval \\\"\$(pyenv init -)\\\"\nsource ~/.bash_completion\n# Added new alias\nalias gs=\\\"git status\\\"\n\"}}" 0 "Allow reasonable Write to .bash_profile"
|
|
66
|
+
|
|
67
|
+
# --- Allow: Write to unprotected files ---
|
|
68
|
+
test_hook '{"tool_name":"Write","tool_input":{"file_path":"/tmp/test.txt","content":""}}' 0 "Allow empty Write to unprotected file"
|
|
69
|
+
test_hook '{"tool_name":"Write","tool_input":{"file_path":"/tmp/config.sh","content":"echo hello"}}' 0 "Allow Write to unprotected config"
|
|
70
|
+
|
|
71
|
+
# --- Block: Bash truncation commands ---
|
|
72
|
+
echo ""
|
|
73
|
+
echo "= Bash tool: truncation blocking ="
|
|
74
|
+
test_hook "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"> $TEST_DIR/.bashrc\"}}" 2 "Block > redirect truncation of .bashrc"
|
|
75
|
+
test_hook "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"truncate -s 0 $TEST_DIR/.zshrc\"}}" 2 "Block truncate command on .zshrc"
|
|
76
|
+
test_hook "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\": > $TEST_DIR/.bash_profile\"}}" 2 "Block : > truncation of .bash_profile"
|
|
77
|
+
|
|
78
|
+
# --- Allow: Bash reading shell config ---
|
|
79
|
+
test_hook "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"cat $TEST_DIR/.bashrc\"}}" 0 "Allow cat .bashrc"
|
|
80
|
+
test_hook "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"grep PATH $TEST_DIR/.zshrc\"}}" 0 "Allow grep .zshrc"
|
|
81
|
+
|
|
82
|
+
# --- Allow: Bash with unrelated commands ---
|
|
83
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"echo hello"}}' 0 "Allow echo"
|
|
84
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"ls -la"}}' 0 "Allow ls"
|
|
85
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"git status"}}' 0 "Allow git status"
|
|
86
|
+
|
|
87
|
+
# --- Allow: Non-matching tools ---
|
|
88
|
+
echo ""
|
|
89
|
+
echo "= Non-matching tools ="
|
|
90
|
+
test_hook '{"tool_name":"Read","tool_input":{"file_path":"/tmp/test"}}' 0 "Allow Read tool"
|
|
91
|
+
test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test"}}' 0 "Allow Edit tool"
|
|
92
|
+
|
|
93
|
+
# --- Edge cases ---
|
|
94
|
+
echo ""
|
|
95
|
+
echo "= Edge cases ="
|
|
96
|
+
test_hook '{}' 0 "Handle empty input"
|
|
97
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":""}}' 0 "Handle empty command"
|
|
98
|
+
|
|
99
|
+
# Write to a file that doesn't exist yet (should allow)
|
|
100
|
+
test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$TEST_DIR/.new_profile\",\"content\":\"\"}}" 0 "Allow Write to non-existent profile"
|
|
101
|
+
|
|
102
|
+
echo ""
|
|
103
|
+
echo "Results: $PASS passed, $FAIL failed"
|
|
104
|
+
[ "$FAIL" -eq 0 ] && echo "ALL TESTS PASSED" || exit 1
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for subagent-spawn-rate-monitor.sh
|
|
3
|
+
HOOK="examples/subagent-spawn-rate-monitor.sh"
|
|
4
|
+
PASS=0 FAIL=0
|
|
5
|
+
|
|
6
|
+
assert_contains() { if echo "$2" | grep -q "$3"; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1 (expected '$3')"; fi; }
|
|
7
|
+
assert_not_contains() { if ! echo "$2" | grep -q "$3"; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1 (unexpected '$3')"; fi; }
|
|
8
|
+
|
|
9
|
+
# Cleanup state
|
|
10
|
+
rm -f /tmp/cc-subagent-spawn-counter /tmp/cc-subagent-spawn-window
|
|
11
|
+
|
|
12
|
+
# Test 1: First spawn should not warn
|
|
13
|
+
OUT=$(echo '{}' | bash "$HOOK" 2>&1)
|
|
14
|
+
assert_not_contains "first spawn no warn" "$OUT" "HIGH SUBAGENT"
|
|
15
|
+
|
|
16
|
+
# Test 2-5: Spawns 2-5 should not warn
|
|
17
|
+
for i in 2 3 4 5; do
|
|
18
|
+
OUT=$(echo '{}' | bash "$HOOK" 2>&1)
|
|
19
|
+
assert_not_contains "spawn $i no warn" "$OUT" "HIGH SUBAGENT"
|
|
20
|
+
done
|
|
21
|
+
|
|
22
|
+
# Test 6: 6th spawn (>5 threshold) should warn
|
|
23
|
+
OUT=$(echo '{}' | bash "$HOOK" 2>&1)
|
|
24
|
+
assert_contains "6th spawn should warn" "$OUT" "HIGH SUBAGENT"
|
|
25
|
+
assert_contains "should mention token cost" "$OUT" "4.7K"
|
|
26
|
+
assert_contains "should reference issue" "$OUT" "#50213"
|
|
27
|
+
|
|
28
|
+
# Test 7: Reset counter, verify no warning after reset
|
|
29
|
+
rm -f /tmp/cc-subagent-spawn-counter /tmp/cc-subagent-spawn-window
|
|
30
|
+
OUT=$(echo '{}' | bash "$HOOK" 2>&1)
|
|
31
|
+
assert_not_contains "after reset no warn" "$OUT" "HIGH SUBAGENT"
|
|
32
|
+
|
|
33
|
+
# Test 8: Window expiry reset
|
|
34
|
+
echo "1" > /tmp/cc-subagent-spawn-counter
|
|
35
|
+
echo "$(($(date +%s) - 400))" > /tmp/cc-subagent-spawn-window
|
|
36
|
+
OUT=$(echo '{}' | bash "$HOOK" 2>&1)
|
|
37
|
+
assert_not_contains "expired window resets count" "$OUT" "HIGH SUBAGENT"
|
|
38
|
+
|
|
39
|
+
# Cleanup
|
|
40
|
+
rm -f /tmp/cc-subagent-spawn-counter /tmp/cc-subagent-spawn-window
|
|
41
|
+
|
|
42
|
+
echo "Results: $PASS passed, $FAIL failed"
|
|
43
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for system-dir-protection-guard.sh
|
|
3
|
+
# Run: bash tests/test-system-dir-protection-guard.sh
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
PASS=0
|
|
7
|
+
FAIL=0
|
|
8
|
+
HOOK="$(dirname "$0")/../examples/system-dir-protection-guard.sh"
|
|
9
|
+
|
|
10
|
+
test_hook() {
|
|
11
|
+
local input="$1" expected_exit="$2" desc="$3"
|
|
12
|
+
local actual_exit=0
|
|
13
|
+
echo "$input" | bash "$HOOK" > /dev/null 2>/dev/null || actual_exit=$?
|
|
14
|
+
if [ "$actual_exit" -eq "$expected_exit" ]; then
|
|
15
|
+
echo " PASS: $desc"
|
|
16
|
+
PASS=$((PASS + 1))
|
|
17
|
+
else
|
|
18
|
+
echo " FAIL: $desc (expected exit $expected_exit, got $actual_exit)"
|
|
19
|
+
FAIL=$((FAIL + 1))
|
|
20
|
+
fi
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
echo "system-dir-protection-guard.sh tests"
|
|
24
|
+
echo ""
|
|
25
|
+
|
|
26
|
+
# --- Block: rm on system directories ---
|
|
27
|
+
test_hook '{"tool_input":{"command":"rm -rf /etc"}}' 2 "Block rm -rf /etc"
|
|
28
|
+
test_hook '{"tool_input":{"command":"rm -rf /usr"}}' 2 "Block rm -rf /usr"
|
|
29
|
+
test_hook '{"tool_input":{"command":"rm -rf /var"}}' 2 "Block rm -rf /var"
|
|
30
|
+
test_hook '{"tool_input":{"command":"rm -rf /opt"}}' 2 "Block rm -rf /opt"
|
|
31
|
+
test_hook '{"tool_input":{"command":"rm -rf /boot"}}' 2 "Block rm -rf /boot"
|
|
32
|
+
test_hook '{"tool_input":{"command":"rm -rf /root"}}' 2 "Block rm -rf /root"
|
|
33
|
+
test_hook '{"tool_input":{"command":"rm -rf /srv"}}' 2 "Block rm -rf /srv"
|
|
34
|
+
test_hook '{"tool_input":{"command":"rm -rf /home"}}' 2 "Block rm -rf /home"
|
|
35
|
+
test_hook '{"tool_input":{"command":"rm -rf /home/username"}}' 2 "Block rm -rf /home/username"
|
|
36
|
+
test_hook '{"tool_input":{"command":"sudo rm -rf /etc/nginx"}}' 2 "Block sudo rm -rf /etc/nginx"
|
|
37
|
+
test_hook '{"tool_input":{"command":"rm -rf /usr/local"}}' 2 "Block rm -rf /usr/local"
|
|
38
|
+
|
|
39
|
+
# --- Block: rm on critical home directories ---
|
|
40
|
+
test_hook "{\"tool_input\":{\"command\":\"rm -rf $HOME/.ssh\"}}" 2 "Block rm -rf ~/.ssh"
|
|
41
|
+
test_hook "{\"tool_input\":{\"command\":\"rm -rf $HOME/.config\"}}" 2 "Block rm -rf ~/.config"
|
|
42
|
+
test_hook "{\"tool_input\":{\"command\":\"rm -rf $HOME/.local\"}}" 2 "Block rm -rf ~/.local"
|
|
43
|
+
test_hook "{\"tool_input\":{\"command\":\"rm -rf $HOME/.gnupg\"}}" 2 "Block rm -rf ~/.gnupg"
|
|
44
|
+
|
|
45
|
+
# --- Block: mv on system directories (#49554) ---
|
|
46
|
+
test_hook '{"tool_input":{"command":"mv /etc /tmp/etc_backup"}}' 2 "Block mv /etc"
|
|
47
|
+
test_hook '{"tool_input":{"command":"mv /usr/local /tmp/"}}' 2 "Block mv /usr/local"
|
|
48
|
+
test_hook '{"tool_input":{"command":"mv /home/user /tmp/"}}' 2 "Block mv /home/user"
|
|
49
|
+
test_hook '{"tool_input":{"command":"sudo mv /var/lib /tmp/"}}' 2 "Block sudo mv /var/lib"
|
|
50
|
+
test_hook "{\"tool_input\":{\"command\":\"mv $HOME/.ssh /tmp/\"}}" 2 "Block mv ~/.ssh"
|
|
51
|
+
|
|
52
|
+
# --- Block: chmod -R on system directories ---
|
|
53
|
+
test_hook '{"tool_input":{"command":"chmod -R 777 /etc"}}' 2 "Block chmod -R 777 /etc"
|
|
54
|
+
test_hook '{"tool_input":{"command":"sudo chmod -R 755 /usr"}}' 2 "Block sudo chmod -R /usr"
|
|
55
|
+
|
|
56
|
+
# --- Block: chown -R on system directories ---
|
|
57
|
+
test_hook '{"tool_input":{"command":"chown -R user:user /var"}}' 2 "Block chown -R /var"
|
|
58
|
+
test_hook '{"tool_input":{"command":"sudo chown -R root:root /opt"}}' 2 "Block sudo chown -R /opt"
|
|
59
|
+
|
|
60
|
+
# --- Allow: Safe operations ---
|
|
61
|
+
test_hook '{"tool_input":{"command":"rm -rf /tmp/junk"}}' 0 "Allow rm -rf /tmp/junk"
|
|
62
|
+
test_hook '{"tool_input":{"command":"rm node_modules"}}' 0 "Allow rm node_modules"
|
|
63
|
+
test_hook '{"tool_input":{"command":"rm /home/user/project/dist/bundle.js"}}' 0 "Allow rm specific file in project"
|
|
64
|
+
test_hook '{"tool_input":{"command":"ls /etc/hosts"}}' 0 "Allow read-only access to /etc"
|
|
65
|
+
test_hook '{"tool_input":{"command":"cat /usr/local/bin/script"}}' 0 "Allow cat on /usr"
|
|
66
|
+
test_hook '{"tool_input":{"command":"mv /tmp/a.txt /tmp/b.txt"}}' 0 "Allow mv in /tmp"
|
|
67
|
+
test_hook '{"tool_input":{"command":"chmod 644 myfile.txt"}}' 0 "Allow chmod on project file"
|
|
68
|
+
|
|
69
|
+
# --- Allow: Empty/missing input ---
|
|
70
|
+
test_hook '{}' 0 "Allow empty input"
|
|
71
|
+
test_hook '{"tool_input":{}}' 0 "Allow missing command"
|
|
72
|
+
test_hook '{"tool_input":{"command":""}}' 0 "Allow empty command"
|
|
73
|
+
|
|
74
|
+
# --- Allow: Non-destructive system commands ---
|
|
75
|
+
test_hook '{"tool_input":{"command":"grep -r pattern /etc/nginx/"}}' 0 "Allow grep in /etc"
|
|
76
|
+
test_hook '{"tool_input":{"command":"find /usr -name \"*.so\""}}' 0 "Allow find in /usr (no delete)"
|
|
77
|
+
test_hook '{"tool_input":{"command":"cp /etc/hosts /tmp/"}}' 0 "Allow cp from /etc"
|
|
78
|
+
|
|
79
|
+
echo ""
|
|
80
|
+
echo "Results: $PASS passed, $FAIL failed out of $((PASS + FAIL)) tests"
|
|
81
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for tool-retry-budget-guard.sh
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
PASS=0
|
|
6
|
+
FAIL=0
|
|
7
|
+
HOOK="$(dirname "$0")/../examples/tool-retry-budget-guard.sh"
|
|
8
|
+
|
|
9
|
+
# Clean state before tests
|
|
10
|
+
rm -rf /tmp/.cc-retry-budget
|
|
11
|
+
|
|
12
|
+
test_hook() {
|
|
13
|
+
local input="$1" expected_exit="$2" desc="$3"
|
|
14
|
+
local actual_exit=0
|
|
15
|
+
echo "$input" | bash "$HOOK" > /dev/null 2>/dev/null || actual_exit=$?
|
|
16
|
+
if [ "$actual_exit" -eq "$expected_exit" ]; then
|
|
17
|
+
echo " PASS: $desc"
|
|
18
|
+
PASS=$((PASS + 1))
|
|
19
|
+
else
|
|
20
|
+
echo " FAIL: $desc (expected exit $expected_exit, got $actual_exit)"
|
|
21
|
+
FAIL=$((FAIL + 1))
|
|
22
|
+
fi
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
echo "tool-retry-budget-guard.sh tests"
|
|
26
|
+
echo ""
|
|
27
|
+
|
|
28
|
+
# Clean state
|
|
29
|
+
rm -rf /tmp/.cc-retry-budget
|
|
30
|
+
|
|
31
|
+
# --- Allow: First few edits ---
|
|
32
|
+
test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-a.txt"}}' 0 "Allow 1st edit to file A"
|
|
33
|
+
test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-a.txt"}}' 0 "Allow 2nd edit to file A"
|
|
34
|
+
test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-a.txt"}}' 0 "Allow 3rd edit to file A"
|
|
35
|
+
test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-a.txt"}}' 0 "Allow 4th edit to file A"
|
|
36
|
+
|
|
37
|
+
# --- Allow: Warning at 5th (exit 0 but with warning) ---
|
|
38
|
+
test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-a.txt"}}' 0 "Warn at 5th edit (still allow)"
|
|
39
|
+
test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-a.txt"}}' 0 "Warn at 6th edit (still allow)"
|
|
40
|
+
|
|
41
|
+
# --- Block: 7th attempt ---
|
|
42
|
+
test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-a.txt"}}' 2 "Block at 7th consecutive edit"
|
|
43
|
+
|
|
44
|
+
# --- After block, counter resets ---
|
|
45
|
+
rm -rf /tmp/.cc-retry-budget
|
|
46
|
+
test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-a.txt"}}' 0 "Allow after counter reset"
|
|
47
|
+
|
|
48
|
+
# --- Different files don't interfere ---
|
|
49
|
+
rm -rf /tmp/.cc-retry-budget
|
|
50
|
+
test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-b.txt"}}' 0 "Allow edit to file B"
|
|
51
|
+
test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-c.txt"}}' 0 "Allow edit to file C"
|
|
52
|
+
test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-b.txt"}}' 0 "Allow 2nd edit to file B"
|
|
53
|
+
|
|
54
|
+
# --- Write tool also tracked ---
|
|
55
|
+
rm -rf /tmp/.cc-retry-budget
|
|
56
|
+
for i in $(seq 1 6); do
|
|
57
|
+
test_hook '{"tool_name":"Write","tool_input":{"file_path":"/tmp/test-retry-w.txt"}}' 0 "Write attempt $i (allow)"
|
|
58
|
+
done
|
|
59
|
+
test_hook '{"tool_name":"Write","tool_input":{"file_path":"/tmp/test-retry-w.txt"}}' 2 "Block Write at 7th attempt"
|
|
60
|
+
|
|
61
|
+
# --- Non-Edit/Write tools pass through ---
|
|
62
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"ls"}}' 0 "Allow Bash (not tracked)"
|
|
63
|
+
test_hook '{"tool_name":"Read","tool_input":{"file_path":"/tmp/foo"}}' 0 "Allow Read (not tracked)"
|
|
64
|
+
test_hook '{"tool_name":"Grep","tool_input":{"pattern":"test"}}' 0 "Allow Grep (not tracked)"
|
|
65
|
+
|
|
66
|
+
# --- Empty input ---
|
|
67
|
+
test_hook '{}' 0 "Allow empty input"
|
|
68
|
+
test_hook '{"tool_name":"Edit","tool_input":{}}' 0 "Allow Edit with no file_path"
|
|
69
|
+
|
|
70
|
+
# Clean up
|
|
71
|
+
rm -rf /tmp/.cc-retry-budget
|
|
72
|
+
|
|
73
|
+
echo ""
|
|
74
|
+
echo "Results: $PASS passed, $FAIL failed"
|
|
75
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for worktree-branch-pollution-detector.sh
|
|
3
|
+
HOOK="examples/worktree-branch-pollution-detector.sh"
|
|
4
|
+
PASS=0 FAIL=0
|
|
5
|
+
|
|
6
|
+
assert_pass() { if [ $? -eq 0 ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1"; fi; }
|
|
7
|
+
assert_contains() { if echo "$2" | grep -q "$3"; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1 (expected '$3')"; fi; }
|
|
8
|
+
assert_not_contains() { if ! echo "$2" | grep -q "$3"; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1 (unexpected '$3')"; fi; }
|
|
9
|
+
|
|
10
|
+
# Setup temp repo
|
|
11
|
+
TMPDIR=$(mktemp -d)
|
|
12
|
+
cd "$TMPDIR"
|
|
13
|
+
git init -q
|
|
14
|
+
git commit --allow-empty -m "init" -q 2>/dev/null
|
|
15
|
+
|
|
16
|
+
# Clean up expected branch files
|
|
17
|
+
HASH=$(echo "$TMPDIR" | md5sum | cut -c1-8)
|
|
18
|
+
rm -f "/tmp/cc-expected-branch-$HASH"
|
|
19
|
+
|
|
20
|
+
# Test 1: First run records branch (no warning)
|
|
21
|
+
OUT=$(echo '{}' | bash "$OLDPWD/$HOOK" 2>&1)
|
|
22
|
+
assert_not_contains "first run should not warn" "$OUT" "BRANCH CHANGED"
|
|
23
|
+
assert_pass "first run exits 0"
|
|
24
|
+
|
|
25
|
+
# Test 2: Same branch (no warning)
|
|
26
|
+
OUT=$(echo '{}' | bash "$OLDPWD/$HOOK" 2>&1)
|
|
27
|
+
assert_not_contains "same branch should not warn" "$OUT" "BRANCH CHANGED"
|
|
28
|
+
|
|
29
|
+
# Test 3: Switch branch triggers warning
|
|
30
|
+
DEFAULT_BRANCH=$(git branch --show-current)
|
|
31
|
+
git checkout -b feature-x -q 2>/dev/null
|
|
32
|
+
OUT=$(echo '{}' | bash "$OLDPWD/$HOOK" 2>&1)
|
|
33
|
+
assert_contains "branch change should warn" "$OUT" "BRANCH CHANGED"
|
|
34
|
+
assert_contains "should mention expected branch" "$OUT" "$DEFAULT_BRANCH"
|
|
35
|
+
|
|
36
|
+
# Test 4: After warning, new branch is accepted
|
|
37
|
+
OUT=$(echo '{}' | bash "$OLDPWD/$HOOK" 2>&1)
|
|
38
|
+
assert_not_contains "after update should not warn" "$OUT" "BRANCH CHANGED"
|
|
39
|
+
|
|
40
|
+
# Test 5: Non-git directory exits silently
|
|
41
|
+
cd /tmp
|
|
42
|
+
OUT=$(echo '{}' | bash "$OLDPWD/$HOOK" 2>&1)
|
|
43
|
+
assert_not_contains "non-git should not warn" "$OUT" "BRANCH CHANGED"
|
|
44
|
+
|
|
45
|
+
# Cleanup
|
|
46
|
+
rm -rf "$TMPDIR"
|
|
47
|
+
rm -f "/tmp/cc-expected-branch-$HASH"
|
|
48
|
+
|
|
49
|
+
echo "Results: $PASS passed, $FAIL failed"
|
|
50
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
set -euo pipefail
|
|
2
|
+
PASS=0
|
|
3
|
+
FAIL=0
|
|
4
|
+
test_hook() {
|
|
5
|
+
local hook="$1" input="$2" expected_exit="$3" desc="$4"
|
|
6
|
+
local actual_exit=0
|
|
7
|
+
echo "$input" | bash "$hook" > /dev/null 2>/dev/null || actual_exit=$?
|
|
8
|
+
if [ "$actual_exit" -eq "$expected_exit" ]; then
|
|
9
|
+
echo " PASS: $desc"
|
|
10
|
+
PASS=$((PASS + 1))
|
|
11
|
+
else
|
|
12
|
+
echo " FAIL: $desc (expected exit $expected_exit, got $actual_exit)"
|
|
13
|
+
FAIL=$((FAIL + 1))
|
|
14
|
+
fi
|
|
15
|
+
}
|
|
16
|
+
HOOK_CREATE="$(dirname "$0")/../examples/worktree-create-log.sh"
|
|
17
|
+
HOOK_REMOVE="$(dirname "$0")/../examples/worktree-remove-uncommitted-guard.sh"
|
|
18
|
+
echo "=== worktree-create-log.sh ==="
|
|
19
|
+
test_hook "$HOOK_CREATE" '{"branch":"feature/test","path":"/tmp/wt-test"}' 0 "logs creation and passes"
|
|
20
|
+
test_hook "$HOOK_CREATE" '{}' 0 "empty input passes"
|
|
21
|
+
test_hook "$HOOK_CREATE" '' 0 "blank input passes"
|
|
22
|
+
echo ""
|
|
23
|
+
echo "=== worktree-remove-uncommitted-guard.sh ==="
|
|
24
|
+
test_hook "$HOOK_REMOVE" '{"path":"/nonexistent/path"}' 0 "nonexistent path passes"
|
|
25
|
+
test_hook "$HOOK_REMOVE" '{}' 0 "empty input passes"
|
|
26
|
+
test_hook "$HOOK_REMOVE" '' 0 "blank input passes"
|
|
27
|
+
echo ""
|
|
28
|
+
echo "Results: $PASS passed, $FAIL failed"
|
|
29
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|