From f96331844c7aeba28da5f912fcbb258a82e62401 Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Sun, 21 Jun 2026 12:12:49 +0200 Subject: [PATCH] fix(design-gate): resolve claude on a sanitized PATH (real cause of unknown) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "unknown -> exit 11" path triggers when `command -v claude` fails. Root cause is NOT the interactive alias (claude->dtach_claude) not surviving the subshell — that's true but harmless: the real binary is on the inherited PATH, so `command -v` finds it in a normal `bash script.sh` (proven: toggle-external and the gate both resolve claude). The actual lever is PATH carrying the nvm node bin. A skill/hook that shells the gate out with a sanitized PATH, or a node upgrade moving the version-pinned nvm path, loses it. ensure_claude_on_path(): if `command -v claude` already resolves, do nothing; else probe known install dirs (~/.claude/local, ~/.local/bin, /usr/local/bin) and the nvm glob, prepending the bin dir — which carries BOTH claude and its node runtime (claude's shebang needs node, same dir). nvm keeps old versions after an upgrade, so pick the newest that ships claude via sort -V, not the first glob match. If nothing resolves, command -v still fails -> unknown -> exit 11 (fail-visible net stays). Verified: shellcheck clean; normal PATH -> READY exit 0 (function returns early, no regression); PATH=/usr/bin:/bin (sanitized hook) -> now resolves claude via the nvm glob and reports REAL magic state (READY exit 0), where before the fix it was exit 11 unknown. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/design-tool-gate.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/design-tool-gate.sh b/lib/design-tool-gate.sh index d14a560..b72f20b 100755 --- a/lib/design-tool-gate.sh +++ b/lib/design-tool-gate.sh @@ -51,6 +51,33 @@ PROFILE_FILE="$PROFILES_DIR/$PROFILE.profile" [ -x "$PROFILE_SH" ] || { echo "design-gate: profile.sh not executable at $PROFILE_SH" >&2; exit 2; } [ -f "$PROFILE_FILE" ] || { echo "design-gate: profile '$PROFILE' not found" >&2; exit 2; } +# Ensure the claude CLI + its node runtime are reachable even when a skill/hook +# shells this script out with a sanitized PATH. The interactive alias +# claude->dtach_claude never reaches a non-interactive subshell; the real binary +# AND its node bin dir are what matter (claude's shebang needs node, same dir). +# If `command -v claude` already resolves, do nothing; else probe known install +# dirs and prepend. nvm keeps old node versions after an upgrade, so pick the +# newest that actually ships claude (sort -V), not the first glob match. +ensure_claude_on_path() { + command -v claude >/dev/null 2>&1 && return + local cand + for cand in \ + "$HOME/.claude/local/claude" \ + "$HOME/.local/bin/claude" \ + /usr/local/bin/claude; do + [ -x "$cand" ] && { PATH="$(dirname "$cand"):$PATH"; return; } + done + local m newest matches=() + for m in "$HOME"/.nvm/versions/node/*/bin/claude; do + [ -x "$m" ] && matches+=("$m") + done + if [ "${#matches[@]}" -gt 0 ]; then + newest="$(printf '%s\n' "${matches[@]}" | sort -V | tail -1)" + PATH="$(dirname "$newest"):$PATH" + fi +} +ensure_claude_on_path + # Gate scope: the "# GATE-BLOCK:" allowlist (one or more lines, concatenated). # Empty => fall back to "every gate-relevant entry is in scope" (coarse). core_set="$(grep '^# GATE-BLOCK:' "$PROFILE_FILE" 2>/dev/null \