From f36bad5b78d04c7aaef7f9f45df92d6734c02e73 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sat, 23 May 2026 16:55:46 -0700 Subject: [PATCH] Pipe SessionStart hook printf through cat to absorb EPIPE on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows + Git Bash, the SessionStart hook prints a confusing diagnostic at every startup ("printf: write error: Permission denied") when Claude Code closes the hook's stdout pipe before the printf has finished writing. The hook still runs to completion and context still gets injected, but the diagnostic surfaces every session because Git Bash's printf reports EPIPE as "Permission denied" (not "Broken pipe" like Linux) and our `set -euo pipefail` lets that error escape. Piping each printf through `cat` makes the external cat process the recipient of any SIGPIPE / EPIPE. cat's failure does not propagate to the parent bash under pipefail because cat is the last command in the pipeline and exits cleanly when the pipe stays open long enough to hold the data. On macOS/Linux the cat passthrough is transparent (no behavior change, no measurable cost). Verified: - Existing tests/hooks/test-session-start.sh: 7/7 pass on macOS - Manual run on Windows 11 + Git Bash 5.2 + Node 22 produces valid JSON, clean stderr, and exit 0 - JSON output is byte-identical to the unpatched hook Reported by @silvertakana in #1612, attribution preserved in the Co-authored-by trailer below — this is the same fix shape the original PR proposed. Co-authored-by: silvertakana Closes #1612. --- hooks/session-start | 6 +++--- hooks/session-start-codex | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/hooks/session-start b/hooks/session-start index 0731962f..93a6bc2c 100755 --- a/hooks/session-start +++ b/hooks/session-start @@ -37,13 +37,13 @@ session_context="\nYou have superpowers.\n\n**Below is the # See: https://github.com/obra/superpowers/issues/571 if [ -n "${CURSOR_PLUGIN_ROOT:-}" ]; then # Cursor sets CURSOR_PLUGIN_ROOT (may also set CLAUDE_PLUGIN_ROOT) - printf '{\n "additional_context": "%s"\n}\n' "$session_context" + printf '{\n "additional_context": "%s"\n}\n' "$session_context" | cat elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -z "${COPILOT_CLI:-}" ]; then # Claude Code sets CLAUDE_PLUGIN_ROOT without COPILOT_CLI - printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context" + printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context" | cat else # Copilot CLI (sets COPILOT_CLI=1) or unknown platform — SDK standard format - printf '{\n "additionalContext": "%s"\n}\n' "$session_context" + printf '{\n "additionalContext": "%s"\n}\n' "$session_context" | cat fi exit 0 diff --git a/hooks/session-start-codex b/hooks/session-start-codex index a6cc3cf4..f25ea084 100755 --- a/hooks/session-start-codex +++ b/hooks/session-start-codex @@ -21,6 +21,6 @@ escape_for_json() { using_superpowers_escaped=$(escape_for_json "$using_superpowers_content") session_context="\nYou have superpowers.\n\n**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, follow the Codex skill-loading instructions in that skill:**\n\n${using_superpowers_escaped}\n" -printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context" +printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context" | cat exit 0