boma/.claude/hooks/guard-vault-preflight.sh
sjat 39904a778a fix(hooks): scope vault-preflight to staged ansible; catch prose exec re-asks
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) <noreply@anthropic.com>
2026-06-17 17:49:55 +02:00

50 lines
2.5 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# 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: 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
input=$(cat 2>/dev/null) || exit 0
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null) || exit 0
case "$cmd" in
*"git commit"*) : ;; # a git commit — check further
*) exit 0 ;; # not a commit — allow
esac
case "$cmd" in
*"--no-verify"*) exit 0 ;; # hooks skipped anyway — allow
esac
command -v rbw >/dev/null 2>&1 || exit 0 # rbw not installed — allow
rbw unlocked >/dev/null 2>&1 && exit 0 # unlocked — allow
# 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$'
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 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