fix(plugin): every hook's credential + URL encoding was broken

Three defects, each of which independently made a hook a no-op, and all
three failing silently — which is why the whole dynamic side of the plugin
looked "shipped" while doing nothing.

1. Wrong env var case. Claude Code exports userConfig to hooks as
   CLAUDE_PLUGIN_OPTION_<KEY> with the key UPPERCASED. All four hooks read
   CLAUDE_PLUGIN_OPTION_api_endpoint / _api_token, so both values were
   always empty. That killed the SessionStart dynamic tier, process sync,
   prompt auto-inject, and the write-path prior-art trigger at once.

2. jq -rR is line-oriented. `@uri` under -R encodes input LINE BY LINE, so
   a multi-line payload came back as several encoded lines joined by raw
   newlines — an invalid URL, curl fails, hook exits 0 in silence. Now
   -sRr. This one hid behind (1): auto-inject only ever worked for
   single-line prompts, and prior-art (which posts code, always
   multi-line) could never have worked at all.

3. cut -c1-1200 caps each LINE, not the payload, so the prior-art code
   budget wasn't a budget. Now head -c 1200.

Also widens the SessionStart warning: "neither URL nor token arrived" used
to be treated as a benign unconfigured install and stayed quiet. That is
exactly the state defect (1) produced, so the one install state that most
needed a signal was the only one that emitted none. It now says so, and
names the two other features it silently disables.

Verified against the live instance: dynamic rules + project context load,
auto-inject surfaces #2192 on a multi-line prompt, and the write-path
trigger returns the [here] place-arm hit on a multi-line edit with session
dedup suppressing the repeat.

Refs #2198, #2082

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
This commit is contained in:
2026-07-28 18:00:11 -04:00
parent 1b81310847
commit c569cdd0eb
4 changed files with 68 additions and 40 deletions
+19 -9
View File
@@ -13,9 +13,11 @@
# Any failure — unconfigured, unreachable, malformed — exits 0 in silence. A
# recall aid must not be able to stop the operator's work.
#
# Config (same as the other hooks), exported to the hook by Claude Code:
# CLAUDE_PLUGIN_OPTION_api_endpoint base URL, no trailing slash
# CLAUDE_PLUGIN_OPTION_api_token fmcp_ API key (sensitive)
# Config (same as the other hooks), exported to the hook by Claude Code with the
# userConfig key UPPERCASED (see #2198 — the lowercase spelling reads as empty
# and this hook then exits 0 in silence, looking exactly like "no prior art"):
# CLAUDE_PLUGIN_OPTION_API_ENDPOINT base URL, no trailing slash
# CLAUDE_PLUGIN_OPTION_API_TOKEN fmcp_ API key (sensitive)
# SCRIBE_URL / SCRIBE_TOKEN override for the settings.json dogfooding path.
set -uo pipefail
@@ -46,8 +48,8 @@ case "$file_path" in
exit 0 ;;
esac
url=${SCRIBE_URL:-${CLAUDE_PLUGIN_OPTION_api_endpoint:-}}
token=${SCRIBE_TOKEN:-${CLAUDE_PLUGIN_OPTION_api_token:-}}
url=${SCRIBE_URL:-${CLAUDE_PLUGIN_OPTION_API_ENDPOINT:-}}
token=${SCRIBE_TOKEN:-${CLAUDE_PLUGIN_OPTION_API_TOKEN:-}}
# Guard against an unexpanded ${...} placeholder arriving as a literal.
case "$url" in *'${'*) url="" ;; esac
case "$token" in *'${'*) token="" ;; esac
@@ -70,16 +72,24 @@ fi
# token limit well before this, so a bigger slice buys no extra signal — and the
# payload has to stay a GET (a read-scoped API key cannot POST, and every other
# plugin hook works with a read key).
q=$(printf '%s' "$code" | cut -c1-1200)
# `head -c`, not `cut -c1-1200`: cut is line-oriented and caps each line
# separately, so a 400-line edit sailed past the "1200 char" budget entirely and
# built a URL from the whole payload. head -c caps the total, which is the point.
q=$(printf '%s' "$code" | head -c 1200)
path_enc=$(printf '%s' "$rel_path" | jq -rR '@uri' 2>/dev/null) || exit 0
code_enc=$(printf '%s' "$q" | jq -rR '@uri' 2>/dev/null) || code_enc=""
# `-sRr`, not `-rR`: jq -R reads input LINE BY LINE, so a multi-line payload came
# back as several separately-encoded lines joined by raw newlines — an invalid
# URL that made curl fail, and this hook then exited 0 in silence. -s slurps the
# whole input into one string first. Newlines are exactly what code contains, so
# this hook could never have worked without it (issue #2198 / #2082).
path_enc=$(printf '%s' "$rel_path" | jq -sRr '@uri' 2>/dev/null) || exit 0
code_enc=$(printf '%s' "$q" | jq -sRr '@uri' 2>/dev/null) || code_enc=""
# Resolve the working repo's remote so the server can scope to the bound project.
repo=$(git -C "$lookup_dir" remote get-url origin 2>/dev/null || true)
repo_q=""
if [ -n "$repo" ]; then
enc=$(printf '%s' "$repo" | jq -rR '@uri' 2>/dev/null) || enc=""
enc=$(printf '%s' "$repo" | jq -sRr '@uri' 2>/dev/null) || enc=""
[ -n "$enc" ] && repo_q="&repo=${enc}"
fi