fix: emit hook JSON with printf so a POSIX-mode shell cannot mangle it

Seven shell hooks build their JSON by interpolating a python json.dumps
result and emitting it with echo. bash's echo leaves the escapes alone, so
the hooks are correct when run through their own #!/bin/bash shebang. Run
them with sh on macOS, where /bin/sh is bash in POSIX mode with xpg_echo
set, and echo turns the escaped \n inside the string back into a real
newline, producing JSON with a raw control character that every parser
rejects.

printf '%s\n' never interprets backslashes in its argument, so the emitted
bytes are the same under every shell.

Reported by @dylanpulver while rebasing #226: the macOS failure of
test_copilot_shell_hooks_skip_context_when_disabled is this, not his
change. Diagnosis and the one-line repro were his.
This commit is contained in:
OthmanAdi
2026-08-20 21:58:24 +02:00
parent 28925727d1
commit 3c78c3ce44
7 changed files with 7 additions and 7 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ CURRENT_PHASE=$(grep -m1 "^## Current Phase" "$PLAN_FILE" 2>/dev/null || grep -m
if [ -n "$CURRENT_PHASE" ]; then
PYTHON=$(command -v python3 || command -v python)
ESCAPED=$($PYTHON -c "import sys,json; print(json.dumps(sys.stdin.read(), ensure_ascii=False))" <<< "[planning-with-files] Current: $CURRENT_PHASE" 2>/dev/null || echo "\"\"")
echo "{\"additionalContext\":$ESCAPED}"
printf '%s\n' "{\"additionalContext\":$ESCAPED}"
else
echo '{}'
fi
+1 -1
View File
@@ -23,7 +23,7 @@ PYTHON=$(command -v python3 || command -v python)
if [ -n "$PYTHON" ]; then
ESCAPED=$($PYTHON -c "import sys,json; print(json.dumps(sys.stdin.read(), ensure_ascii=False))" <<< "$CONTEXT" 2>/dev/null)
if [ -n "$ESCAPED" ] && [ "$ESCAPED" != "\"\"" ]; then
echo "{\"systemMessage\":$ESCAPED}"
printf '%s\n' "{\"systemMessage\":$ESCAPED}"
exit 0
fi
fi
+1 -1
View File
@@ -19,7 +19,7 @@ if [ -f "$SCRIPT_DIR/check-complete.sh" ]; then
if [ -n "$RESULT" ]; then
PYTHON=$(command -v python3 || command -v python)
ESCAPED=$($PYTHON -c "import sys,json; print(json.dumps(sys.stdin.read(), ensure_ascii=False))" <<< "$RESULT" 2>/dev/null || echo "\"\"")
echo "{\"systemMessage\":$ESCAPED}"
printf '%s\n' "{\"systemMessage\":$ESCAPED}"
exit 0
fi
fi
+1 -1
View File
@@ -24,7 +24,7 @@ fi
if [ -n "$CATCHUP" ]; then
ESCAPED=$($PYTHON -c "import sys,json; print(json.dumps(sys.stdin.read(), ensure_ascii=False))" <<< "$CATCHUP" 2>/dev/null || echo "\"[planning-with-files] Session recovery data available. Read task_plan.md, progress.md, and findings.md.\"")
echo "{\"hookSpecificOutput\":{\"additionalContext\":$ESCAPED}}"
printf '%s\n' "{\"hookSpecificOutput\":{\"additionalContext\":$ESCAPED}}"
else
echo '{"hookSpecificOutput":{"additionalContext":"[planning-with-files] Active plan detected. Read task_plan.md, progress.md, and findings.md before proceeding."}}'
fi
+1 -1
View File
@@ -34,7 +34,7 @@ except:
if [ -n "$ERROR_MSG" ]; then
CONTEXT="[planning-with-files] Error detected: ${ERROR_MSG}. Log this error in task_plan.md under Errors Encountered with the attempt number and resolution."
ESCAPED=$($PYTHON -c "import sys,json; print(json.dumps(sys.stdin.read(), ensure_ascii=False))" <<< "$CONTEXT" 2>/dev/null || echo "\"\"")
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"ErrorOccurred\",\"additionalContext\":$ESCAPED}}"
printf '%s\n' "{\"hookSpecificOutput\":{\"hookEventName\":\"ErrorOccurred\",\"additionalContext\":$ESCAPED}}"
else
echo '{}'
fi
+1 -1
View File
@@ -33,5 +33,5 @@ done
[ -z "$PYTHON" ] && PYTHON=$(command -v python3 2>/dev/null || command -v python 2>/dev/null)
ESCAPED=$(echo "$CONTEXT" | $PYTHON -c "import sys,json; print(json.dumps(sys.stdin.read(), ensure_ascii=False))" 2>/dev/null || echo "\"\"")
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":$ESCAPED}}"
printf '%s\n' "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":$ESCAPED}}"
exit 0
+1 -1
View File
@@ -45,5 +45,5 @@ fi
# Escape context for JSON
ESCAPED=$(echo "$CONTEXT" | $PYTHON -c "import sys,json; print(json.dumps(sys.stdin.read(), ensure_ascii=False))" 2>/dev/null || echo "\"\"")
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"SessionStart\",\"additionalContext\":$ESCAPED}}"
printf '%s\n' "{\"hookSpecificOutput\":{\"hookEventName\":\"SessionStart\",\"additionalContext\":$ESCAPED}}"
exit 0