From 39904a778a9be720a71875795945bc8f06642fa4 Mon Sep 17 00:00:00 2001 From: sjat Date: Wed, 17 Jun 2026 17:49:55 +0200 Subject: [PATCH] fix(hooks): scope vault-preflight to staged ansible; catch prose exec re-asks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit guard-vault-preflight: block a locked 'git commit' only when the staged set (git diff --cached, plus -a/--all) contains ansible content matching the pre-commit ansible-lint hook's files: scope. Docs-/config-only commits never trigger that hook, so they no longer need the vault — fixing the false block on docs-only commits. Fails safe to block when unsure. guard-execution-mode-menu: widen the execution-mode arm to also catch free-form prose re-asks of the subagent-vs-inline choice ('which execution approach?', 'subagent vs inline', ...), which the literal-menu matcher missed; the push re-ask is intentionally left to the dont-reask-settled-defaults memory. Consumes two 2026-06-17 signals in docs/FRICTION.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/hooks/guard-execution-mode-menu.sh | 13 ++++++-- .claude/hooks/guard-vault-preflight.sh | 39 +++++++++++++++------- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/.claude/hooks/guard-execution-mode-menu.sh b/.claude/hooks/guard-execution-mode-menu.sh index cc1d0ae..2fc9d09 100755 --- a/.claude/hooks/guard-execution-mode-menu.sh +++ b/.claude/hooks/guard-execution-mode-menu.sh @@ -6,7 +6,12 @@ # 1. The execution-mode menu — writing-plans / subagent-driven-development script a # "Subagent-Driven vs Inline Execution — which approach?" menu at the plan→execution # handoff. boma's standing preference is to NEVER present it and proceed -# subagent-driven. (Recorded by the 2026-06-10 kaizen review.) +# subagent-driven. (Recorded by the 2026-06-10 kaizen review; the 2026-06-17 review +# widened the matcher to also catch free-form *prose* re-asks of the same choice — +# e.g. "which execution approach?" — which the literal-menu matcher missed. The +# sibling push-vs-not re-ask is deliberately NOT hooked: a genuine "should I push?" +# is sometimes legitimate, so it stays a soft default via the +# dont-reask-settled-defaults memory rather than a hard block.) # 2. The brainstorming spec-review gate — the brainstorming skill scripts "Spec written # and committed … please review it before … the implementation plan." The standing # agreement is to move directly from the committed spec to writing-plans. (Recorded @@ -39,7 +44,11 @@ text=$(jq -rs ' low="${text,,}" if [[ "$low" == *"inline execution"* \ - && ( "$low" == *"which approach"* || "$low" == *"two execution options"* ) ]]; then + && ( "$low" == *"which approach"* || "$low" == *"two execution options"* ) ]] \ + || [[ "$low" == *"subagent-driven or inline"* || "$low" == *"inline or subagent"* ]] \ + || [[ "$low" == *"subagent-driven vs inline"* || "$low" == *"subagent vs inline"* \ + || "$low" == *"inline vs subagent"* ]] \ + || [[ "$low" == *"execution approach"* && "$low" == *"?"* ]]; then cat <<'JSON' {"decision":"block","reason":"Execution-mode menu detected in your final message. boma standing preference (docs/FRICTION.md + always-subagent-driven-execution memory): never present the subagent-driven-vs-inline menu. Drop the menu and proceed with subagent-driven execution directly (superpowers:subagent-driven-development)."} JSON diff --git a/.claude/hooks/guard-vault-preflight.sh b/.claude/hooks/guard-vault-preflight.sh index d664bbf..7e565b1 100755 --- a/.claude/hooks/guard-vault-preflight.sh +++ b/.claude/hooks/guard-vault-preflight.sh @@ -1,12 +1,16 @@ #!/usr/bin/env bash # -# PreToolUse guard (Bash): block `git commit` when the rbw vault agent is locked. -# The pre-commit ansible-lint hook decrypts vault.yml via rbw, so a commit while -# locked fails deep with a confusing error. This catches it early with a clear fix. +# PreToolUse guard (Bash): block `git commit` ONLY when the rbw vault agent is locked +# AND the commit would actually need the vault. The pre-commit ansible-lint hook decrypts +# vault.yml via rbw — but it is scoped (`files: ^(roles|playbooks|inventories)/.*\.ya?ml$`, +# always_run:false), so a docs-/config-only commit never triggers it and needs no vault. +# (2026-06-17 kaizen, docs/FRICTION.md: the old guard blocked *every* locked commit, so a +# docs-only commit got snagged needing a vault password it never uses.) # -# Fails OPEN: only blocks on a definitive "rbw present AND not unlocked" signal. -# If rbw is missing, the command isn't a plain `git commit`, or `--no-verify` is -# used, the action is allowed. +# Fails OPEN: blocks only on a definitive "Ansible content staged AND rbw locked" signal. +# rbw missing, not a plain `git commit`, `--no-verify`, or no Ansible content staged → allow. +# When unsure it errs toward blocking (asking for an unlock is cheap; a deep pre-commit +# failure is not). # set -uo pipefail @@ -22,14 +26,25 @@ case "$cmd" in esac command -v rbw >/dev/null 2>&1 || exit 0 # rbw not installed — allow +rbw unlocked >/dev/null 2>&1 && exit 0 # unlocked — allow -if rbw unlocked >/dev/null 2>&1; then - exit 0 # unlocked — allow -fi +# rbw is LOCKED. Only block if this commit would run the vault-decrypting ansible-lint +# hook — i.e. staged content matches its `files:` scope. Mirror that regex exactly. +ANSIBLE_RE='^(roles|playbooks|inventories)/.*\.ya?ml$' -# rbw present but not unlocked (locked or agent not running) — the commit would -# fail in the pre-commit hook, so block early with guidance. +cd "${CLAUDE_PROJECT_DIR:-.}" 2>/dev/null || exit 0 +files=$(git diff --cached --name-only 2>/dev/null) || exit 0 +# `git commit -a/--all` also sweeps in modified tracked files that aren't staged yet. +# (Substring match — errs toward including them, which only ever over-blocks. Safe.) +case " $cmd " in + *" -a"*|*"--all"*) files="$files"$'\n'"$(git diff --name-only 2>/dev/null)" ;; +esac + +# No Ansible content in the fileset → ansible-lint hook won't run → no vault needed → allow. +printf '%s\n' "$files" | grep -Eq "$ANSIBLE_RE" || exit 0 + +# Ansible content staged AND rbw locked — the commit would fail deep in pre-commit. Block. cat <<'JSON' -{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"rbw is locked — the pre-commit ansible-lint hook needs the vault password to decrypt vault.yml. Run: rbw unlock"}} +{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"rbw is locked and this commit stages Ansible content — the pre-commit ansible-lint hook needs the vault password to decrypt vault.yml. Run: rbw unlock (docs-/config-only commits are exempt and won't hit this guard.)"}} JSON exit 0