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_<key> 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) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 00:43:40 -04:00
parent 3a5835b109
commit 651119cfb0
3 changed files with 24 additions and 9 deletions
+1 -1
View File
@@ -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": {
+1 -1
View File
@@ -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\""
}
]
}
+22 -7
View File
@@ -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_<key> 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=""