#!/usr/bin/env bash # Scribe plugin — SessionStart push channel. # # Curls the operator's Scribe instance for always-on rules + active-project # context and emits it as SessionStart `additionalContext`. Config comes from # the plugin's userConfig, which Claude Code exports to hook subprocesses as # CLAUDE_PLUGIN_OPTION_ env vars: # CLAUDE_PLUGIN_OPTION_api_endpoint base URL, no trailing slash # CLAUDE_PLUGIN_OPTION_api_token fmcp_ API key (sensitive) # # The active project is NOT configured here — it's resolved server-side from # the working repo's git remote (see services/repo_bindings). This keeps a # single install working across many repos/projects: bind each repo once with # the `bind_repo` MCP tool. An unbound repo just yields standing rules + a hint. # # IMPORTANT: do NOT pass config via `${user_config.*}` substitution in # hooks.json — sensitive userConfig values (api_token) are kept in the keychain # and are never spliced into a hook command line, so the placeholder arrives # unexpanded. The harness env vars above are the supported channel. SCRIBE_URL # / SCRIBE_TOKEN still override, for the settings.json dogfooding path where the # hook is wired up by hand. # # FAIL-OPEN: any missing tool/config, network error, or unreachable instance # injects nothing and exits 0. A session must never be blocked by Scribe. set -uo pipefail command -v jq >/dev/null 2>&1 || exit 0 command -v curl >/dev/null 2>&1 || exit 0 url=${SCRIBE_URL:-${CLAUDE_PLUGIN_OPTION_api_endpoint:-}} token=${SCRIBE_TOKEN:-${CLAUDE_PLUGIN_OPTION_api_token:-}} # Guard against an unexpanded `${...}` placeholder reaching us as a literal — it # would otherwise be sent as a garbage Bearer token and 401. Treat as unset. case "$url" in *'${'*) url="" ;; esac case "$token" in *'${'*) token="" ;; esac [ -n "$url" ] && [ -n "$token" ] || exit 0 # Resolve the working repo's remote so the server can map it to a project. Use # the session's project dir when provided, else the current dir. No remote (or # not a git repo) → omit; the server returns standing rules only. repo_dir=${CLAUDE_PROJECT_DIR:-$PWD} repo=$(git -C "$repo_dir" remote get-url origin 2>/dev/null || true) q="" if [ -n "$repo" ]; then enc=$(printf '%s' "$repo" | jq -rR '@uri' 2>/dev/null) || enc="" [ -n "$enc" ] && q="?repo=${enc}" fi body=$(curl -fsS --max-time 8 \ -H "Authorization: Bearer ${token}" \ "${url%/}/api/plugin/context${q}" 2>/dev/null) || exit 0 text=$(printf '%s' "$body" | jq -r '.context // empty' 2>/dev/null) || exit 0 [ -n "$text" ] || exit 0 jq -n --arg c "$text" \ '{hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: $c}}' exit 0