cc-safe-setup 29.6.40 → 29.8.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/compact-circuit-breaker.sh +72 -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/exploration-budget-guard.sh +77 -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/thinking-stall-detector.sh +61 -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-compact-circuit-breaker.sh +134 -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-exploration-budget-guard.sh +164 -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-thinking-stall-detector.sh +151 -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,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,164 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for exploration-budget-guard.sh
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
HOOK="$(dirname "$0")/../examples/exploration-budget-guard.sh"
|
|
6
|
+
PASS=0
|
|
7
|
+
FAIL=0
|
|
8
|
+
STATE_FILE="/tmp/.cc-exploration-budget/exploration-count"
|
|
9
|
+
|
|
10
|
+
setup() {
|
|
11
|
+
rm -f "$STATE_FILE"
|
|
12
|
+
mkdir -p /tmp/.cc-exploration-budget
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
run_hook() {
|
|
16
|
+
echo "$1" | bash "$HOOK" 2>&1 || true
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
assert_pass() {
|
|
20
|
+
local desc="$1"
|
|
21
|
+
local input="$2"
|
|
22
|
+
output=$(echo "$input" | bash "$HOOK" 2>&1) && rc=0 || rc=$?
|
|
23
|
+
if [ "$rc" -eq 0 ] && ! echo "$output" | grep -q "WARNING\|BLOCKED"; then
|
|
24
|
+
PASS=$((PASS + 1))
|
|
25
|
+
echo " PASS: $desc"
|
|
26
|
+
else
|
|
27
|
+
FAIL=$((FAIL + 1))
|
|
28
|
+
echo " FAIL: $desc (expected clean pass, rc=$rc)"
|
|
29
|
+
echo " output: $output"
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
assert_warn() {
|
|
34
|
+
local desc="$1"
|
|
35
|
+
local input="$2"
|
|
36
|
+
output=$(echo "$input" | bash "$HOOK" 2>&1) && rc=0 || rc=$?
|
|
37
|
+
if [ $rc -eq 0 ] && echo "$output" | grep -q "WARNING"; then
|
|
38
|
+
PASS=$((PASS + 1))
|
|
39
|
+
echo " PASS: $desc"
|
|
40
|
+
else
|
|
41
|
+
FAIL=$((FAIL + 1))
|
|
42
|
+
echo " FAIL: $desc (expected warning, rc=$rc)"
|
|
43
|
+
echo " output: $output"
|
|
44
|
+
fi
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
assert_block() {
|
|
48
|
+
local desc="$1"
|
|
49
|
+
local input="$2"
|
|
50
|
+
output=$(echo "$input" | bash "$HOOK" 2>&1) && rc=0 || rc=$?
|
|
51
|
+
if [ $rc -eq 2 ] && echo "$output" | grep -q "BLOCKED"; then
|
|
52
|
+
PASS=$((PASS + 1))
|
|
53
|
+
echo " PASS: $desc"
|
|
54
|
+
else
|
|
55
|
+
FAIL=$((FAIL + 1))
|
|
56
|
+
echo " FAIL: $desc (expected block, rc=$rc)"
|
|
57
|
+
echo " output: $output"
|
|
58
|
+
fi
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
READ_INPUT='{"tool_name":"Read","tool_input":{"file_path":"/tmp/test.txt"}}'
|
|
62
|
+
GLOB_INPUT='{"tool_name":"Glob","tool_input":{"pattern":"*.ts"}}'
|
|
63
|
+
GREP_INPUT='{"tool_name":"Grep","tool_input":{"pattern":"foo"}}'
|
|
64
|
+
EDIT_INPUT='{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test.txt","old_string":"a","new_string":"b"}}'
|
|
65
|
+
WRITE_INPUT='{"tool_name":"Write","tool_input":{"file_path":"/tmp/test.txt","content":"hello"}}'
|
|
66
|
+
OTHER_INPUT='{"tool_name":"Bash","tool_input":{"command":"ls"}}'
|
|
67
|
+
|
|
68
|
+
echo "=== exploration-budget-guard tests ==="
|
|
69
|
+
|
|
70
|
+
# Test 1: Single read passes
|
|
71
|
+
setup
|
|
72
|
+
assert_pass "single Read passes" "$READ_INPUT"
|
|
73
|
+
|
|
74
|
+
# Test 2: Non-tracked tool passes
|
|
75
|
+
setup
|
|
76
|
+
assert_pass "Bash is not tracked" "$OTHER_INPUT"
|
|
77
|
+
|
|
78
|
+
# Test 3: Edit resets counter
|
|
79
|
+
setup
|
|
80
|
+
for i in $(seq 1 20); do
|
|
81
|
+
echo "$READ_INPUT" | bash "$HOOK" >/dev/null 2>&1 || true
|
|
82
|
+
done
|
|
83
|
+
echo "$EDIT_INPUT" | bash "$HOOK" >/dev/null 2>&1 || true
|
|
84
|
+
assert_pass "Read after Edit reset passes" "$READ_INPUT"
|
|
85
|
+
|
|
86
|
+
# Test 4: Write resets counter
|
|
87
|
+
setup
|
|
88
|
+
for i in $(seq 1 20); do
|
|
89
|
+
echo "$READ_INPUT" | bash "$HOOK" >/dev/null 2>&1 || true
|
|
90
|
+
done
|
|
91
|
+
echo "$WRITE_INPUT" | bash "$HOOK" >/dev/null 2>&1 || true
|
|
92
|
+
assert_pass "Read after Write reset passes" "$READ_INPUT"
|
|
93
|
+
|
|
94
|
+
# Test 5: Warning at threshold
|
|
95
|
+
setup
|
|
96
|
+
for i in $(seq 1 24); do
|
|
97
|
+
echo "$READ_INPUT" | bash "$HOOK" >/dev/null 2>&1 || true
|
|
98
|
+
done
|
|
99
|
+
assert_warn "warns at 25 reads" "$READ_INPUT"
|
|
100
|
+
|
|
101
|
+
# Test 6: Different read tools count together
|
|
102
|
+
setup
|
|
103
|
+
for i in $(seq 1 8); do
|
|
104
|
+
echo "$READ_INPUT" | bash "$HOOK" >/dev/null 2>&1 || true
|
|
105
|
+
echo "$GLOB_INPUT" | bash "$HOOK" >/dev/null 2>&1 || true
|
|
106
|
+
echo "$GREP_INPUT" | bash "$HOOK" >/dev/null 2>&1 || true
|
|
107
|
+
done
|
|
108
|
+
assert_warn "mixed read tools warn at 25" "$READ_INPUT"
|
|
109
|
+
|
|
110
|
+
# Test 7: Block at 40
|
|
111
|
+
setup
|
|
112
|
+
for i in $(seq 1 39); do
|
|
113
|
+
echo "$READ_INPUT" | bash "$HOOK" >/dev/null 2>&1 || true
|
|
114
|
+
done
|
|
115
|
+
assert_block "blocks at 40 reads" "$READ_INPUT"
|
|
116
|
+
|
|
117
|
+
# Test 8: Block shows count
|
|
118
|
+
setup
|
|
119
|
+
for i in $(seq 1 41); do
|
|
120
|
+
echo "$READ_INPUT" | bash "$HOOK" >/dev/null 2>&1 || true
|
|
121
|
+
done
|
|
122
|
+
output=$(echo "$READ_INPUT" | bash "$HOOK" 2>&1 || true)
|
|
123
|
+
if echo "$output" | grep -q "EXCEEDED"; then
|
|
124
|
+
PASS=$((PASS + 1))
|
|
125
|
+
echo " PASS: block message shows EXCEEDED"
|
|
126
|
+
else
|
|
127
|
+
FAIL=$((FAIL + 1))
|
|
128
|
+
echo " FAIL: block message should show EXCEEDED"
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# Test 9: Timeout reset (simulate 11 min gap)
|
|
132
|
+
setup
|
|
133
|
+
for i in $(seq 1 30); do
|
|
134
|
+
echo "$READ_INPUT" | bash "$HOOK" >/dev/null 2>&1 || true
|
|
135
|
+
done
|
|
136
|
+
# Fake old timestamp
|
|
137
|
+
echo "30 $(($(date +%s) - 700))" > "$STATE_FILE"
|
|
138
|
+
assert_pass "resets after 10min gap" "$READ_INPUT"
|
|
139
|
+
|
|
140
|
+
# Test 10: Glob passes normally
|
|
141
|
+
setup
|
|
142
|
+
assert_pass "single Glob passes" "$GLOB_INPUT"
|
|
143
|
+
|
|
144
|
+
# Test 11: Grep passes normally
|
|
145
|
+
setup
|
|
146
|
+
assert_pass "single Grep passes" "$GREP_INPUT"
|
|
147
|
+
|
|
148
|
+
# Test 12: Counter persists across different read tools
|
|
149
|
+
setup
|
|
150
|
+
for i in $(seq 1 10); do
|
|
151
|
+
echo "$READ_INPUT" | bash "$HOOK" >/dev/null 2>&1 || true
|
|
152
|
+
done
|
|
153
|
+
for i in $(seq 1 10); do
|
|
154
|
+
echo "$GLOB_INPUT" | bash "$HOOK" >/dev/null 2>&1 || true
|
|
155
|
+
done
|
|
156
|
+
for i in $(seq 1 5); do
|
|
157
|
+
echo "$GREP_INPUT" | bash "$HOOK" >/dev/null 2>&1 || true
|
|
158
|
+
done
|
|
159
|
+
# Count should be 25 now
|
|
160
|
+
assert_warn "warns at 25 mixed reads" "$GREP_INPUT"
|
|
161
|
+
|
|
162
|
+
echo ""
|
|
163
|
+
echo "Results: $PASS passed, $FAIL failed out of $((PASS + FAIL)) tests"
|
|
164
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
|
@@ -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
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for move-delete-sequence-guard.sh
|
|
3
|
+
# Run: bash tests/test-move-delete-sequence-guard.sh
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
PASS=0
|
|
7
|
+
FAIL=0
|
|
8
|
+
HOOK="$(dirname "$0")/../examples/move-delete-sequence-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 "move-delete-sequence-guard.sh tests"
|
|
24
|
+
echo ""
|
|
25
|
+
|
|
26
|
+
# --- Block: mv + rm -rf on parent directory (#49129 pattern) ---
|
|
27
|
+
test_hook '{"tool_input":{"command":"mv /home/user/project/important.txt /tmp/ && rm -rf /home/user/project"}}' 2 "Block mv file then rm -rf parent (&&)"
|
|
28
|
+
test_hook '{"tool_input":{"command":"mv /home/user/project/src/main.py /tmp/backup/ ; rm -rf /home/user/project/src"}}' 2 "Block mv file then rm -rf parent (;)"
|
|
29
|
+
test_hook '{"tool_input":{"command":"mv /data/app/config.yml /tmp/ || rm -rf /data/app"}}' 2 "Block mv file then rm -rf parent (||)"
|
|
30
|
+
|
|
31
|
+
# --- Block: mv + rm -rf on same path ---
|
|
32
|
+
test_hook '{"tool_input":{"command":"mv /home/user/mydir /tmp/backup && rm -rf /home/user/mydir"}}' 2 "Block mv dir then rm -rf same dir"
|
|
33
|
+
|
|
34
|
+
# --- Block: mv + rm -rf on ancestor directory ---
|
|
35
|
+
test_hook '{"tool_input":{"command":"mv /home/user/project/src/lib/util.py /tmp/ && rm -rf /home/user/project"}}' 2 "Block mv file then rm -rf ancestor"
|
|
36
|
+
test_hook '{"tool_input":{"command":"mv /var/data/app/logs/today.log /tmp/ ; rm -rf /var/data"}}' 2 "Block mv file then rm -rf distant ancestor"
|
|
37
|
+
|
|
38
|
+
# --- Allow: mv and rm on unrelated paths ---
|
|
39
|
+
test_hook '{"tool_input":{"command":"mv /tmp/old.txt /tmp/new.txt && rm /var/log/app.log"}}' 0 "Allow mv and rm on unrelated paths"
|
|
40
|
+
test_hook '{"tool_input":{"command":"mv file.txt backup/ && rm other_file.txt"}}' 0 "Allow mv and rm on different files"
|
|
41
|
+
|
|
42
|
+
# --- Allow: mv only (no rm) ---
|
|
43
|
+
test_hook '{"tool_input":{"command":"mv /home/user/file.txt /tmp/"}}' 0 "Allow mv without rm"
|
|
44
|
+
|
|
45
|
+
# --- Allow: rm only (no mv) ---
|
|
46
|
+
test_hook '{"tool_input":{"command":"rm -rf /tmp/junk"}}' 0 "Allow rm without mv"
|
|
47
|
+
|
|
48
|
+
# --- Allow: Empty/missing input ---
|
|
49
|
+
test_hook '{}' 0 "Allow empty input"
|
|
50
|
+
test_hook '{"tool_input":{}}' 0 "Allow missing command"
|
|
51
|
+
test_hook '{"tool_input":{"command":""}}' 0 "Allow empty command"
|
|
52
|
+
|
|
53
|
+
# --- Allow: Safe operations ---
|
|
54
|
+
test_hook '{"tool_input":{"command":"ls -la && echo done"}}' 0 "Allow non-destructive compound command"
|
|
55
|
+
test_hook '{"tool_input":{"command":"git mv old.txt new.txt"}}' 0 "Allow git mv (no rm)"
|
|
56
|
+
|
|
57
|
+
# --- Block: Real-world attack patterns ---
|
|
58
|
+
test_hook '{"tool_input":{"command":"mv /home/user/projects/webapp/src/index.js /tmp/safe/ && rm -rf /home/user/projects/webapp/src"}}' 2 "Block real-world: save one file, delete rest of src"
|
|
59
|
+
test_hook '{"tool_input":{"command":"mv /home/user/data/important.db /tmp/ ; rm -r /home/user/data"}}' 2 "Block real-world: save DB, delete data dir"
|
|
60
|
+
|
|
61
|
+
echo ""
|
|
62
|
+
echo "Results: $PASS passed, $FAIL failed out of $((PASS + FAIL)) tests"
|
|
63
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
set -euo pipefail
|
|
2
|
+
PASS=0
|
|
3
|
+
FAIL=0
|
|
4
|
+
HOOK="$(dirname "$0")/../examples/pr-duplicate-guard.sh"
|
|
5
|
+
test_hook() {
|
|
6
|
+
local input="$1" expected_exit="$2" desc="$3"
|
|
7
|
+
local actual_exit=0
|
|
8
|
+
echo "$input" | bash "$HOOK" > /dev/null 2>/dev/null || actual_exit=$?
|
|
9
|
+
if [ "$actual_exit" -eq "$expected_exit" ]; then
|
|
10
|
+
echo " PASS: $desc"
|
|
11
|
+
PASS=$((PASS + 1))
|
|
12
|
+
else
|
|
13
|
+
echo " FAIL: $desc (expected exit $expected_exit, got $actual_exit)"
|
|
14
|
+
FAIL=$((FAIL + 1))
|
|
15
|
+
fi
|
|
16
|
+
}
|
|
17
|
+
echo "=== pr-duplicate-guard.sh ==="
|
|
18
|
+
test_hook '{"tool_input":{"command":"git push origin main"}}' 0 "git push passes through"
|
|
19
|
+
test_hook '{"tool_input":{"command":"npm publish"}}' 0 "npm publish passes through"
|
|
20
|
+
test_hook '{"tool_input":{"command":"echo hello"}}' 0 "echo passes through"
|
|
21
|
+
test_hook '{"tool_input":{"command":"gh pr list"}}' 0 "gh pr list passes through"
|
|
22
|
+
test_hook '{"tool_input":{"command":"gh pr view 123"}}' 0 "gh pr view passes through"
|
|
23
|
+
test_hook '{"tool_input":{"command":"gh issue create --title test"}}' 0 "gh issue create passes through"
|
|
24
|
+
test_hook '{"tool_input":{"command":"gh pr create --title \"test\" --body \"test\""}}' 0 "gh pr create on unique branch passes"
|
|
25
|
+
test_hook '{}' 0 "empty input passes"
|
|
26
|
+
test_hook '' 0 "blank input passes"
|
|
27
|
+
echo ""
|
|
28
|
+
echo "Results: $PASS passed, $FAIL failed"
|
|
29
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|