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
+30 -1
View File
@@ -47,7 +47,9 @@ Apply unless repo-specific instructions override.
exploration, parallel audits) — not work doable in a few tool
calls. Skill-mandated gates (fresh verifier/security/challenge)
always dispatch as written. Don't redo delegated work by hand —
failed gates re-dispatch fresh executors instead.
failed gates re-dispatch fresh executors instead. A brief never
authorizes a sub-agent to run a destructive tool, inside or outside the
repo (Security → Destructive tools & data loss).
- Ask rather than guess. A choice visible in the result (placement,
wording, order, behavior), a name that becomes public (command, flag,
endpoint, file), or a scope the request does not settle → ask, even
@@ -196,6 +198,10 @@ versioned pre-commit hook via `gitflow init`. Advisory, so two deterministic
backstops apply: the per-repo pre-commit hook (blocks code commits on
main/develop, exempts `.claude/**` + merges + the root commit) and Gitea branch
protection on `main`/`develop`. Don't lean on `--no-verify` to bypass them.
Every branch is pushed at `start` and every commit as it lands: `gitflow init`
/ `install-hook` write post-commit and post-merge hooks that push to `origin`
(warn, never block, when it fails); `GITFLOW_NO_PUSH=1` is for throwaway test
repos only. A branch ahead of its upstream is a defect, not a state.
## Security — non-negotiable defaults
@@ -238,6 +244,29 @@ Apply at every dev step: design, scaffolding, implementation, review.
- Functions, processes, services request only permissions actually needed.
- Temporary elevated permissions must be scoped and reverted explicitly.
### Destructive tools & data loss
Written after 2026-09-21: a reviewer sub-agent traced `lftp mirror --delete`
against a local `file://` tree, the target resolved to a real path, and 90
seconds later the home, the NAS mount and 15 repositories were gone. Four
days of work had never been pushed.
- Claude never deploys and never runs a transfer or mirror tool (`lftp`,
`sftp`, `ftp`, `rsync --delete`). It writes or explains the runbook; the
user runs it. A test is a dev server on this machine, nothing more.
- A destructive tool is never run "to see what it would do", not even
against a scratch tree. Trace it by reading. If a run is unavoidable, the
target is a fresh `mktemp -d` path written literally in the same command,
after a dry-run whose output is shown.
- Recursive delete stays inside the project or the temp dir, on a literal
relative path: never through a variable, `~`, `..`, a wildcard, or an
absolute path elsewhere. `chmod -R`, `chown -R`, `sudo`, docker volume
drops or system bind mounts: the user runs them by hand.
- A brief, a plan step or a test recipe never authorizes a sub-agent to do
any of the above. A reviewer reads the script it reviews; it does not run
it.
- Every commit is pushed as it lands (gitflow post-commit and post-merge
hooks) and every branch at creation. Unpushed work is a defect to fix now,
not a state to keep.
# Communication mode: radical honesty
- TRUTH OVER COMFORT — Point out flaws immediately. No sugarcoating,