diff --git a/plugin/hooks/scribe_session_context.sh b/plugin/hooks/scribe_session_context.sh index c9c90b9..111da25 100755 --- a/plugin/hooks/scribe_session_context.sh +++ b/plugin/hooks/scribe_session_context.sh @@ -23,16 +23,22 @@ # vars above are the supported channel; SCRIBE_URL / SCRIBE_TOKEN override for # the settings.json dogfooding path. # -# FAIL-OPEN: the dynamic tier injects nothing on any error; the static tier -# still fires. A session is never blocked by Scribe. +# FAIL-OPEN, BUT NOT SILENT: the dynamic tier never blocks a session. A *failed* +# dynamic fetch is surfaced as a short status line (not swallowed), so a session +# can tell "couldn't load live context" apart from "Scribe had nothing to say". +# A fully unconfigured install (no url AND no token) is the intended static-only +# mode and stays quiet. set -uo pipefail command -v jq >/dev/null 2>&1 || exit 0 # needed to emit the JSON envelope safely here=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) || exit 0 -# --- Tier 1: static behavioral mandate (always, keyless, networkless) --- out="" +# Append $1 to $out, separated by a horizontal rule when $out already has content. +append() { if [ -n "$out" ]; then out="${out}"$'\n\n---\n\n'"$1"; else out="$1"; fi; } + +# --- Tier 1: static behavioral mandate (always, keyless, networkless) --- [ -f "$here/scribe_static_context.md" ] && out=$(cat "$here/scribe_static_context.md") # --- Tier 2: dynamic rules + active-project context (best-effort) --- @@ -44,7 +50,9 @@ token=${SCRIBE_TOKEN:-${CLAUDE_PLUGIN_OPTION_api_token:-}} case "$url" in *'${'*) url="" ;; esac case "$token" in *'${'*) token="" ;; esac -if command -v curl >/dev/null 2>&1 && [ -n "$url" ] && [ -n "$token" ]; then +dyn="" +status="" +if [ -n "$url" ] && [ -n "$token" ] && command -v curl >/dev/null 2>&1; then # Resolve the working repo's remote so the server can map it to a project. repo_dir=${CLAUDE_PROJECT_DIR:-$PWD} repo=$(git -C "$repo_dir" remote get-url origin 2>/dev/null || true) @@ -56,19 +64,18 @@ if command -v curl >/dev/null 2>&1 && [ -n "$url" ] && [ -n "$token" ]; then body=$(curl -fsS --max-time 8 \ -H "Authorization: Bearer ${token}" \ "${url%/}/api/plugin/context${q}" 2>/dev/null) || body="" - if [ -n "$body" ]; then - dyn=$(printf '%s' "$body" | jq -r '.context // empty' 2>/dev/null) || dyn="" - if [ -n "$dyn" ]; then - if [ -n "$out" ]; then - out="${out}"$'\n\n---\n\n'"${dyn}" - else - out="$dyn" - fi - fi - fi + [ -n "$body" ] && dyn=$(printf '%s' "$body" | jq -r '.context // empty' 2>/dev/null) + [ -z "$dyn" ] && status="> ⚠️ Scribe: live rules/project context could not be loaded this session (instance unreachable or request failed). The standing guidance above still applies — pull rules with \`list_always_on_rules()\` and project context with \`enter_project()\` as needed." +elif [ -n "$url" ] && [ -z "$token" ]; then + # Endpoint configured but token absent: the signature of the known Claude Code + # userConfig export gap (sensitive values not always reaching the hook). + status="> ⚠️ Scribe: live context disabled this session — the API token did not reach this hook (a known Claude Code plugin-config gap). Tools still work; pull rules with \`list_always_on_rules()\` and project context with \`enter_project()\`." fi -# Nothing to inject (no static file AND no dynamic context) → stay silent. +[ -n "$dyn" ] && append "$dyn" +[ -n "$status" ] && append "$status" + +# Nothing at all to inject (no static file, no dynamic context, no status) → stay silent. [ -n "$out" ] || exit 0 jq -n --arg c "$out" \