feat(plugin): surface dynamic-tier failures in SessionStart hook (A2)

Fail-open but no longer silent. When the dynamic context fetch yields nothing,
append a short status line to the injected context so a session can tell
'couldn't load live context' apart from 'Scribe had nothing to say':
- endpoint+token present but fetch empty/failed -> 'instance unreachable / request failed'
- endpoint present but token absent -> fingerprints the known Claude Code
  userConfig export gap ('API token did not reach this hook')
A fully unconfigured install (no url AND no token) stays quiet — static-only is
the intended mode there. Static Tier 1 still always carries the mandate.

Refs plan 812 item A2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-13 16:14:27 -04:00
parent f125f86e16
commit 700cfc664b
+22 -15
View File
@@ -23,16 +23,22 @@
# vars above are the supported channel; SCRIBE_URL / SCRIBE_TOKEN override for # vars above are the supported channel; SCRIBE_URL / SCRIBE_TOKEN override for
# the settings.json dogfooding path. # the settings.json dogfooding path.
# #
# FAIL-OPEN: the dynamic tier injects nothing on any error; the static tier # FAIL-OPEN, BUT NOT SILENT: the dynamic tier never blocks a session. A *failed*
# still fires. A session is never blocked by Scribe. # 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 set -uo pipefail
command -v jq >/dev/null 2>&1 || exit 0 # needed to emit the JSON envelope safely 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 here=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) || exit 0
# --- Tier 1: static behavioral mandate (always, keyless, networkless) ---
out="" 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") [ -f "$here/scribe_static_context.md" ] && out=$(cat "$here/scribe_static_context.md")
# --- Tier 2: dynamic rules + active-project context (best-effort) --- # --- 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 "$url" in *'${'*) url="" ;; esac
case "$token" in *'${'*) token="" ;; 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. # Resolve the working repo's remote so the server can map it to a project.
repo_dir=${CLAUDE_PROJECT_DIR:-$PWD} repo_dir=${CLAUDE_PROJECT_DIR:-$PWD}
repo=$(git -C "$repo_dir" remote get-url origin 2>/dev/null || true) 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 \ body=$(curl -fsS --max-time 8 \
-H "Authorization: Bearer ${token}" \ -H "Authorization: Bearer ${token}" \
"${url%/}/api/plugin/context${q}" 2>/dev/null) || body="" "${url%/}/api/plugin/context${q}" 2>/dev/null) || body=""
if [ -n "$body" ]; then [ -n "$body" ] && dyn=$(printf '%s' "$body" | jq -r '.context // empty' 2>/dev/null)
dyn=$(printf '%s' "$body" | jq -r '.context // empty' 2>/dev/null) || dyn="" [ -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."
if [ -n "$dyn" ]; then elif [ -n "$url" ] && [ -z "$token" ]; then
if [ -n "$out" ]; then # Endpoint configured but token absent: the signature of the known Claude Code
out="${out}"$'\n\n---\n\n'"${dyn}" # userConfig export gap (sensitive values not always reaching the hook).
else 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()\`."
out="$dyn"
fi
fi
fi
fi 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 [ -n "$out" ] || exit 0
jq -n --arg c "$out" \ jq -n --arg c "$out" \