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,65 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for dotenv-read-guard.sh
|
|
3
|
+
HOOK="$(dirname "$0")/../examples/dotenv-read-guard.sh"
|
|
4
|
+
PASS=0; FAIL=0
|
|
5
|
+
|
|
6
|
+
run_test() {
|
|
7
|
+
local desc="$1" input="$2" expect="$3"
|
|
8
|
+
result=$(echo "$input" | bash "$HOOK" 2>/dev/null; echo $?)
|
|
9
|
+
code=$(echo "$result" | tail -1)
|
|
10
|
+
if [ "$code" = "$expect" ]; then
|
|
11
|
+
echo "PASS: $desc"
|
|
12
|
+
((PASS++))
|
|
13
|
+
else
|
|
14
|
+
echo "FAIL: $desc (expected $expect, got $code)"
|
|
15
|
+
((FAIL++))
|
|
16
|
+
fi
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Should block .env files
|
|
20
|
+
run_test "Block .env" \
|
|
21
|
+
'{"tool_input":{"file_path":"/home/user/project/.env"}}' "2"
|
|
22
|
+
|
|
23
|
+
run_test "Block .env.local" \
|
|
24
|
+
'{"tool_input":{"file_path":"/app/.env.local"}}' "2"
|
|
25
|
+
|
|
26
|
+
run_test "Block .env.production" \
|
|
27
|
+
'{"tool_input":{"file_path":"/deploy/.env.production"}}' "2"
|
|
28
|
+
|
|
29
|
+
run_test "Block .env.staging" \
|
|
30
|
+
'{"tool_input":{"file_path":"/app/.env.staging"}}' "2"
|
|
31
|
+
|
|
32
|
+
run_test "Block .env.development" \
|
|
33
|
+
'{"tool_input":{"file_path":"/app/.env.development"}}' "2"
|
|
34
|
+
|
|
35
|
+
run_test "Block .env.test" \
|
|
36
|
+
'{"tool_input":{"file_path":"/project/.env.test"}}' "2"
|
|
37
|
+
|
|
38
|
+
# Should allow non-.env files
|
|
39
|
+
run_test "Allow .env.example" \
|
|
40
|
+
'{"tool_input":{"file_path":"/project/.env.example"}}' "0"
|
|
41
|
+
|
|
42
|
+
run_test "Allow README.md" \
|
|
43
|
+
'{"tool_input":{"file_path":"/project/README.md"}}' "0"
|
|
44
|
+
|
|
45
|
+
run_test "Allow package.json" \
|
|
46
|
+
'{"tool_input":{"file_path":"/project/package.json"}}' "0"
|
|
47
|
+
|
|
48
|
+
run_test "Allow config.ts" \
|
|
49
|
+
'{"tool_input":{"file_path":"/src/config.ts"}}' "0"
|
|
50
|
+
|
|
51
|
+
run_test "Allow env.ts (not dotenv)" \
|
|
52
|
+
'{"tool_input":{"file_path":"/src/env.ts"}}' "0"
|
|
53
|
+
|
|
54
|
+
run_test "Allow .envrc (direnv)" \
|
|
55
|
+
'{"tool_input":{"file_path":"/project/.envrc"}}' "0"
|
|
56
|
+
|
|
57
|
+
# Edge cases
|
|
58
|
+
run_test "Empty input" '{}' "0"
|
|
59
|
+
|
|
60
|
+
run_test "No file_path" \
|
|
61
|
+
'{"tool_input":{}}' "0"
|
|
62
|
+
|
|
63
|
+
echo ""
|
|
64
|
+
echo "Results: $PASS passed, $FAIL failed"
|
|
65
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for auto-mode-safety-enforcer.sh
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
PASS=0
|
|
6
|
+
FAIL=0
|
|
7
|
+
HOOK="$(dirname "$0")/../examples/auto-mode-safety-enforcer.sh"
|
|
8
|
+
|
|
9
|
+
test_hook() {
|
|
10
|
+
local input="$1" expected_exit="$2" desc="$3"
|
|
11
|
+
local actual_exit=0
|
|
12
|
+
echo "$input" | bash "$HOOK" > /dev/null 2>/dev/null || actual_exit=$?
|
|
13
|
+
if [ "$actual_exit" -eq "$expected_exit" ]; then
|
|
14
|
+
echo " PASS: $desc"
|
|
15
|
+
PASS=$((PASS + 1))
|
|
16
|
+
else
|
|
17
|
+
echo " FAIL: $desc (expected exit $expected_exit, got $actual_exit)"
|
|
18
|
+
FAIL=$((FAIL + 1))
|
|
19
|
+
fi
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
echo "auto-mode-safety-enforcer.sh tests"
|
|
23
|
+
echo ""
|
|
24
|
+
|
|
25
|
+
# --- Block: Critical rm operations ---
|
|
26
|
+
test_hook '{"tool_input":{"command":"rm -rf /"}}' 2 "Block rm -rf /"
|
|
27
|
+
test_hook '{"tool_input":{"command":"rm -rf ~"}}' 2 "Block rm -rf ~"
|
|
28
|
+
test_hook '{"tool_input":{"command":"rm -rf ~/."}}' 2 "Block rm -rf ~/."
|
|
29
|
+
test_hook '{"tool_input":{"command":"sudo rm -rf /home"}}' 2 "Block sudo rm -rf /home"
|
|
30
|
+
test_hook '{"tool_input":{"command":"rm -rf /etc"}}' 2 "Block rm -rf /etc"
|
|
31
|
+
test_hook '{"tool_input":{"command":"rm -rf /usr"}}' 2 "Block rm -rf /usr"
|
|
32
|
+
test_hook "{\"tool_input\":{\"command\":\"rm -rf $HOME/.ssh\"}}" 2 "Block rm -rf ~/.ssh"
|
|
33
|
+
test_hook "{\"tool_input\":{\"command\":\"rm $HOME/.git-credentials\"}}" 2 "Block rm ~/.git-credentials"
|
|
34
|
+
test_hook "{\"tool_input\":{\"command\":\"rm -f $HOME/.bashrc\"}}" 2 "Block rm ~/.bashrc"
|
|
35
|
+
|
|
36
|
+
# --- Block: Disk operations ---
|
|
37
|
+
test_hook '{"tool_input":{"command":"sudo dd if=/dev/zero of=/dev/sda"}}' 2 "Block dd to disk"
|
|
38
|
+
test_hook '{"tool_input":{"command":"sudo mkfs.ext4 /dev/sda1"}}' 2 "Block mkfs"
|
|
39
|
+
test_hook '{"tool_input":{"command":"sudo fdisk /dev/sda"}}' 2 "Block fdisk"
|
|
40
|
+
|
|
41
|
+
# --- Block: System process kill ---
|
|
42
|
+
test_hook '{"tool_input":{"command":"kill -9 1"}}' 2 "Block kill PID 1"
|
|
43
|
+
test_hook '{"tool_input":{"command":"killall systemd"}}' 2 "Block killall systemd"
|
|
44
|
+
|
|
45
|
+
# --- Allow: Safe operations ---
|
|
46
|
+
test_hook '{"tool_input":{"command":"rm -rf node_modules"}}' 0 "Allow rm node_modules"
|
|
47
|
+
test_hook '{"tool_input":{"command":"rm /tmp/test.txt"}}' 0 "Allow rm in /tmp"
|
|
48
|
+
test_hook '{"tool_input":{"command":"ls -la"}}' 0 "Allow ls"
|
|
49
|
+
test_hook '{"tool_input":{"command":"git status"}}' 0 "Allow git"
|
|
50
|
+
test_hook '{"tool_input":{"command":"npm install"}}' 0 "Allow npm install"
|
|
51
|
+
test_hook '{}' 0 "Allow empty input"
|
|
52
|
+
|
|
53
|
+
echo ""
|
|
54
|
+
echo "Results: $PASS passed, $FAIL failed out of $((PASS + FAIL)) tests"
|
|
55
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for case-insensitive-path-guard.sh
|
|
3
|
+
# Run: bash tests/test-case-insensitive-path-guard.sh
|
|
4
|
+
# NOTE: Full case-mismatch detection only works on macOS APFS.
|
|
5
|
+
# On Linux, the hook exits 0 for all inputs (no case-insensitive FS).
|
|
6
|
+
# These tests verify the non-macOS path (exit 0) and input parsing.
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
PASS=0
|
|
10
|
+
FAIL=0
|
|
11
|
+
HOOK="$(dirname "$0")/../examples/case-insensitive-path-guard.sh"
|
|
12
|
+
|
|
13
|
+
test_hook() {
|
|
14
|
+
local input="$1" expected_exit="$2" desc="$3"
|
|
15
|
+
local actual_exit=0
|
|
16
|
+
echo "$input" | bash "$HOOK" > /dev/null 2>/dev/null || actual_exit=$?
|
|
17
|
+
if [ "$actual_exit" -eq "$expected_exit" ]; then
|
|
18
|
+
echo " PASS: $desc"
|
|
19
|
+
PASS=$((PASS + 1))
|
|
20
|
+
else
|
|
21
|
+
echo " FAIL: $desc (expected exit $expected_exit, got $actual_exit)"
|
|
22
|
+
FAIL=$((FAIL + 1))
|
|
23
|
+
fi
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
echo "case-insensitive-path-guard.sh tests"
|
|
27
|
+
echo ""
|
|
28
|
+
|
|
29
|
+
# On Linux, all commands should pass through (exit 0)
|
|
30
|
+
# The hook only activates on macOS (uname == Darwin)
|
|
31
|
+
IS_LINUX=0
|
|
32
|
+
[ "$(uname)" != "Darwin" ] && IS_LINUX=1
|
|
33
|
+
|
|
34
|
+
if [ "$IS_LINUX" -eq 1 ]; then
|
|
35
|
+
echo "Running on Linux — all tests should pass through (exit 0)"
|
|
36
|
+
echo ""
|
|
37
|
+
|
|
38
|
+
# --- Pass-through on Linux ---
|
|
39
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"rm -rf ~/Projects"}}' 0 "Linux: rm -rf passes through"
|
|
40
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"rm -rf ~/Documents"}}' 0 "Linux: rm Documents passes through"
|
|
41
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"mv ~/old ~/new"}}' 0 "Linux: mv passes through"
|
|
42
|
+
|
|
43
|
+
# --- Non-destructive commands always pass ---
|
|
44
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"ls ~/Projects"}}' 0 "Linux: ls passes through"
|
|
45
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"git status"}}' 0 "Linux: git status passes through"
|
|
46
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"echo hello"}}' 0 "Linux: echo passes through"
|
|
47
|
+
|
|
48
|
+
# --- Safe paths always pass ---
|
|
49
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"rm -rf node_modules"}}' 0 "Linux: rm node_modules passes"
|
|
50
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"rm -rf /tmp/test"}}' 0 "Linux: rm /tmp passes"
|
|
51
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"rm -rf .cache"}}' 0 "Linux: rm .cache passes"
|
|
52
|
+
|
|
53
|
+
# --- Empty/missing inputs ---
|
|
54
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":""}}' 0 "Empty command"
|
|
55
|
+
test_hook '{"tool_name":"Bash","tool_input":{}}' 0 "No command"
|
|
56
|
+
test_hook '{}' 0 "Empty JSON"
|
|
57
|
+
|
|
58
|
+
else
|
|
59
|
+
echo "Running on macOS — testing case-mismatch detection"
|
|
60
|
+
echo ""
|
|
61
|
+
|
|
62
|
+
# On macOS, safe paths should still pass
|
|
63
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"rm -rf node_modules"}}' 0 "macOS: rm node_modules passes"
|
|
64
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"rm -rf /tmp/test"}}' 0 "macOS: rm /tmp passes"
|
|
65
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"rm -rf __pycache__"}}' 0 "macOS: rm __pycache__ passes"
|
|
66
|
+
|
|
67
|
+
# Non-destructive commands pass
|
|
68
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"ls ~/Projects"}}' 0 "macOS: ls passes through"
|
|
69
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"git status"}}' 0 "macOS: git status passes"
|
|
70
|
+
|
|
71
|
+
# Empty inputs
|
|
72
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":""}}' 0 "Empty command"
|
|
73
|
+
test_hook '{"tool_name":"Bash","tool_input":{}}' 0 "No command"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
echo ""
|
|
77
|
+
echo "Results: $PASS passed, $FAIL failed out of $((PASS + FAIL))"
|
|
78
|
+
[ "$FAIL" -eq 0 ] && echo "ALL TESTS PASSED" || exit 1
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for context-usage-drift-alert.sh
|
|
3
|
+
HOOK="examples/context-usage-drift-alert.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
|
+
# Use a test-specific counter file
|
|
10
|
+
COUNTER_FILE="/tmp/cc-context-usage-counter-$(date +%Y%m%d)"
|
|
11
|
+
rm -f "$COUNTER_FILE"
|
|
12
|
+
|
|
13
|
+
# Test 1: Calls 1-49 should not warn
|
|
14
|
+
for i in $(seq 1 49); do
|
|
15
|
+
echo '{}' | bash "$HOOK" 2>/dev/null
|
|
16
|
+
done
|
|
17
|
+
OUT=$(echo '{}' | bash "$HOOK" 2>&1)
|
|
18
|
+
# Call 50 should give checkpoint
|
|
19
|
+
assert_contains "call 50 should checkpoint" "$OUT" "checkpoint"
|
|
20
|
+
assert_contains "should mention /cost" "$OUT" "/cost"
|
|
21
|
+
|
|
22
|
+
# Test 2: Calls 51-99 should not warn
|
|
23
|
+
for i in $(seq 51 99); do
|
|
24
|
+
echo '{}' | bash "$HOOK" 2>/dev/null
|
|
25
|
+
done
|
|
26
|
+
OUT=$(echo '{}' | bash "$HOOK" 2>&1)
|
|
27
|
+
# Call 100 should give strong warning
|
|
28
|
+
assert_contains "call 100 should warn high" "$OUT" "HIGH CONTEXT"
|
|
29
|
+
assert_contains "should mention /compact" "$OUT" "/compact"
|
|
30
|
+
assert_contains "should reference issue" "$OUT" "#50204"
|
|
31
|
+
|
|
32
|
+
# Test 3: Calls 101-149
|
|
33
|
+
for i in $(seq 101 149); do
|
|
34
|
+
echo '{}' | bash "$HOOK" 2>/dev/null
|
|
35
|
+
done
|
|
36
|
+
OUT=$(echo '{}' | bash "$HOOK" 2>&1)
|
|
37
|
+
# Call 150 should give critical warning
|
|
38
|
+
assert_contains "call 150 critical warning" "$OUT" "VERY HIGH"
|
|
39
|
+
assert_contains "should mention saving state" "$OUT" "Save"
|
|
40
|
+
|
|
41
|
+
# Test 4: Normal calls between thresholds should be silent
|
|
42
|
+
rm -f "$COUNTER_FILE"
|
|
43
|
+
echo "10" > "$COUNTER_FILE"
|
|
44
|
+
OUT=$(echo '{}' | bash "$HOOK" 2>&1)
|
|
45
|
+
assert_not_contains "non-threshold call should be silent" "$OUT" "checkpoint"
|
|
46
|
+
assert_not_contains "non-threshold no warning" "$OUT" "HIGH"
|
|
47
|
+
|
|
48
|
+
# Cleanup
|
|
49
|
+
rm -f "$COUNTER_FILE"
|
|
50
|
+
|
|
51
|
+
echo "Results: $PASS passed, $FAIL failed"
|
|
52
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for dangerous-pip-flag-guard.sh
|
|
3
|
+
# Run: bash tests/test-dangerous-pip-flag-guard.sh
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
PASS=0
|
|
7
|
+
FAIL=0
|
|
8
|
+
HOOK="$(dirname "$0")/../examples/dangerous-pip-flag-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 "dangerous-pip-flag-guard.sh tests"
|
|
24
|
+
echo ""
|
|
25
|
+
|
|
26
|
+
# --- Block: --break-system-packages ---
|
|
27
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"pip install --break-system-packages requests"}}' 2 "Block --break-system-packages"
|
|
28
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"pip3 install --break-system-packages numpy"}}' 2 "Block pip3 --break-system-packages"
|
|
29
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"pip install requests --break-system-packages"}}' 2 "Block flag after package name"
|
|
30
|
+
|
|
31
|
+
# --- Block: sudo pip install ---
|
|
32
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"sudo pip install flask"}}' 2 "Block sudo pip install"
|
|
33
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"sudo pip3 install django"}}' 2 "Block sudo pip3 install"
|
|
34
|
+
|
|
35
|
+
# --- Block: targeting system directories ---
|
|
36
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"pip install --target=/usr/lib/python3/dist-packages requests"}}' 2 "Block install to /usr/lib"
|
|
37
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"pip install --target /opt/python/lib requests"}}' 2 "Block install to /opt"
|
|
38
|
+
|
|
39
|
+
# --- Allow: normal pip install ---
|
|
40
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"pip install requests"}}' 0 "Allow normal pip install"
|
|
41
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"pip install --user requests"}}' 0 "Allow --user install"
|
|
42
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"pip install -r requirements.txt"}}' 0 "Allow requirements.txt"
|
|
43
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"pip3 install flask==2.0"}}' 0 "Allow versioned install"
|
|
44
|
+
|
|
45
|
+
# --- Allow: non-pip commands ---
|
|
46
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"npm install express"}}' 0 "Allow npm install"
|
|
47
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"git status"}}' 0 "Allow git status"
|
|
48
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"echo hello"}}' 0 "Allow echo"
|
|
49
|
+
|
|
50
|
+
# --- Allow: empty/missing ---
|
|
51
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":""}}' 0 "Allow empty command"
|
|
52
|
+
test_hook '{"tool_name":"Bash","tool_input":{}}' 0 "Allow no command"
|
|
53
|
+
|
|
54
|
+
echo ""
|
|
55
|
+
echo "Results: $PASS passed, $FAIL failed out of $((PASS + FAIL))"
|
|
56
|
+
[ "$FAIL" -eq 0 ] && echo "ALL TESTS PASSED" || exit 1
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for dotfile-protection-guard.sh
|
|
3
|
+
# Run: bash tests/test-dotfile-protection-guard.sh
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
PASS=0
|
|
7
|
+
FAIL=0
|
|
8
|
+
HOOK="$(dirname "$0")/../examples/dotfile-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 "dotfile-protection-guard.sh tests"
|
|
24
|
+
echo ""
|
|
25
|
+
|
|
26
|
+
# --- Block: Shell config files ---
|
|
27
|
+
test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$HOME/.bashrc\"}}" 2 "Block Write to .bashrc"
|
|
28
|
+
test_hook "{\"tool_name\":\"Edit\",\"tool_input\":{\"file_path\":\"$HOME/.zshrc\"}}" 2 "Block Edit to .zshrc"
|
|
29
|
+
test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$HOME/.bash_profile\"}}" 2 "Block Write to .bash_profile"
|
|
30
|
+
test_hook "{\"tool_name\":\"Edit\",\"tool_input\":{\"file_path\":\"$HOME/.profile\"}}" 2 "Block Edit to .profile"
|
|
31
|
+
test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$HOME/.zshenv\"}}" 2 "Block Write to .zshenv"
|
|
32
|
+
|
|
33
|
+
# --- Block: SSH ---
|
|
34
|
+
test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$HOME/.ssh/id_rsa\"}}" 2 "Block Write to .ssh/id_rsa"
|
|
35
|
+
test_hook "{\"tool_name\":\"Edit\",\"tool_input\":{\"file_path\":\"$HOME/.ssh/config\"}}" 2 "Block Edit to .ssh/config"
|
|
36
|
+
test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$HOME/.ssh/authorized_keys\"}}" 2 "Block Write to .ssh/authorized_keys"
|
|
37
|
+
|
|
38
|
+
# --- Block: Git credentials ---
|
|
39
|
+
test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$HOME/.git-credentials\"}}" 2 "Block Write to .git-credentials"
|
|
40
|
+
test_hook "{\"tool_name\":\"Edit\",\"tool_input\":{\"file_path\":\"$HOME/.gitconfig\"}}" 2 "Block Edit to .gitconfig"
|
|
41
|
+
|
|
42
|
+
# --- Block: Other credentials ---
|
|
43
|
+
test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$HOME/.npmrc\"}}" 2 "Block Write to .npmrc"
|
|
44
|
+
test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$HOME/.aws/credentials\"}}" 2 "Block Write to .aws/credentials"
|
|
45
|
+
test_hook "{\"tool_name\":\"Edit\",\"tool_input\":{\"file_path\":\"$HOME/.config/gh/hosts.yml\"}}" 2 "Block Edit to gh hosts.yml"
|
|
46
|
+
test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$HOME/.netrc\"}}" 2 "Block Write to .netrc"
|
|
47
|
+
test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$HOME/.docker/config.json\"}}" 2 "Block Write to docker config"
|
|
48
|
+
test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$HOME/.kube/config\"}}" 2 "Block Write to kube config"
|
|
49
|
+
test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$HOME/.gnupg/trustdb.gpg\"}}" 2 "Block Write to gnupg"
|
|
50
|
+
|
|
51
|
+
# --- Allow: Claude Code config ---
|
|
52
|
+
test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$HOME/.claude/settings.json\"}}" 0 "Allow Write to .claude/settings.json"
|
|
53
|
+
test_hook "{\"tool_name\":\"Edit\",\"tool_input\":{\"file_path\":\"$HOME/.claude/CLAUDE.md\"}}" 0 "Allow Edit to .claude/CLAUDE.md"
|
|
54
|
+
|
|
55
|
+
# --- Allow: Project files ---
|
|
56
|
+
test_hook '{"tool_name":"Write","tool_input":{"file_path":"/home/user/project/src/main.py"}}' 0 "Allow Write to project file"
|
|
57
|
+
test_hook '{"tool_name":"Edit","tool_input":{"file_path":"./README.md"}}' 0 "Allow Edit to relative path"
|
|
58
|
+
|
|
59
|
+
# --- Allow: Empty input ---
|
|
60
|
+
test_hook '{}' 0 "Allow empty input"
|
|
61
|
+
test_hook '{"tool_name":"Write","tool_input":{}}' 0 "Allow missing file_path"
|
|
62
|
+
|
|
63
|
+
# --- Block: Tilde expansion ---
|
|
64
|
+
test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"~/.bashrc\"}}" 2 "Block Write to ~/.bashrc (tilde)"
|
|
65
|
+
|
|
66
|
+
echo ""
|
|
67
|
+
echo "Results: $PASS passed, $FAIL failed out of $((PASS + FAIL)) tests"
|
|
68
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for effort-tracking-logger.sh
|
|
3
|
+
HOOK="examples/effort-tracking-logger.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_exit() { if [ "$2" -eq "$3" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1 (exit $2, expected $3)"; fi; }
|
|
8
|
+
|
|
9
|
+
LOG_DIR="${HOME}/.claude/effort-log"
|
|
10
|
+
LOG_FILE="$LOG_DIR/$(date +%Y-%m-%d).jsonl"
|
|
11
|
+
rm -rf "$LOG_DIR"
|
|
12
|
+
|
|
13
|
+
# Test 1: Creates log directory
|
|
14
|
+
echo '{"tool_name":"Bash","was_error":"false"}' | bash "$HOOK" 2>&1
|
|
15
|
+
RC=$?
|
|
16
|
+
assert_exit "exit 0" "$RC" 0
|
|
17
|
+
if [ -d "$LOG_DIR" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: log dir not created"; fi
|
|
18
|
+
|
|
19
|
+
# Test 2: Log file created with valid JSONL
|
|
20
|
+
if [ -f "$LOG_FILE" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: log file not created"; fi
|
|
21
|
+
ENTRY=$(cat "$LOG_FILE")
|
|
22
|
+
assert_contains "has timestamp" "$ENTRY" "timestamp"
|
|
23
|
+
assert_contains "has tool name" "$ENTRY" "Bash"
|
|
24
|
+
assert_contains "has error field" "$ENTRY" "error"
|
|
25
|
+
|
|
26
|
+
# Test 3: Valid JSON
|
|
27
|
+
python3 -c "import json; json.loads(open('$LOG_FILE').read().strip())" 2>/dev/null
|
|
28
|
+
if [ $? -eq 0 ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: invalid JSON"; fi
|
|
29
|
+
|
|
30
|
+
# Test 4: Multiple entries appended
|
|
31
|
+
echo '{"tool_name":"Read","was_error":"false"}' | bash "$HOOK" 2>&1
|
|
32
|
+
echo '{"tool_name":"Edit","was_error":"true"}' | bash "$HOOK" 2>&1
|
|
33
|
+
LINES=$(wc -l < "$LOG_FILE")
|
|
34
|
+
if [ "$LINES" -eq 3 ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: expected 3 lines, got $LINES"; fi
|
|
35
|
+
|
|
36
|
+
# Test 5: Error field correctly parsed
|
|
37
|
+
LAST=$(tail -1 "$LOG_FILE")
|
|
38
|
+
assert_contains "error=true parsed" "$LAST" '"error": true'
|
|
39
|
+
|
|
40
|
+
# Test 6: Tool name correctly parsed
|
|
41
|
+
SECOND=$(sed -n '2p' "$LOG_FILE")
|
|
42
|
+
assert_contains "Read tool name" "$SECOND" '"tool": "Read"'
|
|
43
|
+
|
|
44
|
+
# Test 7: Unknown tool handled
|
|
45
|
+
echo '{}' | bash "$HOOK" 2>&1
|
|
46
|
+
RC=$?
|
|
47
|
+
assert_exit "unknown tool exit 0" "$RC" 0
|
|
48
|
+
LAST=$(tail -1 "$LOG_FILE")
|
|
49
|
+
assert_contains "unknown tool logged" "$LAST" "unknown"
|
|
50
|
+
|
|
51
|
+
# Cleanup
|
|
52
|
+
rm -rf "$LOG_DIR"
|
|
53
|
+
|
|
54
|
+
echo "effort-tracking-logger: $PASS passed, $FAIL failed"
|
|
55
|
+
exit $FAIL
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for financial-operation-guard.sh
|
|
3
|
+
# Run: bash tests/test-financial-operation-guard.sh
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
PASS=0
|
|
7
|
+
FAIL=0
|
|
8
|
+
HOOK="$(dirname "$0")/../examples/financial-operation-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 "financial-operation-guard.sh tests"
|
|
24
|
+
echo ""
|
|
25
|
+
|
|
26
|
+
# --- Block: Exchange API calls (#46828 pattern) ---
|
|
27
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"python3 -c \"ex.transfer(USDT, 1446.65, spot, swap)\" bitget"}}' 2 "Block Bitget fund transfer (#46828 exact pattern)"
|
|
28
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"curl https://api.binance.com/api/v3/order -X POST"}}' 2 "Block Binance order API"
|
|
29
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"python3 trade.py --exchange bybit --withdraw 500"}}' 2 "Block Bybit withdrawal"
|
|
30
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"ccxt kraken transfer USDT from spot to futures"}}' 2 "Block Kraken transfer via ccxt"
|
|
31
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"python3 -c \"coinbase.create_order(symbol, side, amount)\""}}' 2 "Block Coinbase order"
|
|
32
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"curl -X POST https://api.okx.com/api/v5/trade/order"}}' 2 "Block OKX trade order"
|
|
33
|
+
|
|
34
|
+
# --- Block: Generic crypto transfers ---
|
|
35
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"python3 -c \"transfer(USDT, 1000, wallet_a, wallet_b)\""}}' 2 "Block USDT transfer"
|
|
36
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"send_eth --to 0xabc --amount 5 --wallet main"}}' 2 "Block ETH send"
|
|
37
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"python3 withdraw_btc.py --balance 0.5"}}' 2 "Block BTC withdrawal"
|
|
38
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"python3 -c \"swap(usdc, 500, from_wallet)\""}}' 2 "Block USDC swap"
|
|
39
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"bridge sol from mainnet to polygon --funds 100"}}' 2 "Block SOL bridge"
|
|
40
|
+
|
|
41
|
+
# --- Block: Payment processor operations ---
|
|
42
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"curl -X POST https://api.stripe.com/v1/charges -d amount=5000"}}' 2 "Block Stripe charge"
|
|
43
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"python3 -c \"stripe.Transfer.create(amount=1000)\""}}' 2 "Block Stripe transfer"
|
|
44
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"paypal-cli send payment --to user@email.com --amount 200"}}' 2 "Block PayPal payment"
|
|
45
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"python3 -c \"square.payments.create_payment(body)\""}}' 2 "Block Square payment"
|
|
46
|
+
|
|
47
|
+
# --- Allow: Safe operations ---
|
|
48
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"curl https://api.example.com/data"}}' 0 "Allow normal API call"
|
|
49
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"python3 analyze_trades.py --read-only"}}' 0 "Allow read-only trade analysis"
|
|
50
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"cat balance.txt"}}' 0 "Allow reading balance file"
|
|
51
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"echo transfer complete"}}' 0 "Allow echo containing transfer"
|
|
52
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"git push origin main"}}' 0 "Allow git push"
|
|
53
|
+
test_hook '{"tool_name":"Bash","tool_input":{"command":"npm install stripe"}}' 0 "Allow npm install stripe (not an operation)"
|
|
54
|
+
test_hook '{}' 0 "Allow empty input"
|
|
55
|
+
test_hook '{"tool_name":"Bash","tool_input":{}}' 0 "Allow no command"
|
|
56
|
+
|
|
57
|
+
echo ""
|
|
58
|
+
echo "Results: $PASS passed, $FAIL failed out of $((PASS + FAIL)) tests"
|
|
59
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for home-critical-bash-guard.sh
|
|
3
|
+
# Run: bash tests/test-home-critical-bash-guard.sh
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
PASS=0
|
|
7
|
+
FAIL=0
|
|
8
|
+
HOOK="$(dirname "$0")/../examples/home-critical-bash-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 "home-critical-bash-guard.sh tests"
|
|
24
|
+
echo ""
|
|
25
|
+
|
|
26
|
+
# --- Block: rm on critical paths ---
|
|
27
|
+
test_hook "{\"tool_input\":{\"command\":\"rm -rf $HOME/.ssh\"}}" 2 "Block rm -rf ~/.ssh"
|
|
28
|
+
test_hook "{\"tool_input\":{\"command\":\"rm $HOME/.git-credentials\"}}" 2 "Block rm ~/.git-credentials"
|
|
29
|
+
test_hook "{\"tool_input\":{\"command\":\"rm -f $HOME/.bashrc\"}}" 2 "Block rm ~/.bashrc"
|
|
30
|
+
test_hook "{\"tool_input\":{\"command\":\"sudo rm $HOME/.zshrc\"}}" 2 "Block sudo rm ~/.zshrc"
|
|
31
|
+
test_hook "{\"tool_input\":{\"command\":\"rm $HOME/.npmrc\"}}" 2 "Block rm ~/.npmrc"
|
|
32
|
+
test_hook "{\"tool_input\":{\"command\":\"rm -rf $HOME/.gnupg\"}}" 2 "Block rm -rf ~/.gnupg"
|
|
33
|
+
test_hook "{\"tool_input\":{\"command\":\"rm $HOME/.aws/credentials\"}}" 2 "Block rm ~/.aws/credentials"
|
|
34
|
+
|
|
35
|
+
# --- Block: mv on critical paths ---
|
|
36
|
+
test_hook "{\"tool_input\":{\"command\":\"mv $HOME/.bashrc /tmp/\"}}" 2 "Block mv ~/.bashrc"
|
|
37
|
+
test_hook "{\"tool_input\":{\"command\":\"mv $HOME/.ssh/config /tmp/bak\"}}" 2 "Block mv ~/.ssh/config"
|
|
38
|
+
|
|
39
|
+
# --- Block: Truncation via redirect ---
|
|
40
|
+
test_hook "{\"tool_input\":{\"command\":\"> $HOME/.bashrc\"}}" 2 "Block > ~/.bashrc truncation"
|
|
41
|
+
test_hook "{\"tool_input\":{\"command\":\"echo '' > $HOME/.zshrc\"}}" 2 "Block echo > ~/.zshrc"
|
|
42
|
+
|
|
43
|
+
# --- Block: chmod 777 on critical files ---
|
|
44
|
+
test_hook "{\"tool_input\":{\"command\":\"chmod 777 $HOME/.ssh/id_rsa\"}}" 2 "Block chmod 777 on .ssh/id_rsa"
|
|
45
|
+
|
|
46
|
+
# --- Allow: Safe commands ---
|
|
47
|
+
test_hook '{"tool_input":{"command":"rm -rf node_modules"}}' 0 "Allow rm node_modules"
|
|
48
|
+
test_hook '{"tool_input":{"command":"rm /tmp/test.txt"}}' 0 "Allow rm in /tmp"
|
|
49
|
+
test_hook '{"tool_input":{"command":"ls -la ~/.ssh"}}' 0 "Allow ls on .ssh (read-only)"
|
|
50
|
+
test_hook '{"tool_input":{"command":"cat ~/.bashrc"}}' 0 "Allow cat on .bashrc (read-only)"
|
|
51
|
+
test_hook '{"tool_input":{"command":"git status"}}' 0 "Allow git commands"
|
|
52
|
+
|
|
53
|
+
# --- Allow: Empty input ---
|
|
54
|
+
test_hook '{}' 0 "Allow empty input"
|
|
55
|
+
test_hook '{"tool_input":{}}' 0 "Allow missing command"
|
|
56
|
+
|
|
57
|
+
echo ""
|
|
58
|
+
echo "Results: $PASS passed, $FAIL failed out of $((PASS + FAIL)) tests"
|
|
59
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for model-version-change-alert.sh
|
|
3
|
+
HOOK="examples/model-version-change-alert.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-model-version-history"
|
|
11
|
+
rm -f "$HISTORY"
|
|
12
|
+
|
|
13
|
+
# Test 1: First run (no history) — no alert
|
|
14
|
+
OUT=$(CLAUDE_MODEL="opus-4.7" bash "$HOOK" 2>&1)
|
|
15
|
+
RC=$?
|
|
16
|
+
assert_not_contains "first run should not alert" "$OUT" "MODEL CHANGED"
|
|
17
|
+
assert_exit "first run exit 0" "$RC" 0
|
|
18
|
+
|
|
19
|
+
# Test 2: Same model — no alert
|
|
20
|
+
OUT=$(CLAUDE_MODEL="opus-4.7" bash "$HOOK" 2>&1)
|
|
21
|
+
assert_not_contains "same model no alert" "$OUT" "MODEL CHANGED"
|
|
22
|
+
|
|
23
|
+
# Test 3: Model changed — should alert
|
|
24
|
+
OUT=$(CLAUDE_MODEL="opus-4.6" bash "$HOOK" 2>&1)
|
|
25
|
+
assert_contains "model change should alert" "$OUT" "MODEL CHANGED"
|
|
26
|
+
assert_contains "should show old model" "$OUT" "opus-4.7"
|
|
27
|
+
assert_contains "should show new model" "$OUT" "opus-4.6"
|
|
28
|
+
assert_contains "should reference issue" "$OUT" "#49689"
|
|
29
|
+
|
|
30
|
+
# Test 4: Exit code always 0
|
|
31
|
+
RC=$?
|
|
32
|
+
assert_exit "exit 0 on alert" "$RC" 0
|
|
33
|
+
|
|
34
|
+
# Test 5: Unknown model — no update, no alert
|
|
35
|
+
echo "opus-4.7" > "$HISTORY"
|
|
36
|
+
OUT=$(bash "$HOOK" 2>&1) # No CLAUDE_MODEL set
|
|
37
|
+
assert_not_contains "unknown model no alert" "$OUT" "MODEL CHANGED"
|
|
38
|
+
|
|
39
|
+
# Test 6: History file is updated correctly
|
|
40
|
+
echo "opus-4.6" > "$HISTORY"
|
|
41
|
+
CLAUDE_MODEL="opus-4.7" bash "$HOOK" > /dev/null 2>&1
|
|
42
|
+
STORED=$(cat "$HISTORY")
|
|
43
|
+
if [ "$STORED" = "opus-4.7" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: history should store new model (got $STORED)"; fi
|
|
44
|
+
|
|
45
|
+
# Test 7: Back-to-back changes detected
|
|
46
|
+
CLAUDE_MODEL="sonnet-4.5" bash "$HOOK" > /dev/null 2>&1
|
|
47
|
+
OUT=$(CLAUDE_MODEL="haiku-4.5" bash "$HOOK" 2>&1)
|
|
48
|
+
assert_contains "sequential change detected" "$OUT" "MODEL CHANGED"
|
|
49
|
+
assert_contains "shows sonnet" "$OUT" "sonnet-4.5"
|
|
50
|
+
|
|
51
|
+
# Cleanup
|
|
52
|
+
rm -f "$HISTORY"
|
|
53
|
+
|
|
54
|
+
echo "model-version-change-alert: $PASS passed, $FAIL failed"
|
|
55
|
+
exit $FAIL
|