mirror of
https://github.com/obra/superpowers.git
synced 2026-06-11 05:09:05 +08:00
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 <silvertakana@users.noreply.github.com>
Closes #1612.
50 lines
2.2 KiB
Bash
Executable File
50 lines
2.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# SessionStart hook for superpowers plugin
|
|
|
|
set -euo pipefail
|
|
|
|
# Determine plugin root directory
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
|
|
# Read using-superpowers content
|
|
using_superpowers_content=$(cat "${PLUGIN_ROOT}/skills/using-superpowers/SKILL.md" 2>&1 || echo "Error reading using-superpowers skill")
|
|
|
|
# Escape string for JSON embedding using bash parameter substitution.
|
|
# Each ${s//old/new} is a single C-level pass - orders of magnitude
|
|
# faster than the character-by-character loop this replaces.
|
|
escape_for_json() {
|
|
local s="$1"
|
|
s="${s//\\/\\\\}"
|
|
s="${s//\"/\\\"}"
|
|
s="${s//$'\n'/\\n}"
|
|
s="${s//$'\r'/\\r}"
|
|
s="${s//$'\t'/\\t}"
|
|
printf '%s' "$s"
|
|
}
|
|
|
|
using_superpowers_escaped=$(escape_for_json "$using_superpowers_content")
|
|
session_context="<EXTREMELY_IMPORTANT>\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, use the 'Skill' tool:**\n\n${using_superpowers_escaped}\n</EXTREMELY_IMPORTANT>"
|
|
|
|
# Output context injection as JSON.
|
|
# Cursor hooks expect additional_context (snake_case).
|
|
# Claude Code hooks expect hookSpecificOutput.additionalContext (nested).
|
|
# Copilot CLI (v1.0.11+) and others expect additionalContext (top-level, SDK standard).
|
|
# Claude Code reads BOTH additional_context and hookSpecificOutput without
|
|
# deduplication, so we must emit only the field the current platform consumes.
|
|
#
|
|
# Uses printf instead of heredoc to work around bash 5.3+ heredoc hang.
|
|
# 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" | 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" | cat
|
|
else
|
|
# Copilot CLI (sets COPILOT_CLI=1) or unknown platform — SDK standard format
|
|
printf '{\n "additionalContext": "%s"\n}\n' "$session_context" | cat
|
|
fi
|
|
|
|
exit 0
|