From f0b7e89468e6aeff6cf12569844ef71f951325bc Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Thu, 2 Jul 2026 14:07:22 +0200 Subject: [PATCH 1/5] =?UTF-8?q?fix(rtk):=20rtk=20resolution=20+=20absolute?= =?UTF-8?q?-path=20rewrite=20=E2=80=94=20compression=20was=20silently=20de?= =?UTF-8?q?ad?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rtk lives at ~/.cargo/bin but the hand-managed .bashrc lost the cargo line: command -v failed in hook AND tool shell, so the hook no-op'd with a stderr warn on every Bash call — input compression silently OFF. - Resolve RTK_BIN by probing known install dirs (LRN-036 class). - Substitute the ABSOLUTE path at the rewrite head: a bare 'rtk …' exits 127 in the tool shell, whose PATH the hook cannot fix (proven). - Compound rewrites carrying further bare rtk segments pass through unrewritten: quoted text (commit messages) makes a global substitution unsafe — lose compression, never emit a command that 127s (proven: a commit chain 127'd mid-flow). - detect_rtk probes the same dirs so the banner reports capability. - Re-pinned .rtk-hook.sha256: the rtk BINARY verifies the hook against it at execution time and refuses a modified hook — the pin is live machinery, not a vestige; coupling documented in the header. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016zA3Qh2Q1QpcGXzXxKeDHR --- hooks/.rtk-hook.sha256 | 2 +- hooks/rtk-rewrite.sh | 39 ++++++++++++++++++++++++++++++++++++--- lib/detect-plugins.sh | 4 +++- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/hooks/.rtk-hook.sha256 b/hooks/.rtk-hook.sha256 index 79741f9..2305033 100644 --- a/hooks/.rtk-hook.sha256 +++ b/hooks/.rtk-hook.sha256 @@ -1 +1 @@ -ef0d630994fd7ef5f2b84fb66cd6249c493bb8736bcacd4734d7c798125018fb rtk-rewrite.sh +0f43229d17d03342d27c0b836b9b70f25f98dfb80a35ffc8dc2488034cb8719c rtk-rewrite.sh diff --git a/hooks/rtk-rewrite.sh b/hooks/rtk-rewrite.sh index f7a42b5..faaf089 100755 --- a/hooks/rtk-rewrite.sh +++ b/hooks/rtk-rewrite.sh @@ -7,6 +7,10 @@ # which is the single source of truth (src/discover/registry.rs). # To add or change rewrite rules, edit the Rust registry — not this file. # +# INTEGRITY PIN: the rtk binary verifies this file against +# hooks/.rtk-hook.sha256 at execution time and refuses to run on mismatch. +# ANY edit here must re-pin: (cd hooks && sha256sum rtk-rewrite.sh > .rtk-hook.sha256) +# # Exit code protocol for `rtk rewrite`: # 0 + stdout Rewrite found, no deny/ask rule matched → auto-allow # 1 No RTK equivalent → pass through unchanged @@ -18,14 +22,27 @@ if ! command -v jq &>/dev/null; then exit 0 fi -if ! command -v rtk &>/dev/null; then +# PATH heal: hook/tool-shell PATH may lack the cargo bin dir (hand-managed +# ~/.bashrc can lose the cargo line — LRN-036 class). Resolve the ABSOLUTE +# binary path: the rewritten command executes in the tool shell, whose PATH +# the hook cannot fix — a bare `rtk …` rewrite would exit 127 there. +RTK_BIN="$(command -v rtk 2>/dev/null || true)" +RTK_ON_PATH=1 +if [ -z "$RTK_BIN" ]; then + RTK_ON_PATH=0 + for _d in "$HOME/.cargo/bin" "$HOME/.local/bin"; do + if [ -x "$_d/rtk" ]; then RTK_BIN="$_d/rtk"; break; fi + done +fi + +if [ -z "$RTK_BIN" ]; then echo "[rtk] WARNING: rtk is not installed or not in PATH. Hook cannot rewrite commands. Install: https://github.com/rtk-ai/rtk#installation" >&2 exit 0 fi # Version guard: rtk rewrite was added in 0.23.0. # Older binaries: warn once and exit cleanly (no silent failure). -RTK_VERSION=$(rtk --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) +RTK_VERSION=$("$RTK_BIN" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) if [ -n "$RTK_VERSION" ]; then MAJOR=$(echo "$RTK_VERSION" | cut -d. -f1) MINOR=$(echo "$RTK_VERSION" | cut -d. -f2) @@ -44,7 +61,7 @@ if [ -z "$CMD" ]; then fi # Delegate all rewrite + permission logic to the Rust binary. -REWRITTEN=$(rtk rewrite "$CMD" 2>/dev/null) +REWRITTEN=$("$RTK_BIN" rewrite "$CMD" 2>/dev/null) EXIT_CODE=$? case $EXIT_CODE in @@ -70,6 +87,22 @@ case $EXIT_CODE in ;; esac +# When rtk is NOT on PATH, a bare `rtk …` rewrite exits 127 in the tool +# shell (whose PATH the hook cannot fix). Substitute the absolute path at +# the string head — the only position safe to rewrite. Compound commands +# (`a && b`) can carry further bare rtk segments we canNOT substitute +# safely (quoted text, e.g. commit messages, may contain the same +# pattern): if any remain at a command position, pass through unrewritten +# — lose the compression, never emit a command that 127s. +if [ "$RTK_ON_PATH" -eq 0 ]; then + case "$REWRITTEN" in + rtk\ *) REWRITTEN="$RTK_BIN ${REWRITTEN#rtk }" ;; + esac + if printf '%s' "$REWRITTEN" | grep -Eq '(^|[;&|][[:space:]]*)rtk[[:space:]]'; then + exit 0 + fi +fi + ORIGINAL_INPUT=$(echo "$INPUT" | jq -c '.tool_input') UPDATED_INPUT=$(echo "$ORIGINAL_INPUT" | jq --arg cmd "$REWRITTEN" '.command = $cmd') diff --git a/lib/detect-plugins.sh b/lib/detect-plugins.sh index 7f0da36..4635306 100644 --- a/lib/detect-plugins.sh +++ b/lib/detect-plugins.sh @@ -10,7 +10,9 @@ # --- Always-on plugins --- detect_rtk() { - command -v rtk &>/dev/null + command -v rtk &>/dev/null && return 0 + # PATH heal: hook/session PATH may lack the cargo bin dir (LRN-036 class) + [ -x "$HOME/.cargo/bin/rtk" ] || [ -x "$HOME/.local/bin/rtk" ] } detect_superpowers() { From 8e61d03c43c3922b66499de5162b229a3f416886 Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Thu, 2 Jul 2026 14:07:29 +0200 Subject: [PATCH 2/5] =?UTF-8?q?fix(session-start):=20update-check=20reads?= =?UTF-8?q?=20origin/main=20=E2=80=94=20dead=20since=20master=E2=86=92main?= =?UTF-8?q?=20migration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git show origin/master:version.txt fatal-ed since the gitflow migration (2026-06-29): the 'update available' banner could never fire while a synchronous git fetch was still paid every session for a discarded result. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016zA3Qh2Q1QpcGXzXxKeDHR --- hooks/session-start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/session-start.sh b/hooks/session-start.sh index 016061b..31a9bcf 100644 --- a/hooks/session-start.sh +++ b/hooks/session-start.sh @@ -182,7 +182,7 @@ printf "│ 📦 v%-45s│\n" "$CONFIG_VERSION" # Version check: compare local vs remote (non-blocking) _remote_ver="" if [ -n "$REPO_DIR" ] && [ -d "$REPO_DIR/.git" ]; then - _remote_ver=$(cd "$REPO_DIR" 2>/dev/null && git fetch origin --quiet 2>/dev/null && git show origin/master:version.txt 2>/dev/null) || _remote_ver="" + _remote_ver=$(cd "$REPO_DIR" 2>/dev/null && git fetch origin --quiet 2>/dev/null && git show origin/main:version.txt 2>/dev/null) || _remote_ver="" fi if [ -n "$_remote_ver" ] && [ "$_remote_ver" != "$CONFIG_VERSION" ]; then printf "│ 🔄 update available: v%-27s│\n" "$_remote_ver" From 17fb6dda4362d011a67e7d839e7161a321c3a986 Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Thu, 2 Jul 2026 14:07:31 +0200 Subject: [PATCH 3/5] =?UTF-8?q?fix(tests):=20run-reconcile=20T6c=20?= =?UTF-8?q?=E2=80=94=20oracle=20pointed=20at=20the=20removed=20parasite=20?= =?UTF-8?q?dir?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit $MEM/../skills resolved to .claude/skills/ (the LRN-042 parasite, removed 2026-06-30 by make plugin Step 8.5), not the real skills/. Green at build time only because the parasite still existed — green-for-wrong-reason (LRN-077 class); red ever since. Suite back to 20/20. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016zA3Qh2Q1QpcGXzXxKeDHR --- lib/tests/run-reconcile.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/tests/run-reconcile.sh b/lib/tests/run-reconcile.sh index b83c2c3..03d8609 100755 --- a/lib/tests/run-reconcile.sh +++ b/lib/tests/run-reconcile.sh @@ -65,7 +65,10 @@ if has "$cand" "--help"; then ok "T5 surfaced --help candidate (BDR-001 ⇄ --he echo; echo "=== T6 live oracle smoke — oracles QUERY real git/fs (not a name) ===" if reconcile_oracle_merge_done "$REPO" "prune-memory"; then ok "T6a merge_done(prune-memory) via git log"; else no "T6a merge not found in git"; fi if reconcile_oracle_sha_exists "$REPO" "be1dcef"; then ok "T6b sha_exists(be1dcef) via cat-file"; else no "T6b sha missing"; fi -dk="$MEM/../skills/darwin-skill" +# $REPO here = lib/ (see line 12) → lib/../skills = the real skills/ dir. +# Was "$MEM/../skills" = .claude/skills/ — the LRN-042 parasite dir, removed +# 2026-06-30 by make plugin Step 8.5: green-for-wrong-reason (LRN-077 class). +dk="$REPO/../skills/darwin-skill" if reconcile_oracle_path_present "$dk"; then ok "T6c path_present(darwin-skill) via fs"; else no "T6c path absent"; fi echo; echo "================ $pass GREEN / $fail RED ================" From ca8df168853e47cd8285792fd4cab41d78fdaf21 Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Thu, 2 Jul 2026 14:07:40 +0200 Subject: [PATCH 4/5] fix(doctor): kill 3 permanent false sentinels (LRN-047 class) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EXPECTED_DENY hardcoded 100 vs 99 real → derive from committed settings.json (HEAD): auto-tracks legit deny edits, still flags live-vs-committed divergence. - EXPECTED_SKILLS required gstack 'health' (OFF by default, profile- managed): false warn on a default install with a wrong remedy — link.sh cannot restore gstack skills. Dropped; 'status' kept (repo- owned personal skill, git ls-files proven). - disable-model-invocation check required a key BDR-019 stripped repo-wide (2026-06-09) → warned on every owned skill since. Inverted into a BDR-019 regression watch. - pass message derives the skill list from the array (LRN-005 class: no hardcoded display drift). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016zA3Qh2Q1QpcGXzXxKeDHR --- doctor.sh | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/doctor.sh b/doctor.sh index ca36f08..58442be 100644 --- a/doctor.sh +++ b/doctor.sh @@ -213,11 +213,20 @@ print(len(d.get('permissions',{}).get('deny',[]))) if [ "$DENY_COUNT" = "?" ]; then warn "Could not parse deny count (python3 unavailable or JSON parse error)" else - EXPECTED_DENY=100 - if [ "$DENY_COUNT" -eq "$EXPECTED_DENY" ] 2>/dev/null; then - pass "Deny rules: $DENY_COUNT" + # Expected = deny count in the last COMMITTED settings.json. A hardcoded + # number drifts on every legit deny-list edit (false-warned for weeks at + # 100 vs 99 — LRN-047 class); deriving from HEAD auto-tracks legit edits + # and still flags live-vs-committed divergence. + EXPECTED_DENY=$(git -C "$REPO" show HEAD:settings.json 2>/dev/null | python3 -c " +import json,sys +print(len(json.load(sys.stdin).get('permissions',{}).get('deny',[]))) +" 2>/dev/null || echo "?") + if [ "$EXPECTED_DENY" = "?" ]; then + warn "Could not derive expected deny count from committed settings.json" + elif [ "$DENY_COUNT" -eq "$EXPECTED_DENY" ] 2>/dev/null; then + pass "Deny rules: $DENY_COUNT (matches committed settings.json)" else - warn "Deny rules: $DENY_COUNT (expected $EXPECTED_DENY) — settings may have been manually modified" + warn "Deny rules: $DENY_COUNT (committed: $EXPECTED_DENY) — live settings diverge from last commit" fi fi else @@ -310,8 +319,11 @@ else warn "gstack/browse/dist/ symlink missing — run: bash link.sh" fi -# Check owned skills have disable-model-invocation (skip external/symlinked skills) -MISSING_DMI=() +# BDR-019 (2026-06-09) stripped disable-model-invocation repo-wide so the +# model/orchestrators can self-route. The old check required the key on +# every owned skill — permanent false-warn since. Inverted: warn if any +# owned skill REintroduces the key (regression watch on BDR-019). +PRESENT_DMI=() for f in "$HOME/.claude/skills/"*/SKILL.md; do [ -f "$f" ] || continue dir=$(dirname "$f") @@ -319,19 +331,22 @@ for f in "$HOME/.claude/skills/"*/SKILL.md; do [ -L "$dir" ] && continue [ -L "$f" ] && continue name=$(basename "$dir") - if ! grep -q "disable-model-invocation" "$f" 2>/dev/null; then - MISSING_DMI+=("$name") + if grep -q "disable-model-invocation" "$f" 2>/dev/null; then + PRESENT_DMI+=("$name") fi done -if [ ${#MISSING_DMI[@]} -eq 0 ]; then - pass "All owned skills have disable-model-invocation" +if [ ${#PRESENT_DMI[@]} -eq 0 ]; then + pass "No owned skill carries disable-model-invocation (BDR-019)" else - warn "Owned skills missing disable-model-invocation: ${MISSING_DMI[*]}" + warn "Owned skills reintroduce disable-model-invocation (BDR-019 regression): ${PRESENT_DMI[*]}" fi -# Check expected skills are present +# Check expected skills are present. Repo-owned skills only: gstack skills +# (health, status, …) are OFF by default and toggled per profile — requiring +# them here false-warns on a default install, and "run link.sh" cannot +# restore them (they are profile-managed, not link.sh-managed). EXPECTED_SKILLS=( - "analyze" "doc" "health" "init-project" "onboard" "plugin-check" + "analyze" "doc" "init-project" "onboard" "plugin-check" "refactor" "ship-feature" "status" ) MISSING_SKILLS=() @@ -341,7 +356,7 @@ for skill in "${EXPECTED_SKILLS[@]}"; do fi done if [ ${#MISSING_SKILLS[@]} -eq 0 ]; then - pass "All ${#EXPECTED_SKILLS[@]} expected skills present (analyze, doc, health, init-project, onboard, plugin-check, refactor, ship-feature, status)" + pass "All ${#EXPECTED_SKILLS[@]} expected skills present (${EXPECTED_SKILLS[*]})" else warn "Missing skills: ${MISSING_SKILLS[*]} — run: bash link.sh" fi From 731ed95c985d320d34e295df9e659bf9ee70ccfe Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Thu, 2 Jul 2026 14:28:31 +0200 Subject: [PATCH 5/5] =?UTF-8?q?feat(rtk):=20drop=20auto-allow=20=E2=80=94?= =?UTF-8?q?=20permission=20control=20returns=20to=20settings.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The exit-0 branch emitted permissionDecision:allow, making rtk's internal Rust registry a PARALLEL permission authority: a rewritten command bypassed settings.json deny/ask entirely (audit #11). Both rewrite paths now emit updatedInput only; the rewritten command goes through native evaluation. Companion allow rules for read-only 'rtk ' forms land in settings.json (audit-hardening branch) to keep the safe majority frictionless. Re-pinned. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016zA3Qh2Q1QpcGXzXxKeDHR --- hooks/.rtk-hook.sha256 | 2 +- hooks/rtk-rewrite.sh | 45 ++++++++++++++++++------------------------ 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/hooks/.rtk-hook.sha256 b/hooks/.rtk-hook.sha256 index 2305033..f908504 100644 --- a/hooks/.rtk-hook.sha256 +++ b/hooks/.rtk-hook.sha256 @@ -1 +1 @@ -0f43229d17d03342d27c0b836b9b70f25f98dfb80a35ffc8dc2488034cb8719c rtk-rewrite.sh +871efa28daf7c06a9c9039a2875407e2536646f5d82f7e7a9c6a80dd3742929c rtk-rewrite.sh diff --git a/hooks/rtk-rewrite.sh b/hooks/rtk-rewrite.sh index faaf089..21dc98e 100755 --- a/hooks/rtk-rewrite.sh +++ b/hooks/rtk-rewrite.sh @@ -12,7 +12,12 @@ # ANY edit here must re-pin: (cd hooks && sha256sum rtk-rewrite.sh > .rtk-hook.sha256) # # Exit code protocol for `rtk rewrite`: -# 0 + stdout Rewrite found, no deny/ask rule matched → auto-allow +# 0 + stdout Rewrite found, no rtk deny/ask rule matched → rewrite. NO +# permissionDecision is emitted (auto-allow dropped 2026-07-02: +# it made rtk's registry a parallel permission authority that +# bypassed settings.json deny/ask). The REWRITTEN command goes +# through native evaluation; explicit `rtk ` allow rules +# in settings.json keep read-only forms frictionless. # 1 No RTK equivalent → pass through unchanged # 2 Deny rule matched → pass through (Claude Code native deny handles it) # 3 + stdout Ask rule matched → rewrite but let Claude Code prompt the user @@ -66,8 +71,8 @@ EXIT_CODE=$? case $EXIT_CODE in 0) - # Rewrite found, no permission rules matched — safe to auto-allow. - # If the output is identical, the command was already using RTK. + # Rewrite found. If the output is identical, the command was + # already using RTK — nothing to do. [ "$CMD" = "$REWRITTEN" ] && exit 0 ;; 1) @@ -106,26 +111,14 @@ fi ORIGINAL_INPUT=$(echo "$INPUT" | jq -c '.tool_input') UPDATED_INPUT=$(echo "$ORIGINAL_INPUT" | jq --arg cmd "$REWRITTEN" '.command = $cmd') -if [ "$EXIT_CODE" -eq 3 ]; then - # Ask: rewrite the command, omit permissionDecision so Claude Code prompts. - jq -n \ - --argjson updated "$UPDATED_INPUT" \ - '{ - "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "updatedInput": $updated - } - }' -else - # Allow: rewrite the command and auto-allow. - jq -n \ - --argjson updated "$UPDATED_INPUT" \ - '{ - "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "permissionDecision": "allow", - "permissionDecisionReason": "RTK auto-rewrite", - "updatedInput": $updated - } - }' -fi +# Rewrite WITHOUT a permissionDecision (exit 0 and exit 3 alike): the +# rewritten command goes through Claude Code's native allow/deny/ask +# evaluation. Permission control lives in settings.json, not in rtk. +jq -n \ + --argjson updated "$UPDATED_INPUT" \ + '{ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "updatedInput": $updated + } + }'