feat(guardrails): push every commit, static deny for destructive tools, brief carries no user authority

Layer C of the plan written after the 2026-09-21 wipe (BDR-095): a reviewer
sub-agent traced `lftp mirror --delete` against a local file:// tree, the
prose tiers named neither lftp nor a local trace, the brief had authorized
it, and four days of commits had never left the machine.

- gitflow: `start` pushes the branch with its upstream, merge targets are
  pushed after each merge, and `init`/`install-hook` write post-commit and
  post-merge hooks that push every commit as it lands (warn, never block;
  GITFLOW_NO_PUSH=1 for throwaway repos). T18 + T19 (installed == emitted).
- hooks/unpushed-guard.sh on SessionStart and Stop: branch ahead of its
  upstream, no upstream, or no origin. Non-blocking systemMessage.
- settings.json: static deny for transfer and mirror tools, rsync --delete,
  xargs rm, pipe-to-shell, chmod/chown -R, sudo/doas/pkexec, disk tools,
  chattr, docker volume drops/prune/--privileged/socket/-v /:, git history
  destruction, --no-verify and core.hooksPath; new hard_deny "destructive
  tool against a local path, brief carries no user authority"; soft_deny
  reworded + discarding uncommitted work; environment records the incident.
- CLAUDE.global.md "Destructive tools & data loss"; the four report-only
  agents trace by reading, never by running, whatever the brief says.
- lib/tests/guard-bash.test.sh: executable spec of the PreToolUse guard
  (214 cases). The hook itself is not shipped (BLK-022); the spec skips.
This commit is contained in:
bastien
2026-09-22 07:43:12 +02:00
parent 475200bc83
commit 9da5d8d52c
16 changed files with 699 additions and 18 deletions
+34
View File
@@ -304,6 +304,40 @@ git add -A; git commit -q -m "chore + spec"
gitflow_finish >/dev/null 2>&1
chk "T17d chore leaves transient (not in scope)" '[ -n "$(git ls-tree -r develop --name-only -- docs/superpowers)" ]'
echo "T18 — auto-push: branch pushed at start, every commit pushed (BDR-095)"
newrepo pushsrc; echo a>a; hookon; gitflow_init >/dev/null 2>&1
bare="$WORK/pushsrc.git"; git init -q --bare "$bare"; git remote add origin "$bare"
git push -q origin main develop 2>/dev/null
gitflow_start feature ap >/dev/null 2>&1
chk "T18a start pushed the branch" 'git ls-remote --heads origin feature/ap | grep -q feature/ap'
echo w>w; git add w; git commit -q -m w 2>/dev/null
chk "T18b commit pushed by post-commit" '[ "$(git rev-parse HEAD)" = "$(git -C "$bare" rev-parse feature/ap)" ]'
echo w2>>w; git add w; GITFLOW_NO_PUSH=1 git commit -q -m w2 2>/dev/null
chk "T18c GITFLOW_NO_PUSH=1 → not pushed" '[ "$(git rev-parse HEAD)" != "$(git -C "$bare" rev-parse feature/ap)" ]'
git remote set-url origin /nonexistent/x.git
echo w3>>w; git add w
# shellcheck disable=SC2034 # ap_out/ap_rc are read by the deferred chk evals
ap_out="$(git commit -q -m w3 2>&1)"; ap_rc=$?
chk "T18d unreachable origin → commit still succeeds" "[ $ap_rc -eq 0 ]"
chk "T18e unreachable origin → loud warning" 'printf "%s" "$ap_out" | grep -q "FAILED"'
git remote set-url origin "$bare"
gitflow_finish >/dev/null 2>&1
chk "T18f finish pushed develop (merge commit)" '[ "$(git rev-parse develop)" = "$(git -C "$bare" rev-parse develop)" ]'
newrepo noremote; echo a>a; hookon; gitflow_init >/dev/null 2>&1
gitflow_start feature nr >/dev/null 2>&1; echo w>w; git add w
# shellcheck disable=SC2034
nr_out="$(git commit -q -m w 2>&1)"; nr_rc=$?
chk "T18g no origin → silent, commit ok" "[ $nr_rc -eq 0 ] && ! printf '%s' \"\$nr_out\" | grep -q FAILED"
echo "T19 — installed hooks == emitted hooks in the config repo (LRN-114 drift gate)"
if [ -d "$HERE/../.githooks" ]; then
chk "T19a pre-commit installed == emitted" 'diff -q <(_gitflow_emit_pre_commit) "$HERE/../.githooks/pre-commit" >/dev/null'
chk "T19b post-commit installed == emitted" 'diff -q <(_gitflow_emit_push_hook post-commit) "$HERE/../.githooks/post-commit" >/dev/null'
chk "T19c post-merge installed == emitted" 'diff -q <(_gitflow_emit_push_hook post-merge) "$HERE/../.githooks/post-merge" >/dev/null'
else
ok "T19 skipped (no .githooks next to the lib)"
fi
echo
echo "==== RESULT: $PASS passed, $FAIL failed ===="
[ "$FAIL" -eq 0 ]