Commit Graph
10 Commits
Author SHA1 Message Date
bastien 32d8f981df feat(gitflow): delete a branch only after a verified merge, main/develop undeletable
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).
2026-09-24 11:35:01 +02:00
bastien f608d34c3e feat(gitflow): hooks in every repo, no per-project step
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.
2026-09-22 16:34:27 +02:00
bastien 9da5d8d52c 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.
2026-09-22 07:43:12 +02:00
Bastien Chanot 5eccc3f1c4 feat(automode): docker and node framed by the classifier, ask rules retired 2026-09-16 22:03:23 +02:00
Bastien Chanot 3b0167c6cb feat(settings): rebuild destructive-command cover in autoMode, scope the classifier environment
`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.
2026-09-15 19:44:24 +02:00
Bastien Chanot 07ca738b3f fix(settings): Write() deny rules inert — convert to Edit(), close write gaps
Startup emitted 15 warnings: "Write(**/.env) is not matched by file
permission checks — only Edit(path) rules are."

Write(path) rules never matched. The 5 secret-file write bans were dead
config — .env, secrets/**, *.pem, *.key were freely writable. Converting
to Edit() makes them enforced: permissions.md:242 "Edit rules apply to all
built-in tools that edit files", and :244 prescribes exactly this ("add an
Edit deny rule for paths no tool may change").

- settings.json: Write(...) -> Edit(...) on the 5 patterns.
- Mirror the 9 secret patterns Read denied but Edit did not: *.p12, *.pfx,
  id_rsa*, id_ed25519*, .ssh/**, credentials, credentials.json,
  .aws/credentials, .azure/**. Read/Edit parity now 14/14. Claude could
  previously overwrite an SSH private key or ~/.aws/credentials.
- New read-allowed/write-denied class: lockfiles (*.lock,
  package-lock.json, pnpm-lock.yaml, go.sum) + node_modules/**. Reading
  aids diagnosis; hand-editing is always wrong — the package manager
  regenerates them via Bash, which Edit deny does not block.
- templates/settings/SETTINGS.md taught the broken Write() pattern; fixed
  at the source so /onboard stops propagating it.

Rule syntax has no negation and deny beats allow, so deny globs cannot
carry exceptions — see the .env.example conflict noted in the follow-up.
2026-07-16 14:45:26 +02:00
Bastien Chanot af4f5cc6a4 job5: J5-15 delete orphan settings.local.json template (content recoverable at a145e3c) 2026-07-07 00:50:29 +02:00
Bastien Chanot e42a77cb1b job3: R12 SETTINGS.md — document auto mode (BDR-004, this config's default) 2026-07-06 17:00:03 +02:00
bchanot f55a2b3fdf final version seems 2026-04-08 13:46:45 +02:00
bastien a145e3cc03 added settings 2026-04-02 15:59:00 +02:00