Add PreToolUse guard hooks: generated-file + rbw vault pre-flight

Two project hooks (deny-only, fail open): block Write/Edit of generated
inventories/<env>/hosts.yml, and block git commit when the rbw vault agent is
locked. Both pipe-tested across all paths. Activate with a Claude Code restart
(the watcher only tracks settings.json present at session start).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
sjat 2026-05-30 22:14:40 +02:00
parent 11af84938d
commit 80bf9afea9
3 changed files with 89 additions and 0 deletions

View file

@ -0,0 +1,25 @@
#!/usr/bin/env bash
#
# PreToolUse guard (Write|Edit): block edits to generated inventory files.
# `inventories/<env>/hosts.yml` is produced by tf_to_inventory.py — editing it by
# hand is overwritten on the next `make tf-inventory`. The git pre-commit hooks do
# NOT catch this, so we enforce it here.
#
# Fails OPEN: any parsing/other error allows the action (never wedge tool use).
#
set -uo pipefail
input=$(cat 2>/dev/null) || exit 0
file=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty' 2>/dev/null) || exit 0
[ -n "$file" ] || exit 0
case "$file" in
*/inventories/*/hosts.yml | inventories/*/hosts.yml)
cat <<'JSON'
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"inventories/<env>/hosts.yml is GENERATED by tf_to_inventory.py. Edit terraform/environments/<env>/main.tf (local.vms) and run `make tf-inventory`. The control node is the documented manual exception (docs/runbooks/new-host.md)."}}
JSON
exit 0
;;
esac
exit 0

View file

@ -0,0 +1,35 @@
#!/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.
#
# 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.
#
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
if rbw unlocked >/dev/null 2>&1; then
exit 0 # unlocked — allow
fi
# rbw present but not unlocked (locked or agent not running) — the commit would
# fail in the pre-commit hook, so block early with guidance.
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"}}
JSON
exit 0

29
.claude/settings.json Normal file
View file

@ -0,0 +1,29 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/guard-generated-files.sh\"",
"timeout": 10,
"statusMessage": "Checking generated-file guard"
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/guard-vault-preflight.sh\"",
"timeout": 10,
"statusMessage": "Checking rbw vault pre-flight"
}
]
}
]
}
}