Skip to content

Commit a5f7446

Browse files
ianliuyCopilot
andcommitted
Fix Windows hook dispatch via explicit bash invocation
Normalize Windows path separators before validator path parsing so wrong-round checks still apply to backslash-separated RLCR paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f4e5721 commit a5f7446

6 files changed

Lines changed: 131 additions & 8 deletions

File tree

hooks/hooks.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"hooks": [
77
{
88
"type": "command",
9-
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-plan-file-validator.sh"
9+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/loop-plan-file-validator.sh\""
1010
}
1111
]
1212
}
@@ -17,7 +17,7 @@
1717
"hooks": [
1818
{
1919
"type": "command",
20-
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-write-validator.sh"
20+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/loop-write-validator.sh\""
2121
}
2222
]
2323
},
@@ -26,7 +26,7 @@
2626
"hooks": [
2727
{
2828
"type": "command",
29-
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-edit-validator.sh"
29+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/loop-edit-validator.sh\""
3030
}
3131
]
3232
},
@@ -35,7 +35,7 @@
3535
"hooks": [
3636
{
3737
"type": "command",
38-
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-read-validator.sh"
38+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/loop-read-validator.sh\""
3939
}
4040
]
4141
},
@@ -44,7 +44,7 @@
4444
"hooks": [
4545
{
4646
"type": "command",
47-
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-bash-validator.sh"
47+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/loop-bash-validator.sh\""
4848
}
4949
]
5050
}
@@ -55,7 +55,7 @@
5555
"hooks": [
5656
{
5757
"type": "command",
58-
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-post-bash-hook.sh"
58+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/loop-post-bash-hook.sh\""
5959
}
6060
]
6161
}
@@ -65,7 +65,7 @@
6565
"hooks": [
6666
{
6767
"type": "command",
68-
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/loop-codex-stop-hook.sh",
68+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/loop-codex-stop-hook.sh\"",
6969
"timeout": 7200
7070
}
7171
]

hooks/lib/loop-common.sh

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,12 @@ to_lower() {
792792
echo "$1" | tr '[:upper:]' '[:lower:]'
793793
}
794794

