Root causes of the 2026-09-24 errors turned into mechanisms (BDR-100). hard_deny 'Routing around a guardrail': a refused command is never rerun through a wrapper, alias, heredoc, Makefile target, env file, other shell or other agent; the same clause in 14 agents and in the doctrine's sub-agent rule. make test suite=<file> runs one suite hermetically so the denied env-prefix form is never needed by hand. lib/tests/doctrine-citers.test.sh: every CLAUDE.md "Section" / § Label citation across skills, agents, lib, rules and hooks must resolve to a heading or bold label (flip-tested); its first run fixed rest-api-node.md. Doctrine 'After code changes' step 4: a changed rule, heading, label or threshold → grep every citer in the same commit.
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.
Both line-3 descriptions contained an unquoted ': ' (and '|') that fails
python3 yaml.safe_load ('mapping values are not allowed here'). seo-analyzer's
line was last rewritten by job9/a5a7b54 AFTER job2's F7 strict-YAML rule
(git blame); security-auditor's dates to job6. Single-quote wrap, no internal
apostrophes. Gate: yaml.safe_load over ALL agents/*.md now clean. Review A4.
- README + plugins.lock.json graphifyy note: pipx/PyPI install only, never
npm/npx — a different publisher (rhanka/graphify) squats the same
'graphifyy' name on npm as a version-shadowing shim with its own
conflicting 'graphify' bin (F-X1).
- agents/security-auditor.md: one-line caveat that p/* semgrep packs are
fetched from the registry at runtime — the CLI version pin does not
freeze ruleset content, so a new BLOCK can appear on unchanged code.
MCP magic (F-X3): version pin declined by user call (stays @latest in
lib/toggle-external.sh). ${VAR} env expansion confirmed unsupported at
~/.claude.json user scope (Claude Code docs — expansion is .mcp.json
project-scope only), so the BDR-026 reference-not-plaintext pattern
doesn't transfer here; existing mitigations (canonical ~/.claude/.env,
gitignore, audit env-field filtering) remain the practical ceiling.
~/.claude.json regenerated out-of-repo via toggle-external.sh disable+
enable magic to pick up the already-rotated MAGIC_API_KEY (no repo diff,
no commit for that file — traced in the job6 final report).
agents/security-auditor.md: fresh read-only-on-code SAST gate. Pinned
rulesets p/security-audit + p/secrets + p/owasp-top-ten (owasp REQUIRED —
measured: the 2-ruleset baseline misses SQLi + path-traversal entirely on
realistic Flask code), never --config auto, never auto login (BDR-048).
Severity map: secrets ERROR → CRITICAL, other ERROR → HIGH (block),
WARNING/INFO → reported. gate mode (diff, no Write) vs audit mode (Write
only to REPORT, rule-locked). DEGRADED (semgrep absent) still runs the
checklist and still blocks — never a vacuous pass (LRN-048). Anti-gaming:
a new un-gated nosemgrep suppression is BLOCKING. PROOF mandatory, mute
auditor never a PASS, blind (no iteration history), blocks HIGH/CRITICAL
only (LRN-047).
Grafts: onboard STEP 6 L3a dispatches it in audit mode (report
.onboard-audit/semgrep.md) in BOTH gstack branches — complement to cso
(cso is a gstack submodule, unmodifiable); synthesis picks it up via the
existing .onboard-audit/ sweep. audit-delta security axis runs the SAST
pass first, folds findings into the existing gate/fix/re-verify flow.
lib/tests/security-auditor.test.sh: 28 structure locks green, shellcheck
clean. Behavioral dogfood (fresh agents on a planted fixture):
BLOCK(9) on the vuln commit (2 secrets→CRITICAL, semgrep+checklist
complementarity — checklist caught the 6 semgrep missed off-context);
BLOCK(1) on a new nosemgrep suppression (understood semgrep's 0 was the
mask); DEGRADED → BLOCK(7) on grep-detectable secrets with semgrep hidden.
FP measured on real repos (faunosteo, game): owasp adds only hygiene
findings, contained by diff-scoping.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpphkdTosUzokBDNG7PToS