From 651119cfb0cda3e89f06ff65b188d816bd5ca301 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 10 Jun 2026 00:43:40 -0400 Subject: [PATCH] fix(plugin): read API token from CLAUDE_PLUGIN_OPTION env, not user_config placeholder The SessionStart push-channel hook passed the key via SCRIBE_TOKEN="${user_config.api_token}" in hooks.json, but api_token is sensitive:true. Claude Code keeps sensitive userConfig in the keychain and does not interpolate it into hook command strings (only into mcpServers headers), so the hook received the literal placeholder, sent it as the Bearer token, and the context endpoint 401'd -> fail-open -> no context injected. Read the harness-exported CLAUDE_PLUGIN_OPTION_ env vars instead (SCRIBE_* still override for the settings.json dogfooding path), and treat any unexpanded ${...} literal as unset so the hook fails open cleanly instead of 401-ing. Bump 0.1.1 -> 0.1.2 so installs refresh the cache. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugin/.claude-plugin/plugin.json | 2 +- plugin/hooks/hooks.json | 2 +- plugin/hooks/scribe_session_context.sh | 29 +++++++++++++++++++------- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 9b1c34d..4240356 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "scribe", "description": "Scribe second brain for Claude Code: MCP tools over your notes/tasks/projects/rules, a session-start push channel that surfaces your always-on rules + active-project context, and a set of universal process-skills (brainstorm, debug, TDD, plan, verify). Replaces superpowers + file-memory with one app-backed plugin.", - "version": "0.1.1", + "version": "0.1.2", "author": { "name": "Bryan Van Deusen" }, "mcpServers": { "scribe": { diff --git a/plugin/hooks/hooks.json b/plugin/hooks/hooks.json index 8e4a6d7..76ce66e 100644 --- a/plugin/hooks/hooks.json +++ b/plugin/hooks/hooks.json @@ -5,7 +5,7 @@ "hooks": [ { "type": "command", - "command": "SCRIBE_URL=\"${user_config.api_endpoint}\" SCRIBE_TOKEN=\"${user_config.api_token}\" SCRIBE_PROJECT_ID=\"${user_config.project_id}\" bash \"${CLAUDE_PLUGIN_ROOT}/hooks/scribe_session_context.sh\"" + "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/scribe_session_context.sh\"" } ] } diff --git a/plugin/hooks/scribe_session_context.sh b/plugin/hooks/scribe_session_context.sh index ca7b2c5..4ed77bb 100755 --- a/plugin/hooks/scribe_session_context.sh +++ b/plugin/hooks/scribe_session_context.sh @@ -3,10 +3,18 @@ # # Curls the operator's Scribe instance for always-on rules + active-project # context and emits it as SessionStart `additionalContext`. Config comes from -# plugin userConfig, passed in as env by hooks/hooks.json: -# SCRIBE_URL base URL, no trailing slash (user_config.api_endpoint) -# SCRIBE_TOKEN fmcp_ API key (user_config.api_token) -# SCRIBE_PROJECT_ID optional numeric project id (user_config.project_id) +# 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) +# CLAUDE_PLUGIN_OPTION_project_id optional numeric project id +# +# IMPORTANT: do NOT pass these 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 / SCRIBE_PROJECT_ID 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. @@ -15,9 +23,16 @@ set -uo pipefail command -v jq >/dev/null 2>&1 || exit 0 command -v curl >/dev/null 2>&1 || exit 0 -url=${SCRIBE_URL:-} -token=${SCRIBE_TOKEN:-} -project_id=${SCRIBE_PROJECT_ID:-} +url=${SCRIBE_URL:-${CLAUDE_PLUGIN_OPTION_api_endpoint:-}} +token=${SCRIBE_TOKEN:-${CLAUDE_PLUGIN_OPTION_api_token:-}} +project_id=${SCRIBE_PROJECT_ID:-${CLAUDE_PLUGIN_OPTION_project_id:-}} + +# 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 +case "$project_id" in *'${'*) project_id="" ;; esac + [ -n "$url" ] && [ -n "$token" ] || exit 0 q=""