diff --git a/hooks/hooks.json b/hooks/hooks.json index dcb9926..be17df6 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-plan-file-validator.sh" + "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/loop-plan-file-validator.sh\"" } ] } @@ -17,7 +17,7 @@ "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-write-validator.sh" + "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/loop-write-validator.sh\"" } ] }, @@ -26,7 +26,7 @@ "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-edit-validator.sh" + "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/loop-edit-validator.sh\"" } ] }, @@ -35,7 +35,7 @@ "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-read-validator.sh" + "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/loop-read-validator.sh\"" } ] }, @@ -44,7 +44,7 @@ "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-bash-validator.sh" + "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/loop-bash-validator.sh\"" } ] } @@ -55,7 +55,7 @@ "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-post-bash-hook.sh" + "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/loop-post-bash-hook.sh\"" } ] } @@ -65,7 +65,7 @@ "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-codex-stop-hook.sh", + "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/loop-codex-stop-hook.sh\"", "timeout": 7200 } ] diff --git a/hooks/lib/loop-common.sh b/hooks/lib/loop-common.sh index 5726b23..0133767 100755 --- a/hooks/lib/loop-common.sh +++ b/hooks/lib/loop-common.sh @@ -792,6 +792,12 @@ to_lower() { echo "$1" | tr '[:upper:]' '[:lower:]' } +# Normalize Windows path separators to the POSIX-style separators used by the +# hook validators' string patterns. +normalize_path_separators() { + echo "${1//\\//}" +} + # Check if a path (lowercase) matches a round file pattern # Usage: is_round_file "$lowercase_path" "summary|prompt|todos|contract" is_round_file_type() { @@ -1252,7 +1258,7 @@ is_cancel_authorized() { # Check if a path is inside .humanize/rlcr directory is_in_humanize_loop_dir() { local path="$1" - echo "$path" | grep -q '\.humanize/rlcr/' + normalize_path_separators "$path" | grep -q '\.humanize/rlcr/' } # Check if a git add command would add .humanize files to version control diff --git a/hooks/loop-edit-validator.sh b/hooks/loop-edit-validator.sh index fb9f8e1..56dbc2a 100755 --- a/hooks/loop-edit-validator.sh +++ b/hooks/loop-edit-validator.sh @@ -28,6 +28,7 @@ if [[ "$TOOL_NAME" != "Edit" ]]; then fi FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path // ""') +FILE_PATH=$(normalize_path_separators "$FILE_PATH") FILE_PATH_LOWER=$(to_lower "$FILE_PATH") # Extract session_id from hook input for session-aware loop filtering diff --git a/hooks/loop-read-validator.sh b/hooks/loop-read-validator.sh index b812288..aae3001 100755 --- a/hooks/loop-read-validator.sh +++ b/hooks/loop-read-validator.sh @@ -44,6 +44,7 @@ if ! require_tool_input_field "$HOOK_INPUT" "file_path"; then fi FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path // ""') +FILE_PATH=$(normalize_path_separators "$FILE_PATH") FILE_PATH_LOWER=$(to_lower "$FILE_PATH") # Extract session_id from hook input for session-aware loop filtering diff --git a/hooks/loop-write-validator.sh b/hooks/loop-write-validator.sh index 1d8f1e3..f069104 100755 --- a/hooks/loop-write-validator.sh +++ b/hooks/loop-write-validator.sh @@ -45,6 +45,7 @@ if ! require_tool_input_field "$HOOK_INPUT" "file_path"; then fi FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path // ""') +FILE_PATH=$(normalize_path_separators "$FILE_PATH") FILE_PATH_LOWER=$(to_lower "$FILE_PATH") # Extract session_id from hook input for session-aware loop filtering diff --git a/tests/test-allowlist-validators.sh b/tests/test-allowlist-validators.sh index fc5c2c9..aa3b51c 100755 --- a/tests/test-allowlist-validators.sh +++ b/tests/test-allowlist-validators.sh @@ -218,6 +218,50 @@ else fail "Write validator round-3-contract.md" "exit 2 with round error" "exit $EXIT_CODE, output: $RESULT" fi +WINDOWS_LOOP_DIR="${LOOP_DIR//\//\\}" + +# Test 11c: Write validator blocks stale summary with Windows separators +echo "Test 11c: Write validator blocks Windows-path round-3-summary.md" +WINDOWS_FILE_PATH="${WINDOWS_LOOP_DIR}\\round-3-summary.md" +HOOK_INPUT=$(jq -nc --arg file_path "$WINDOWS_FILE_PATH" '{tool_name: "Write", tool_input: {file_path: $file_path}}') +set +e +RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-write-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 2 ]] && echo "$RESULT" | grep -qi "round"; then + pass "Write validator blocks Windows-path round-3-summary.md" +else + fail "Write validator Windows-path round-3-summary.md" "exit 2 with round error" "exit $EXIT_CODE, output: $RESULT" +fi + +# Test 11d: Write validator allows current summary with Windows separators +echo "Test 11d: Write validator allows Windows-path round-5-summary.md" +WINDOWS_FILE_PATH="${WINDOWS_LOOP_DIR}\\round-5-summary.md" +HOOK_INPUT=$(jq -nc --arg file_path "$WINDOWS_FILE_PATH" '{tool_name: "Write", tool_input: {file_path: $file_path}}') +set +e +RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-write-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 0 ]]; then + pass "Write validator allows Windows-path round-5-summary.md" +else + fail "Write validator Windows-path round-5-summary.md" "exit 0" "exit $EXIT_CODE, output: $RESULT" +fi + +# Test 11e: Write validator blocks plan.md backup with Windows separators +echo "Test 11e: Write validator blocks Windows-path plan.md backup" +WINDOWS_FILE_PATH="${WINDOWS_LOOP_DIR}\\plan.md" +HOOK_INPUT=$(jq -nc --arg file_path "$WINDOWS_FILE_PATH" '{tool_name: "Write", tool_input: {file_path: $file_path}}') +set +e +RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-write-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 2 ]] && echo "$RESULT" | grep -qi "plan"; then + pass "Write validator blocks Windows-path plan.md backup" +else + fail "Write validator Windows-path plan.md backup" "exit 2 with plan error" "exit $EXIT_CODE, output: $RESULT" +fi + echo "" echo "=== Test: Edit Validator Allowlist ===" echo "" @@ -274,6 +318,48 @@ else fail "Edit validator round-0-contract.md" "exit 2 with round error" "exit $EXIT_CODE, output: $RESULT" fi +# Test 13d: Edit validator blocks stale summary with Windows separators +echo "Test 13d: Edit validator blocks Windows-path round-3-summary.md" +WINDOWS_FILE_PATH="${WINDOWS_LOOP_DIR}\\round-3-summary.md" +HOOK_INPUT=$(jq -nc --arg file_path "$WINDOWS_FILE_PATH" '{tool_name: "Edit", tool_input: {file_path: $file_path}}') +set +e +RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-edit-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 2 ]] && echo "$RESULT" | grep -qi "round"; then + pass "Edit validator blocks Windows-path round-3-summary.md" +else + fail "Edit validator Windows-path round-3-summary.md" "exit 2 with round error" "exit $EXIT_CODE, output: $RESULT" +fi + +# Test 13e: Edit validator allows current summary with Windows separators +echo "Test 13e: Edit validator allows Windows-path round-5-summary.md" +WINDOWS_FILE_PATH="${WINDOWS_LOOP_DIR}\\round-5-summary.md" +HOOK_INPUT=$(jq -nc --arg file_path "$WINDOWS_FILE_PATH" '{tool_name: "Edit", tool_input: {file_path: $file_path}}') +set +e +RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-edit-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 0 ]]; then + pass "Edit validator allows Windows-path round-5-summary.md" +else + fail "Edit validator Windows-path round-5-summary.md" "exit 0" "exit $EXIT_CODE, output: $RESULT" +fi + +# Test 13f: Edit validator blocks plan.md backup with Windows separators +echo "Test 13f: Edit validator blocks Windows-path plan.md backup" +WINDOWS_FILE_PATH="${WINDOWS_LOOP_DIR}\\plan.md" +HOOK_INPUT=$(jq -nc --arg file_path "$WINDOWS_FILE_PATH" '{tool_name: "Edit", tool_input: {file_path: $file_path}}') +set +e +RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-edit-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 2 ]] && echo "$RESULT" | grep -qi "plan"; then + pass "Edit validator blocks Windows-path plan.md backup" +else + fail "Edit validator Windows-path plan.md backup" "exit 2 with plan error" "exit $EXIT_CODE, output: $RESULT" +fi + # Test 14: Edit validator blocks round-4-todos.md echo "Test 14: Edit validator blocks round-4-todos.md" HOOK_INPUT='{"tool_name": "Edit", "tool_input": {"file_path": "'$LOOP_DIR'/round-4-todos.md"}}' @@ -369,6 +455,34 @@ else fail "Read validator round-3-contract.md" "exit 2 with round error" "exit $EXIT_CODE, output: $RESULT" fi +# Test 18c: Read validator blocks stale summary with Windows separators +echo "Test 18c: Read validator blocks Windows-path round-3-summary.md" +WINDOWS_FILE_PATH="${WINDOWS_LOOP_DIR}\\round-3-summary.md" +HOOK_INPUT=$(jq -nc --arg file_path "$WINDOWS_FILE_PATH" '{tool_name: "Read", tool_input: {file_path: $file_path}}') +set +e +RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-read-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 2 ]] && echo "$RESULT" | grep -qi "round"; then + pass "Read validator blocks Windows-path round-3-summary.md" +else + fail "Read validator Windows-path round-3-summary.md" "exit 2 with round error" "exit $EXIT_CODE, output: $RESULT" +fi + +# Test 18d: Read validator allows current contract with Windows separators +echo "Test 18d: Read validator allows Windows-path round-5-contract.md" +WINDOWS_FILE_PATH="${WINDOWS_LOOP_DIR}\\round-5-contract.md" +HOOK_INPUT=$(jq -nc --arg file_path "$WINDOWS_FILE_PATH" '{tool_name: "Read", tool_input: {file_path: $file_path}}') +set +e +RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-read-validator.sh" 2>&1) +EXIT_CODE=$? +set -e +if [[ $EXIT_CODE -eq 0 ]]; then + pass "Read validator allows Windows-path round-5-contract.md" +else + fail "Read validator Windows-path round-5-contract.md" "exit 0" "exit $EXIT_CODE, output: $RESULT" +fi + echo "" echo "=== Test: Bash Validator Allowlist (Path-Restricted) ===" echo ""