795+
# Normalize Windows path separators to the POSIX-style separators used by the
796+
# hook validators' string patterns.
797+
normalize_path_separators() {
798+
echo "${1//\\//}"
799+
}
800+
795801
# Check if a path (lowercase) matches a round file pattern
796802
# Usage: is_round_file "$lowercase_path" "summary|prompt|todos|contract"
797803
is_round_file_type() {
@@ -1252,7 +1258,7 @@ is_cancel_authorized() {
12521258
# Check if a path is inside .humanize/rlcr directory
12531259
is_in_humanize_loop_dir() {
12541260
local path="$1"
1255-
echo "$path" | grep -q '\.humanize/rlcr/'
1261+
normalize_path_separators "$path" | grep -q '\.humanize/rlcr/'
12561262
}
12571263

12581264
# Check if a git add command would add .humanize files to version control

hooks/loop-edit-validator.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ if [[ "$TOOL_NAME" != "Edit" ]]; then
2828
fi
2929

3030
FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path // ""')
31+
FILE_PATH=$(normalize_path_separators "$FILE_PATH")
3132
FILE_PATH_LOWER=$(to_lower "$FILE_PATH")
3233

3334
# Extract session_id from hook input for session-aware loop filtering

hooks/loop-read-validator.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ if ! require_tool_input_field "$HOOK_INPUT" "file_path"; then
4444
fi
4545

4646
FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path // ""')
47+
FILE_PATH=$(normalize_path_separators "$FILE_PATH")
4748
FILE_PATH_LOWER=$(to_lower "$FILE_PATH")
4849

4950
# Extract session_id from hook input for session-aware loop filtering

hooks/loop-write-validator.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ if ! require_tool_input_field "$HOOK_INPUT" "file_path"; then
4545
fi
4646

4747
FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path // ""')
48+
FILE_PATH=$(normalize_path_separators "$FILE_PATH")
4849
FILE_PATH_LOWER=$(to_lower "$FILE_PATH")
4950

5051
# Extract session_id from hook input for session-aware loop filtering

tests/test-allowlist-validators.sh

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,50 @@ else
218218
fail "Write validator round-3-contract.md" "exit 2 with round error" "exit $EXIT_CODE, output: $RESULT"
219219
fi
220220

221+
WINDOWS_LOOP_DIR="${LOOP_DIR//\//\\}"
222+
223+
# Test 11c: Write validator blocks stale summary with Windows separators
224+
echo "Test 11c: Write validator blocks Windows-path round-3-summary.md"
225+
WINDOWS_FILE_PATH="${WINDOWS_LOOP_DIR}\\round-3-summary.md"
226+
HOOK_INPUT=$(jq -nc --arg file_path "$WINDOWS_FILE_PATH" '{tool_name: "Write", tool_input: {file_path: $file_path}}')
227+
set +e
228+
RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-write-validator.sh" 2>&1)
229+
EXIT_CODE=$?
230+
set -e
231+
if [[ $EXIT_CODE -eq 2 ]] && echo "$RESULT" | grep -qi "round"; then
232+
pass "Write validator blocks Windows-path round-3-summary.md"
233+
else
234+
fail "Write validator Windows-path round-3-summary.md" "exit 2 with round error" "exit $EXIT_CODE, output: $RESULT"
235+
fi
236+
237+
# Test 11d: Write validator allows current summary with Windows separators
238+
echo "Test 11d: Write validator allows Windows-path round-5-summary.md"
239+
WINDOWS_FILE_PATH="${WINDOWS_LOOP_DIR}\\round-5-summary.md"
240+
HOOK_INPUT=$(jq -nc --arg file_path "$WINDOWS_FILE_PATH" '{tool_name: "Write", tool_input: {file_path: $file_path}}')
241+
set +e
242+
RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-write-validator.sh" 2>&1)
243+
EXIT_CODE=$?
244+
set -e
245+
if [[ $EXIT_CODE -eq 0 ]]; then
246+
pass "Write validator allows Windows-path round-5-summary.md"
247+
else
248+
fail "Write validator Windows-path round-5-summary.md" "exit 0" "exit $EXIT_CODE, output: $RESULT"
249+
fi
250+
251+
# Test 11e: Write validator blocks plan.md backup with Windows separators
252+
echo "Test 11e: Write validator blocks Windows-path plan.md backup"
253+
WINDOWS_FILE_PATH="${WINDOWS_LOOP_DIR}\\plan.md"
254+
HOOK_INPUT=$(jq -nc --arg file_path "$WINDOWS_FILE_PATH" '{tool_name: "Write", tool_input: {file_path: $file_path}}')
255+
set +e
256+
RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-write-validator.sh" 2>&1)
257+
EXIT_CODE=$?
258+
set -e
259+
if [[ $EXIT_CODE -eq 2 ]] && echo "$RESULT" | grep -qi "plan"; then
260+
pass "Write validator blocks Windows-path plan.md backup"
261+
else
262+
fail "Write validator Windows-path plan.md backup" "exit 2 with plan error" "exit $EXIT_CODE, output: $RESULT"
263+
fi
264+
221265
echo ""
222266
echo "=== Test: Edit Validator Allowlist ==="
223267
echo ""
@@ -274,6 +318,48 @@ else
274318
fail "Edit validator round-0-contract.md" "exit 2 with round error" "exit $EXIT_CODE, output: $RESULT"
275319
fi
276320

321+
# Test 13d: Edit validator blocks stale summary with Windows separators
322+
echo "Test 13d: Edit validator blocks Windows-path round-3-summary.md"
323+
WINDOWS_FILE_PATH="${WINDOWS_LOOP_DIR}\\round-3-summary.md"
324+
HOOK_INPUT=$(jq -nc --arg file_path "$WINDOWS_FILE_PATH" '{tool_name: "Edit", tool_input: {file_path: $file_path}}')
325+
set +e
326+
RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-edit-validator.sh" 2>&1)
327+
EXIT_CODE=$?
328+
set -e
329+
if [[ $EXIT_CODE -eq 2 ]] && echo "$RESULT" | grep -qi "round"; then
330+
pass "Edit validator blocks Windows-path round-3-summary.md"
331+
else
332+
fail "Edit validator Windows-path round-3-summary.md" "exit 2 with round error" "exit $EXIT_CODE, output: $RESULT"
333+
fi
334+
335+
# Test 13e: Edit validator allows current summary with Windows separators
336+
echo "Test 13e: Edit validator allows Windows-path round-5-summary.md"
337+
WINDOWS_FILE_PATH="${WINDOWS_LOOP_DIR}\\round-5-summary.md"
338+
HOOK_INPUT=$(jq -nc --arg file_path "$WINDOWS_FILE_PATH" '{tool_name: "Edit", tool_input: {file_path: $file_path}}')
339+
set +e
340+
RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-edit-validator.sh" 2>&1)
341+
EXIT_CODE=$?
342+
set -e
343+
if [[ $EXIT_CODE -eq 0 ]]; then
344+
pass "Edit validator allows Windows-path round-5-summary.md"
345+
else
346+
fail "Edit validator Windows-path round-5-summary.md" "exit 0" "exit $EXIT_CODE, output: $RESULT"
347+
fi
348+
349+
# Test 13f: Edit validator blocks plan.md backup with Windows separators
350+
echo "Test 13f: Edit validator blocks Windows-path plan.md backup"
351+
WINDOWS_FILE_PATH="${WINDOWS_LOOP_DIR}\\plan.md"
352+
HOOK_INPUT=$(jq -nc --arg file_path "$WINDOWS_FILE_PATH" '{tool_name: "Edit", tool_input: {file_path: $file_path}}')
353+
set +e
354+
RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-edit-validator.sh" 2>&1)
355+
EXIT_CODE=$?
356+
set -e
357+
if [[ $EXIT_CODE -eq 2 ]] && echo "$RESULT" | grep -qi "plan"; then
358+
pass "Edit validator blocks Windows-path plan.md backup"
359+
else
360+
fail "Edit validator Windows-path plan.md backup" "exit 2 with plan error" "exit $EXIT_CODE, output: $RESULT"
361+
fi
362+
277363
# Test 14: Edit validator blocks round-4-todos.md
278364
echo "Test 14: Edit validator blocks round-4-todos.md"
279365
HOOK_INPUT='{"tool_name": "Edit", "tool_input": {"file_path": "'$LOOP_DIR'/round-4-todos.md"}}'
@@ -369,6 +455,34 @@ else
369455
fail "Read validator round-3-contract.md" "exit 2 with round error" "exit $EXIT_CODE, output: $RESULT"
370456
fi
371457

458+
# Test 18c: Read validator blocks stale summary with Windows separators
459+
echo "Test 18c: Read validator blocks Windows-path round-3-summary.md"
460+
WINDOWS_FILE_PATH="${WINDOWS_LOOP_DIR}\\round-3-summary.md"
461+
HOOK_INPUT=$(jq -nc --arg file_path "$WINDOWS_FILE_PATH" '{tool_name: "Read", tool_input: {file_path: $file_path}}')
462+
set +e
463+
RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-read-validator.sh" 2>&1)
464+
EXIT_CODE=$?
465+
set -e
466+
if [[ $EXIT_CODE -eq 2 ]] && echo "$RESULT" | grep -qi "round"; then
467+
pass "Read validator blocks Windows-path round-3-summary.md"
468+
else
469+
fail "Read validator Windows-path round-3-summary.md" "exit 2 with round error" "exit $EXIT_CODE, output: $RESULT"
470+
fi
471+
472+
# Test 18d: Read validator allows current contract with Windows separators
473+
echo "Test 18d: Read validator allows Windows-path round-5-contract.md"
474+
WINDOWS_FILE_PATH="${WINDOWS_LOOP_DIR}\\round-5-contract.md"
475+
HOOK_INPUT=$(jq -nc --arg file_path "$WINDOWS_FILE_PATH" '{tool_name: "Read", tool_input: {file_path: $file_path}}')
476+
set +e
477+
RESULT=$(echo "$HOOK_INPUT" | "$PROJECT_ROOT/hooks/loop-read-validator.sh" 2>&1)
478+
EXIT_CODE=$?
479+
set -e
480+
if [[ $EXIT_CODE -eq 0 ]]; then
481+
pass "Read validator allows Windows-path round-5-contract.md"
482+
else
483+
fail "Read validator Windows-path round-5-contract.md" "exit 0" "exit $EXIT_CODE, output: $RESULT"
484+
fi
485+
372486
echo ""
373487
echo "=== Test: Bash Validator Allowlist (Path-Restricted) ==="
374488
echo ""

0 commit comments

Comments
 (0)