mirror of
https://github.com/obra/superpowers.git
synced 2026-09-29 05:32:56 +08:00
* fix(hooks): make SessionStart hooks resolve without external PATH lookups
SessionStart:startup hooks can run before Claude Code's PATH is repaired
(anthropics/claude-code#43127), or ahead of a sibling PATH-fixing hook in
the same startup batch. hooks/session-start and the Unix half of
hooks/run-hook.cmd both shelled out to dirname, cat, and a PATH lookup for
bash itself, so an empty PATH made every SessionStart hook fail outright:
hooks/session-start: line 7: dirname: command not found
hooks/session-start: line 44: cat: command not found
None of those external calls are necessary:
- ${0%/*} replaces $(dirname "$0") for both scripts.
- ${BASH:-bash} replaces a bare 'bash' in run-hook.cmd's exec — bash always
sets $BASH to its own absolute path once running, so no PATH lookup is
needed to re-invoke it.
- $(<file) (a bash builtin) replaces 'cat file' for reading the skill body.
- The trailing '| cat' after each printf branch was a no-op pass-through
through another external binary; removed.
Adds a regression test in tests/hooks/test-session-start.sh that invokes
both hooks under an empty PATH.
Fixes #2310.
* Split $0 on / or \ so cmd-dispatched hooks find the plugin root
run-hook.cmd's Windows half starts bash with a backslash path
(C:\...\hooks\session-start). ${0%/*} found no slash there, so
session-start resolved the wrong plugin root and injected 'Error
reading using-superpowers skill' instead of the skill. MSYS dirname
handled backslashes, so dev was fine. Seen on Windows 11 + Git Bash
5.2.37 via cmd /c run-hook.cmd session-start.
---------
Co-authored-by: Jesse Vincent <jesse@primeradiant.com>
86 lines
3.8 KiB
Bash
Executable File
86 lines
3.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# SessionStart hook for superpowers plugin
|
|
|
|
set -euo pipefail
|
|
|
|
# Determine plugin root directory.
|
|
# Splits $0 on its last / or \ (run-hook.cmd's cmd half passes a Windows
|
|
# path), with no separator meaning the current directory. This avoids
|
|
# dirname because a SessionStart:startup hook can run with a broken/empty
|
|
# PATH (anthropics/claude-code#43127) before any PATH-repairing sibling hook
|
|
# in the same startup batch takes effect.
|
|
case "$0" in
|
|
*/*|*\\*) SCRIPT_DIR="$(cd "${0%[/\\]*}" && pwd)" ;;
|
|
*) SCRIPT_DIR="$(pwd)" ;;
|
|
esac
|
|
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
|
|
# Read using-superpowers content via bash's builtin $(< file) instead of
|
|
# cat, for the same PATH-independence reason.
|
|
skill_md="${PLUGIN_ROOT}/skills/using-superpowers/SKILL.md"
|
|
if [ -r "$skill_md" ]; then
|
|
using_superpowers_content="$(<"$skill_md")"
|
|
else
|
|
using_superpowers_content="Error reading using-superpowers skill"
|
|
fi
|
|
|
|
# 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.
|
|
# Muse sets MUSE_PLUGIN_ROOT and expects additionalContext (SDK standard).
|
|
#
|
|
# Uses printf instead of heredoc to work around bash 5.3+ heredoc hang.
|
|
# See: https://github.com/obra/superpowers/issues/571
|
|
#
|
|
# emit_json pipes printf through `cat` when cat is on PATH, to absorb EPIPE
|
|
# on Windows. On Windows + Git Bash, the harness can close the hook's stdout
|
|
# before printf finishes, and Git Bash's printf reports that EPIPE as
|
|
# "printf: write error: Permission denied", which `set -euo pipefail` lets
|
|
# escape as a startup diagnostic every session. Letting cat receive the EPIPE
|
|
# keeps it out of the hook's error path (#1612). When PATH is broken and cat
|
|
# can't be found (#2310), it falls back to printf alone so context still
|
|
# gets injected.
|
|
emit_json() {
|
|
# shellcheck disable=SC2059 # every caller passes a literal format string
|
|
if command -v cat >/dev/null 2>&1; then
|
|
printf "$@" | cat
|
|
else
|
|
printf "$@"
|
|
fi
|
|
}
|
|
|
|
if [ -n "${CURSOR_PLUGIN_ROOT:-}" ]; then
|
|
# Cursor sets CURSOR_PLUGIN_ROOT (may also set CLAUDE_PLUGIN_ROOT)
|
|
emit_json '{\n "additional_context": "%s"\n}\n' "$session_context"
|
|
elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -z "${COPILOT_CLI:-}" ] && [ -z "${MUSE_PLUGIN_ROOT:-}" ]; then
|
|
# Claude Code sets CLAUDE_PLUGIN_ROOT without COPILOT_CLI/MUSE_PLUGIN_ROOT
|
|
emit_json '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context"
|
|
elif [ -n "${MUSE_PLUGIN_ROOT:-}" ]; then
|
|
# Muse sets MUSE_PLUGIN_ROOT — try Claude-style nested output for Muse Spark
|
|
emit_json '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context"
|
|
else
|
|
# Copilot CLI (sets COPILOT_CLI=1) or unknown platform — SDK standard format
|
|
emit_json '{\n "additionalContext": "%s"\n}\n' "$session_context"
|
|
fi
|
|
|
|
exit 0
|