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:
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# hooks/unpushed-guard.sh — SessionStart + Stop: surface work that exists on
|
||||
# this disk only (BDR-095). The 21/09 wipe cost four days of commits that had
|
||||
# never left the machine; the post-commit hook now pushes every commit, so a
|
||||
# branch ahead of its upstream is a real signal (push refused, offline, hook
|
||||
# not installed), not noise.
|
||||
#
|
||||
# Non-blocking by contract: a systemMessage for the user, never a decision.
|
||||
# SessionStart also reports uncommitted changes (a dead session leaves some
|
||||
# behind); Stop reports unpushed commits only, since a dirty tree mid-work is
|
||||
# the normal state at a turn end.
|
||||
set -u
|
||||
|
||||
payload=$(cat 2>/dev/null)
|
||||
field() { printf '%s' "$payload" | jq -r "$1 // empty" 2>/dev/null; }
|
||||
event=$(field '.hook_event_name')
|
||||
cwd=$(field '.cwd'); [ -n "$cwd" ] || cwd=$PWD
|
||||
cd "$cwd" 2>/dev/null || exit 0
|
||||
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
|
||||
br=$(git symbolic-ref --short -q HEAD 2>/dev/null) || exit 0
|
||||
|
||||
# Commits that no remote holds, as one clause; empty when everything is pushed.
|
||||
unpushed_clause() {
|
||||
local up n
|
||||
if ! git remote get-url origin >/dev/null 2>&1; then
|
||||
echo "no 'origin' remote, every commit lives on this disk only"
|
||||
return
|
||||
fi
|
||||
if up=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null); then
|
||||
n=$(git rev-list --count "$up..HEAD" 2>/dev/null || echo 0)
|
||||
[ "$n" -gt 0 ] && echo "$n commit(s) on '$br' not on $up, push: git push"
|
||||
else
|
||||
# commits no remote-tracking ref holds: the ones only this disk has
|
||||
n=$(git rev-list --count HEAD --not --remotes 2>/dev/null || echo 0)
|
||||
echo "'$br' has no upstream ($n commit(s) on this disk only), push: git push -u origin $br"
|
||||
fi
|
||||
}
|
||||
|
||||
msg=$(unpushed_clause)
|
||||
if [ "$event" = "SessionStart" ]; then
|
||||
dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
||||
[ "$dirty" -gt 0 ] && msg="${msg:+$msg; }$dirty uncommitted change(s) in $cwd"
|
||||
fi
|
||||
[ -n "$msg" ] || exit 0
|
||||
|
||||
msg="⚠ unpushed work: $msg"
|
||||
if [ "$event" = "SessionStart" ]; then
|
||||
jq -cn --arg m "$msg" \
|
||||
'{systemMessage: $m, hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: $m}}'
|
||||
else
|
||||
jq -cn --arg m "$msg" '{systemMessage: $m}'
|
||||
fi
|
||||
Reference in New Issue
Block a user