#!/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="\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" # 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