Security-gate MEDIUM: a self-service floor-guard: allow <reason> neutralised
the detector in the same commit. User chose strict: the tool prints WAIVED,
the contract authorizes, the verifier counts the rest as gaps. BDR-102
amendment.
Fourteen lines of stack-agnostic micro-rules (dvh, safe-area, paste,
tabular-nums, text-wrap, z-index scale, compositor-only motion, 44 px
targets, focus-visible, status not by color alone, errors by the field,
one accent per view). Case 3 of the 6-repo review: nothing installed.
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.
Prose tightened section by section, blank lines after headings removed, the six classic Security subsections folded into one labelled list (Destructive tools & data loss kept as a heading), numbered lists collapsed, memory-registries and gitflow paragraphs re-flowed. Deliberately dropped: the release-candidate, audit-delta and init-project/onboard routing lines (name-obvious, BDR-031 criterion) and rationale clauses. Every ## heading verbatim; graphify section byte-identical so the pending feature branch merges clean. Words 2694 to 2302. BDR-098.
lib/graphify-gate.sh counts tracked code files (graphify's AST extension set, vendored trees excluded) and, from 200 with no graphify-out/graph.json, prints one banner-sized line; session-start shows it with the /graphify hint. Nothing is built, installed or updated: the rule is the user's (BDR-097), grounded in the LRN-162 measurements (AST build 2.3 s, 0 tokens, a query 2 to 3k tokens). Doctrine section and plugin-advisor thresholds follow the same rule; graphify claude install stays rejected. Test: 11 checks. GRAPHIFY_MIN_CODE_FILES overrides the threshold.
`gitflow_delete` now ends with `_gitflow_delete_remote`: after the local
copy is gone, the remote tip is read with `ls-remote --exit-code`, checked
against develop/main with the same ancestor test, and only then removed
with `push origin --delete`. Same contract as the pushes (BDR-095): best
effort, warn never fail. No origin, `GITFLOW_NO_PUSH=1` or
`gitflow.autopush false` skip it; an unreachable origin or a remote tip
holding commits the bases lack keeps the remote branch, loudly. A base is
never targeted, by construction and by an explicit guard.
The static deny on hand `git push --delete` stays: it matches the Bash
tool's command string, the lib is the sanctioned path. Prose (hard_deny,
environment), doctrine, gitflow SKILL (table, op, warning row),
SETTINGS.md and CHANGELOG updated. T24: 9 checks (finish removes the
copy, bases untouched, unmerged remote tip kept, never pushed silent,
unreachable origin loud, autopush opt-out). 161/163, the 2 failures are
the pre-existing T16a (gitleaks absent on this host).
Since BDR-095 `start` sets an auto-pushed upstream, so `git branch -d`
checked "merged into origin/<branch>" (always true, the post-commit hook
keeps it in sync) instead of "merged into develop". T22a proves it: an
unmerged feature with its upstream in sync is deleted by `-d` alone.
- `gitflow_delete` is the single delete path (finish + CLI `delete`):
refuses main/develop (rc 6) and any branch that is not an ancestor of
develop or main (rc 5, `gitflow_merged_into_base`, fail closed when
neither base exists), then `-d` as a second layer. CLI `merged`, `hooks`.
- Fourth generated hook `reference-transaction`: in the `prepared` call,
a deletion of refs/heads/main or refs/heads/develop exits 1, whatever
issued it (branch -d/-D, update-ref -d, rename, script, sub-agent).
`git config gitflow.protect false` opts a foreign clone out.
- `GITFLOW_HOOKS` is the one hook list: write/emit/reconcile, T19d and
doctor.sh (`gitflow.sh hooks`) read it. `.githooks/` and `githooks/`
regenerated with the fourth hook.
- settings.json: static deny on hand `git branch -d/--delete/-dr/-rd` and
on renames of main/develop; hard_deny "Branch deletion by hand"; the
Disarming entry covers all four hooks and `gitflow.*` config; the
protected-branches environment line states the rule.
- Doctrine (CLAUDE.global.md gitflow section), gitflow SKILL (`delete`
op, rc 5/6 rows, common mistake), guard-bash spec T8w flips to deny,
SETTINGS.md, README, CHANGELOG.
- Tests: T22 (12) lib guard incl. the premise proof, T23 (11) hook;
T19 covers the fourth hook. 152/154, the 2 failures are the
pre-existing T16a (gitleaks absent on this host).
Global: `make link` generates githooks/ from lib/gitflow.sh and sets git's
global core.hooksPath to ~/.claude/githooks, so every repo on the machine
runs the pre-commit protection and the post-commit / post-merge push, even
one that never ran gitflow init. A repo's own local core.hooksPath still
wins, so hooks/session-start.sh calls `gitflow reconcile-hooks` once per
session and rewrites a .githooks/ that lags the lib (LRN-114 automated);
the pre-commit exemption now covers .githooks/** next to .claude/**.
Per-repo opt-outs for a foreign clone: `git config gitflow.protect false`
(branch model) and `git config gitflow.autopush false` (push). Both, and
the GIT_CONFIG_GLOBAL= / GIT_CONFIG= env bypass, are static deny rules.
`make test` and the two suites that commit on main export
GIT_CONFIG_GLOBAL=/dev/null so the machine's global hooks never fire in
throwaway repos. doctor gains "Git hooks" (global setting, githooks/ equal
to the emitters) and "Scratchpad" (warn when TMPDIR sits on a tmpfs with
usrquota: systemd caps each user at 80% of it, which killed two shells
today, BLK-021). Tests: T18h, T19d, T20 (reconcile), T21 (whitelist and
protect opt-out); this repo's own stale .githooks/ refreshed.
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.
make plugin never installed impeccable. The 3.2.0 pin had rotted upstream
(the CLI fetches its skill dist at install time; that release's zip is
gone), the --scope=project staging moved the skill dir alone and dropped
the 4 impeccable-* subagents, and /impeccable init was never announced.
Step 8d now installs at --scope=global straight through the
~/.claude/{skills,agents} symlinks into the repo (both paths gitignored),
guards on those symlinks existing, keeps a profile-parked copy parked,
falls back to @latest on a pin failure with a bump-the-lock warning, and
prints the per-project init hint. update-all.sh mirrors the shape.
Found while probing: with a copy already installed a rotted pin exits 0
("Could not check for skill updates ... left unchanged"), byte-identical
on disk to an up-to-date rerun, so imp_install reads the installer output
instead of trusting the exit code. Harness 4/4 in a sandbox HOME with the
real installer.
plugins.lock.json: impeccable 3.2.0 -> 4.1.0 (CLI only). link.sh drops
impeccable from EXTERNAL_SKILLS. lib/design-gate.md section 5: suggest-only
/impeccable init check when a frontend project has no PRODUCT.md.
Upstream supersedes `@21st-dev/magic` with `@21st-dev/cli` (bin `21st`):
same endpoint, `21st login` in place of an API key, no MCP process loaded
into every session.
- install-plugins.sh Step 8.7: `npm i -g @21st-dev/cli` (pinned in
plugins.lock.json), staged `21st skills install`, TTY-only login offer,
pack disabled by default. update-all.sh 7.4 refreshes both.
- The documented `21st install-skill` cannot be used: the installer refuses
to follow a symlink on the target path and `~/.claude/skills` is one. The
install runs under a throwaway HOME and the result moves into
skills-external/21st-* (gitignored), symlinked on demand.
- toggle-external.sh manages `21st` as a pack (names globbed from
skills-external/21st-*, parked under plain names). `magic` is gone.
- The 5 design skills join design/web/web-full/full and MANAGED_EXTERNALS;
21st-registry and 21st-design-sync stay parked. MANAGED_MCPS is now empty
and profile.sh's dead magic branches are removed.
- Design gate: GATE-BLOCK gains `21st` (required-manual, magic's old slot)
and `21st-ui-build`; PATH repair extended to the npm global bin.
- settings.json: the 4 mcp__magic__* ask entries go; the outward-facing
21st verbs land in autoMode.soft_deny, the tier that holds under auto
mode (LRN-153).
- Docs: README, CLAUDE.global.md, design-gate.md, profile SKILL.md,
.env.example, .gitleaks.toml, link.sh. BDR-093, LRN-158.
Tests: profile-set-managed 17/17, make test green except 2 pre-existing
gitflow FAILs (gitleaks binary absent on this host), shellcheck clean.
Every checklist command is emitted on exactly one line, however long; a
legacy backslash continuation in the runbook is joined at instantiation,
and bootstrap / learn patches write runbook lines the same way. After the
checklist the hand-back carries a Post-deploy tests block derived from the
delta diff: by-hand checks tied to delta files plus Suggestions for gaps.
Cold-resume re-display and re-hand-back regenerate both.
RED/GREEN on a scratch runbook: 4/4 baseline runs reproduced the
continuation verbatim and printed no tests; 4/4 runs on the edited skill
joined it and printed the block in the recipe's shape.
`permissions.ask` gates nothing under `defaultMode: auto` (LRN-146,
verified live), so the ten rules that left the static tiers had no cover
left: rsync / kill -9 / killall / pkill out of deny, and python3 -c /
python -c / xargs / sed / cp / mv out of ask.
autoMode.soft_deny (7 rules) takes over what an explicit instruction
should be able to clear: writes outside the working directory,
rsync --delete, SIGKILL and kill-by-name, in-place edits spanning more
than one file, directory moves, and inline interpreters or xargs that
delete or write outside the cwd. Intent clears a soft block for the
current turn only, stated as a rule since no setting expresses it.
autoMode.hard_deny (3 rules) takes the classes no command pattern can
express: secret exfiltration, production deployment, and disarming the
guardrails. Adding a restriction stays allowed, removing one does not.
permissions.deny gains ten .env reader rules (sed awk cut tr sort uniq
diff od xxd strings). Six of those tools sat in permissions.allow, so
reading a .env through them triggered nothing.
autoMode.environment named another project, its FTP deploy target and its
customer data, inside the file link.sh:21 symlinks to
~/.claude/settings.json, where it reached every repo and contradicted
this one's Gitea remote. Rewritten machine-generic; the project facts
moved to that project's gitignored .claude/settings.local.json. All three
lists now open with "$defaults", which the original omitted, so the
built-in classifier entries are inherited rather than replaced.
doctor.sh check_automode backstops both defects. SETTINGS.md documents
the block and a tier-choice table. README no longer claims the ask tier
makes every mcp__magic__* call require a live confirmation.
Two or more project paths dispatch one general-purpose runner per repo,
all in a single message, instead of processing repos one by one. The
runner inherits the session model — no pin, it carries tour's reflection
(fix decisions, convergence) — and every agent inside keeps its defined
tier (security-auditor sonnet, Phase B opus, doc-syncer sonnet). A dead
or mute runner becomes an explicit RUNNER FAILED summary row; the gated
capitalize offer stays in the main loop, never in a runner.
Bounded LRN-083 derogation recorded in BDR-084: the per-project fix loop
moves into its runner, but nothing a runner decides touches shared state
— independent repos, per-repo chore branches, branches left unmerged for
human review exactly as inline. Mechanics proven before building: nested
probe, 3 sub-agent windows all overlapping, 9.1s vs ~18s sequential.
Census §12: 6 locks, flip-tested. Single-project path unchanged.
The include is authoritative, but feat/bugfix/ship-feature/init-project
each restate the verify loop inline — an orchestrator following the
restatement alone would have skipped the floor. Each now carries the
GATE 0 bullet ahead of GATE 1 (4 new structure locks, flip-tested).
The contract-interview weight table stops promising a hotfix oracle
nothing executes: hotfix runs no floor, the hotfixer runs the suite
itself. CHANGELOG extended with the wiring + the RED result.
BDR-083 records what was taken from unlazy and, more usefully, what was
refused and why. LRN-141: an external skill's machinery encodes its threat
model, not yours — take the invariants, refuse the machinery. LRN-142:
structure locks are fixed-string, so reflowing a doctrine paragraph reds
them; fix the doc, not the lock.
- MANAGED_EXTERNALS (emil-design-eng, frontend-design,
design-motion-principles, impeccable) + MANAGED_MCPS (magic):
cmd_set now trims both when the profile does not list them —
design leftovers no longer survive a 'set backend'
- cmd_set refactored to 4 symmetric trim helpers; nothing outside
the MANAGED_* allowlists is ever auto-toggled (darwin-skill manual)
- enable_skill external: from-source fallback (ln -sf
skills-external/<name>), mirrors toggle-external.sh
- stale usage() NOTE + SKILL.md updated to the both-ways reality
- hermetic test: 16 checks, fixture repo + fake claude shim (gstack
on-demand, from-source, park/restore, magic add/remove, non-managed
untouched); shellcheck + full make test green
RC fix (soak day 1): `make update` step 7.5 failed on a 300s clone timeout
of alchaincyf/find-skills. The skill (search the skills.sh registry from
Claude) was never used; the discovery case stays reachable manually via
`npx -y skills find <query>`. Removed from install-plugins.sh (install list
+ summary + comment), update-all.sh (refresh list), link.sh
(NPX_EXTERNAL_SKILLS), lib/toggle-external.sh (MANAGED_TOOLS + case arms),
plugin-advisor.md, .gitignore; local skills/find-skills symlink deleted.
Memory-registry and test-fixture mentions kept — append-only history.
toggle-external `list` verified post-removal; suites 8/8.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpphkdTosUzokBDNG7PToS