diff --git a/.claude/memory/decisions.md b/.claude/memory/decisions.md
index 119a4c9..356e6c1 100644
--- a/.claude/memory/decisions.md
+++ b/.claude/memory/decisions.md
@@ -1213,3 +1213,4 @@ Branch feature/user-writing-web-rules, UNMERGED (human gate).
- **Alternatives rejected**: hook also refusing UNMERGED deletion → files backend rename = delete + create in separate transactions, `branch -d` passes zeros as old oid → merged check impossible/false positives on `branch -m`, user-shell friction; lib + deny carry that rule. Remote cleanup after finish (`push --delete origin/
`) → in static deny since BDR-095, not requested; origin/
accumulates, flagged to user. `-D` in the lib → no, `-d` stays as defense in depth. Interactive brainstorm → user absent (autonomous run), request unambiguous; trade-off (hook blast radius) stated in the report instead.
- **Status**: accepted, on feature/branch-delete-guard, UNMERGED (human gate). gitflow-test 152/154 (2 pre-existing T16a, gitleaks absent), T22 12/12 + T23 11/11, shellcheck clean incl. emitted hook, doctor 4/4 hooks match. Hook LIVE machine-wide via global `githooks/` while the branch is checked out (symlink follows the checkout).
- **Reference**: `lib/gitflow.sh`, `lib/gitflow-test.sh` T22/T23, `githooks/reference-transaction`, `.githooks/reference-transaction`, `settings.json`, `doctor.sh`, `skills/gitflow/SKILL.md`, `CLAUDE.global.md`, `templates/settings/SETTINGS.md`. Extends [[BDR-095]]; links [[LRN-161]], [[LRN-114]].
+- **Amendment 2026-09-24 (user go: "nettoie aussi les branches distantes une fois mergées")**: `_gitflow_delete_remote` runs after the local delete — `ls-remote --exit-code` reads the remote tip, `gitflow_merged_into_base ` re-checks it (unknown or unmerged sha → remote copy KEPT, loud), then `push origin --delete`. Best effort like the pushes: no origin / `GITFLOW_NO_PUSH=1` / `gitflow.autopush false` → skip; unreachable or refused → "NOT removed" + the hand command, rc 0. Explicit protected-base guard inside the helper too. Static deny on hand `git push --delete` UNCHANGED: it matches the Bash tool's command string, the lib's sub-process is the sanctioned path (prose says so). Rejected: making a failed remote delete fail `finish` (merge done, local gone → nothing to roll back; loud is enough); deleting without re-checking the remote tip (a push from another clone would be lost). T24 9/9, suite 161/163 (2 pre-existing T16a). Live: origin/feature/branch-delete-guard + origin/feature/destructive-guardrails removed by `gitflow.sh delete` (both tips verified merged), bases untouched. On feature/remote-branch-cleanup, UNMERGED (human gate).
diff --git a/.claude/memory/journal.md b/.claude/memory/journal.md
index b6eea8c..3eec46d 100644
--- a/.claude/memory/journal.md
+++ b/.claude/memory/journal.md
@@ -497,3 +497,4 @@ rules:
- Shipped [[BDR-096]] on feature/branch-delete-guard: `gitflow_delete` (rc 5 unmerged / rc 6 protected; CLI `delete` `merged` `hooks`), 4th hook `reference-transaction` vetoing delete/rename of main/develop (live via global `githooks/`), `GITFLOW_HOOKS` single list, static deny on hand `branch -d/--delete` + base renames, hard_deny entry, doctrine + SKILL + docs. 152/154 (2 pre-existing T16a), doctor 4/4, shellcheck clean. UNMERGED — human gate.
- Inline probes denied 4× by the guardrails themselves (deny strings in command text) → probe = test file, content via Write. Open for the user: `origin/
` accumulates after finish (`push --delete` denied), CLAUDE.global.md 352L (>320 budget), user's `feedbackDrafts` settings line left uncommitted on purpose.
- feature/branch-delete-guard merged into develop on user go, `gitflow finish` → b2e252e, pushed by the lib + post-merge hook (develop == origin/develop, no hand push). First live run of `gitflow_delete`: branch verified merged → deleted. User asked "push automatically after every merge": already the case since [[BDR-095]] (`_gitflow_merge_into` pushes the target, post-merge hook, T18f) — evidenced, nothing added. Remote `origin/feature/{branch-delete-guard,destructive-guardrails}` remain (`push --delete` denied) — user's call.
+- User go: remote copy cleaned too. `_gitflow_delete_remote` (tip re-checked against the bases before `push --delete`, best effort, loud KEPT/NOT removed), T24 9 checks, prose + doctrine + SKILL + docs. 161/163. Live run through the lib on the two stale merged remotes: origin/feature/branch-delete-guard + origin/feature/destructive-guardrails removed by `gitflow.sh delete` (both tips verified merged), bases untouched. Branch feature/remote-branch-cleanup UNMERGED — human gate. BDR-096 amended.
diff --git a/.claude/tasks/TODO.md b/.claude/tasks/TODO.md
index d472793..c8ec826 100644
--- a/.claude/tasks/TODO.md
+++ b/.claude/tasks/TODO.md
@@ -38,8 +38,11 @@ sync via post-commit) instead of "merged into HEAD" — its safety valve is dead
T22 12/12 + T23 11/11, shellcheck clean incl. emitted hook, doctor
4/4 hooks match. Merged into develop b2e252e (user go 2026-09-24).
BDR-096, LRN-161.
-Out of scope, flagged: remote branch cleanup after finish (`git push --delete`
-is in static deny since BDR-095; origin/
now accumulates — user's call).
+- [x] D8 (user go 2026-09-24, feature/remote-branch-cleanup) `_gitflow_delete_remote`:
+ after the local delete, remote tip read + re-checked against develop/main,
+ then `push origin --delete`; best effort (skip: no origin / NO_PUSH /
+ autopush=false; loud: unreachable, unmerged remote tip). T24 9/9, 161/163.
+ Live on the 2 stale merged remotes: origin/feature/branch-delete-guard + origin/feature/destructive-guardrails removed by `gitflow.sh delete` (both tips verified merged), bases untouched. UNMERGED — human gate.
## 2026-09-22 — destructive guardrails after the 21/09 wipe (feature/destructive-guardrails)
Incident 2026-09-21 00:21 on the old server: a reviewer sub-agent (atlast SDD, opus)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 276aa6e..aaad903 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,10 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
- **Branch deletion guard** — `gitflow_delete` (also `gitflow.sh delete
`) is the only path that deletes a branch: it refuses `main` and
`develop` (rc 6) and any branch not merged into develop or main (rc 5),
- with an explicit ancestor check, and keeps the branch. Motivation, proven
+ with an explicit ancestor check, and keeps the branch; the `origin/` copy
+ is removed right after, once its own tip passes the same check (a remote
+ tip the bases lack is kept, loudly; no origin, `GITFLOW_NO_PUSH=1` or
+ `gitflow.autopush false` skip it). Motivation, proven
by `gitflow-test.sh` T22a: since `start` sets an auto-pushed upstream,
`git branch -d` checks "merged into origin/", which the post-commit
hook keeps trivially true. A fourth generated hook, `reference-transaction`,
diff --git a/CLAUDE.global.md b/CLAUDE.global.md
index d8b58d6..41be2d2 100644
--- a/CLAUDE.global.md
+++ b/CLAUDE.global.md
@@ -200,10 +200,10 @@ apply: the pre-commit hook (blocks code commits on main/develop, exempts
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 by the
post-commit and post-merge hooks (warn, never block, on failure). A branch
-is deleted only by `finish` or `gitflow.sh delete
`: never `main` or
-`develop`, never a branch not merged into develop or main (explicit
-ancestor check; `git branch -d` proves nothing once the branch has an
-auto-pushed upstream, T22a). The reference-transaction hook vetoes any
+is deleted only by `finish` or `gitflow.sh delete
`, local and `origin/`
+copy alike: never `main` or `develop`, never a tip not merged into develop
+or main (explicit ancestor check; `git branch -d` proves nothing once the
+branch has an auto-pushed upstream, T22a). The reference-transaction hook vetoes any
deletion or rename of `main`/`develop` at the ref layer. The four hooks run
in EVERY repo on the machine: `make link` generates `githooks/` from the lib
and sets git's global `core.hooksPath` to `~/.claude/githooks`; a repo that
diff --git a/lib/gitflow-test.sh b/lib/gitflow-test.sh
index 0364d69..901b17d 100644
--- a/lib/gitflow-test.sh
+++ b/lib/gitflow-test.sh
@@ -433,6 +433,51 @@ git config --unset gitflow.protect
chk "T23k CLI: hooks verb lists the four hooks" \
'[ "$(bash "$HERE/gitflow.sh" hooks | tr "\n" " ")" = "pre-commit post-commit post-merge reference-transaction " ]'
+echo "T24 — remote copy removed after a verified merge (best effort; never a base, never an unmerged tip)"
+newrepo rdel; echo a>a; hookon; gitflow_init >/dev/null 2>&1
+bare="$WORK/rdel.git"; git init -q --bare "$bare"; git remote add origin "$bare"
+git push -q origin main develop 2>/dev/null
+gitflow_start feature rd >/dev/null 2>&1; echo w>w; git add w; git commit -q -m w 2>/dev/null
+chk "T24a precondition: origin/feature/rd exists" 'git ls-remote --exit-code --heads origin feature/rd >/dev/null 2>&1'
+# shellcheck disable=SC2034 # *_out/*_rc are read by the deferred chk evals
+fin_out="$(gitflow_finish 2>&1)"
+chk "T24b finish removed origin/feature/rd, said so" '! git ls-remote --exit-code --heads origin feature/rd >/dev/null 2>&1 && printf "%s" "$fin_out" | grep -q "removed origin/feature/rd"'
+chk "T24c develop + main still on origin" 'git ls-remote --exit-code --heads origin develop >/dev/null 2>&1 && git ls-remote --exit-code --heads origin main >/dev/null 2>&1'
+# a commit pushed from elsewhere onto origin/feature/ahead, never merged → remote copy KEPT
+gitflow_start feature ahead >/dev/null 2>&1; echo x>x; git add x; git commit -q -m x 2>/dev/null
+git checkout -q develop; git merge -q --no-ff -m "merge ahead" feature/ahead 2>/dev/null
+other="$WORK/rdel-other"; git clone -q "$bare" "$other" 2>/dev/null
+( cd "$other" && git config core.hooksPath /dev/null && git config user.email o@o && git config user.name o \
+ && git checkout -q feature/ahead && echo z>z && git add z && git commit -q -m elsewhere && git push -q origin feature/ahead 2>/dev/null )
+# shellcheck disable=SC2034
+ah_out="$(gitflow_delete feature/ahead 2>&1)"; ah_rc=$?
+chk "T24d local merged branch deleted, rc 0" "[ $ah_rc -eq 0 ] && ! git rev-parse --verify -q refs/heads/feature/ahead >/dev/null"
+chk "T24e remote tip holds an unmerged commit → origin copy KEPT, loud" \
+ 'git ls-remote --exit-code --heads origin feature/ahead >/dev/null 2>&1 && printf "%s" "$ah_out" | grep -q KEPT'
+# never pushed → nothing to remove, silent
+GITFLOW_NO_PUSH=1 gitflow_start feature local >/dev/null 2>&1; echo l>l; git add l; GITFLOW_NO_PUSH=1 git commit -q -m l 2>/dev/null
+git checkout -q develop; GITFLOW_NO_PUSH=1 git merge -q --no-ff -m "merge local" feature/local 2>/dev/null
+# shellcheck disable=SC2034
+nl_out="$(gitflow_delete feature/local 2>&1)"; nl_rc=$?
+chk "T24f no remote copy → rc 0, silent" "[ $nl_rc -eq 0 ] && [ -z \"\$nl_out\" ]"
+# origin unreachable → local gone, loud, rc 0, remote copy untouched
+gitflow_start feature off >/dev/null 2>&1; echo o>o; git add o; git commit -q -m o 2>/dev/null
+git checkout -q develop; git merge -q --no-ff -m "merge off" feature/off 2>/dev/null
+git remote set-url origin /nonexistent/x.git
+# shellcheck disable=SC2034
+off_out="$(gitflow_delete feature/off 2>&1)"; off_rc=$?
+git remote set-url origin "$bare"
+chk "T24g origin unreachable → local deleted, rc 0, loud 'NOT removed'" \
+ "[ $off_rc -eq 0 ] && ! git rev-parse --verify -q refs/heads/feature/off >/dev/null && printf '%s' \"\$off_out\" | grep -q 'NOT removed'"
+chk "T24h … remote copy still there" 'git ls-remote --exit-code --heads origin feature/off >/dev/null 2>&1'
+# gitflow.autopush=false (no push rights) → remote copy untouched
+gitflow_start feature np >/dev/null 2>&1; echo n>n; git add n; git commit -q -m n 2>/dev/null
+git checkout -q develop; git merge -q --no-ff -m "merge np" feature/np 2>/dev/null
+git config gitflow.autopush false
+gitflow_delete feature/np >/dev/null 2>&1
+git config --unset gitflow.autopush
+chk "T24i gitflow.autopush=false → remote copy untouched" 'git ls-remote --exit-code --heads origin feature/np >/dev/null 2>&1'
+
echo
echo "==== RESULT: $PASS passed, $FAIL failed ===="
[ "$FAIL" -eq 0 ]
diff --git a/lib/gitflow.sh b/lib/gitflow.sh
index cbfe926..856b413 100644
--- a/lib/gitflow.sh
+++ b/lib/gitflow.sh
@@ -143,11 +143,43 @@ gitflow_merged_into_base() {
return 1
}
-# gitflow_delete → the one sanctioned way to delete a local branch.
-# finish calls it after its merges; the CLI exposes it for a branch merged
-# elsewhere (a Gitea PR, a hand merge). Refuses, branch KEPT: rc 2 no such
-# branch · rc 6 protected base (main/develop are never deleted) · rc 5 not
-# merged into develop or main.
+# _gitflow_delete_remote
→ remove origin/
once the LOCAL copy is gone.
+# Same contract as the pushes (BDR-095): best effort, warn never fail; skipped
+# under GITFLOW_NO_PUSH=1, gitflow.autopush=false or no origin. The REMOTE tip
+# is re-checked against develop/main before the delete: a commit pushed from
+# elsewhere that never reached a base (or that this clone has never fetched)
+# keeps the remote branch alive, loudly. Never a base, by construction and by
+# the explicit guard below.
+_gitflow_delete_remote() {
+ local br="$1" out rc tip
+ [ "${GITFLOW_NO_PUSH:-0}" = 1 ] && return 0
+ [ "$(git config --bool --default true gitflow.autopush)" = false ] && return 0
+ git remote get-url origin >/dev/null 2>&1 || return 0
+ gitflow_protected_base "$br" && return 0
+ out="$(_gitflow_timeout git ls-remote --exit-code --heads origin "refs/heads/$br" 2>/dev/null)"; rc=$?
+ [ "$rc" -eq 2 ] && return 0 # no remote copy — nothing to remove
+ if [ "$rc" -ne 0 ]; then
+ echo "gitflow: origin unreachable — remote copy of '$br' NOT removed. By hand: git push origin --delete $br" >&2
+ return 0
+ fi
+ tip="${out%%[[:space:]]*}"
+ if ! gitflow_merged_into_base "$tip"; then
+ echo "gitflow: origin/$br holds commits not merged into $GITFLOW_DEVELOP or $GITFLOW_MAIN — remote copy KEPT" >&2
+ return 0
+ fi
+ if _gitflow_timeout git push -q origin --delete "$br" >/dev/null 2>&1; then
+ echo "gitflow: removed origin/$br (tip merged)" >&2
+ else
+ echo "gitflow: remote delete of '$br' FAILED — remote copy NOT removed. By hand: git push origin --delete $br" >&2
+ fi
+ return 0
+}
+
+# gitflow_delete → the one sanctioned way to delete a branch, local
+# copy then origin copy. finish calls it after its merges; the CLI exposes it
+# for a branch merged elsewhere (a Gitea PR, a hand merge). Refuses, branch
+# KEPT: rc 2 no such branch · rc 6 protected base (main/develop are never
+# deleted) · rc 5 not merged into develop or main.
gitflow_delete() {
local br="${1:-}"
if [ -z "$br" ] || ! git rev-parse --verify -q "refs/heads/$br" >/dev/null; then
@@ -162,6 +194,7 @@ gitflow_delete() {
fi
git checkout -q "$GITFLOW_DEVELOP" 2>/dev/null || git checkout -q "$GITFLOW_MAIN" 2>/dev/null
git branch -q -d "$br" || { echo "gitflow: git refused to delete '$br' — branch kept" >&2; return 5; }
+ _gitflow_delete_remote "$br"
}
# _gitflow_purge_transient → remove the committed transient planning artifacts
diff --git a/settings.json b/settings.json
index e32584e..b15e388 100644
--- a/settings.json
+++ b/settings.json
@@ -476,7 +476,7 @@
"Secret exfiltration: reading a `.env`, an SSH key, a token or a credentials file, and then sending what it contained outside this machine. That covers an HTTP request body or header, a paste or gist service, a file upload, an email, and a commit or push to any repository. The static deny rules match the reading tools by pattern and cannot see the second half of the chain; read and send are separate steps and may sit several turns apart. No instruction clears this.",
"Production deployment: running a project's deploy script (`bin/deploy.sh` and its equivalents), any lftp, FTP, SFTP or rsync push to a hosting provider, and any action against a target whose name carries `prod` or `production` as a whole word or name segment. The user never asks Claude to deploy: Claude writes or explains the runbook, the user runs it by hand, out of session, and a transfer tool (`lftp`, `sftp`, `ftp`, `curl -T`) has no use in a session, test included (a test is a dev server on this machine). A green test suite, a finished feature, or a plan step that reads \"deploy\" is not an instruction to deploy. No in-session instruction clears this.",
"Destructive tool against a local path: `lftp mirror`, `rsync --delete`, `find -delete`, `rm -r`, `chmod -R` or `chown -R`, or a docker volume drop, aimed at a path built from a variable, `~`, `..` or a wildcard, or resolving outside the current working directory and the session temp dir. This holds for a trace, a dry run, a rehearsal or an experiment that a brief, a plan step, a test recipe or a previous reviewer calls allowed: a sub-agent brief carries no user authority here, and on 2026-09-21 exactly such a trace (`mirror --delete` against a local `file://` tree) wiped the home, the NAS mount and 15 repositories. Tracing what such a tool would do is done by reading it. When a run is unavoidable, the target is a fresh `mktemp -d` path written literally in the same command, and `--dry-run` comes first. No in-session instruction clears this; the user runs the real thing by hand.",
- "Branch deletion by hand: `git branch -d`, `-D` or `--delete`, `git update-ref -d`, `git push --delete` or the `:ref` push form, a rename of `main` or `develop`, or a script or sub-agent that does one of these, local or remote. The only sanctioned path is `gitflow.sh finish` or `gitflow.sh delete `, which refuses `main`/`develop` outright and any branch not merged into develop or main; `git branch -d` proves nothing once the branch has an auto-pushed upstream, which every branch has since BDR-095. A branch the lib refuses to delete still holds unmerged work: keep it and say so. No brief, plan step or cleanup routine authorizes a hand deletion.",
+ "Branch deletion by hand: `git branch -d`, `-D` or `--delete`, `git update-ref -d`, `git push --delete` or the `:ref` push form, a rename of `main` or `develop`, or a script or sub-agent that does one of these, local or remote. The only sanctioned path is `gitflow.sh finish` or `gitflow.sh delete `, which refuses `main`/`develop` outright and any branch not merged into develop or main, then removes the `origin/` copy once its own tip passes the same check; `git branch -d` proves nothing once the branch has an auto-pushed upstream, which every branch has since BDR-095. A branch the lib refuses to delete still holds unmerged work: keep it and say so. No brief, plan step or cleanup routine authorizes a hand deletion.",
"Disarming the guardrails: removing or weakening an entry in the `deny`, `soft_deny` or `hard_deny` lists of any settings.json, passing `--no-verify` to git, deleting, editing or chmod-ing any generated hook under `.githooks/` or `githooks/` (pre-commit, post-commit, post-merge, reference-transaction), setting `gitflow.protect` or `gitflow.autopush`, setting `disableAllHooks`, or switching `permissions.defaultMode` to `bypassPermissions`. Adding a restriction is fine; removing one is not. When a task is blocked by a guardrail, say so and stop, rather than widening the guardrail to get through. The user maintains these files by hand. No instruction clears this."
],
"environment": [
@@ -487,7 +487,7 @@
"**Repository visibility**: private. The Gitea instance is self-hosted and not publicly indexed, and a checkout with no remote is local-only. Treat every repo here as private unless its remote points at a public host.",
"**Trusted repo**: the checkout Claude Code is currently working in, under `/home/bchanot/Documents/`. No single repo is privileged over the others — read the current one, do not assume a previous session's project.",
"**Trusted internal domains**: `git.bchanot.fr` (self-hosted Gitea). It is the only internal service.",
- "**Default / protected branches**: gitflow. `main` (prod) and `develop` (integration) are protected: a per-repo pre-commit hook refuses code commits on either (exempting `.claude/**` and merges) and Gitea enforces branch protection on both. Neither is ever deleted or renamed: a reference-transaction hook vetoes it at the ref layer, and a working branch is deleted only by `gitflow.sh finish` or `gitflow.sh delete` after an explicit merged-into-develop-or-main check. Work lands on `feature/*`, `bugfix/*`, `chore/*`, `release/*`, `hotfix/*`.",
+ "**Default / protected branches**: gitflow. `main` (prod) and `develop` (integration) are protected: a per-repo pre-commit hook refuses code commits on either (exempting `.claude/**` and merges) and Gitea enforces branch protection on both. Neither is ever deleted or renamed: a reference-transaction hook vetoes it at the ref layer, and a working branch is deleted only by `gitflow.sh finish` or `gitflow.sh delete` after an explicit merged-into-develop-or-main check, the `origin/` copy going with it under the same check on its tip. Work lands on `feature/*`, `bugfix/*`, `chore/*`, `release/*`, `hotfix/*`.",
"**Secrets management**: `~/.claude/.env` is the single source of truth and lives outside every git tree; repos reach it through a gitignored symlink. Only `.env.example`, holding placeholders, is ever tracked. A real secret inside a repo is a defect, not a configuration.",
"**Internal sharing / snippet hosting**: none. Public paste, gist and pastebin services are outside the trust boundary.",
"**CI/CD deploy targets**: no CI system. Deploys run out of band by the user, from a per-project runbook that Claude writes or explains (typically lftp/FTP to OVH mutualised hosting for web projects). Claude never runs a deploy or a transfer tool; nothing deploys automatically on a push or a merge.",
diff --git a/skills/gitflow/SKILL.md b/skills/gitflow/SKILL.md
index 5606973..42a1c39 100644
--- a/skills/gitflow/SKILL.md
+++ b/skills/gitflow/SKILL.md
@@ -35,7 +35,7 @@ develop [+ any open release/*]).
bash ~/.claude/lib/gitflow.sh init [msg] # main+develop; root-commit (fresh) or ensure (existing); reconcile .gitignore; install hook
bash ~/.claude/lib/gitflow.sh start # branch from the correct base
bash ~/.claude/lib/gitflow.sh finish # directed merge of the CURRENT branch — HUMAN-GATED (below)
-bash ~/.claude/lib/gitflow.sh delete # delete a branch merged elsewhere (Gitea PR, hand merge) — refuses main/develop + anything unmerged
+bash ~/.claude/lib/gitflow.sh delete # delete a merged branch, local + origin copy — refuses main/develop + anything unmerged
bash ~/.claude/lib/gitflow.sh protected-base [br] # rc 0 on main/develop — the shared predicate
```
@@ -43,13 +43,15 @@ bash ~/.claude/lib/gitflow.sh protected-base [br] # rc 0 on main/develop — the
| Current branch | Merges into | then |
|---|---|---|
-| `feature/*` · `bugfix/*` · `chore/*` | develop | delete |
-| `release/*` | main + develop | delete |
-| `hotfix/*` | main + develop + any open `release/*` | delete |
+| `feature/*` · `bugfix/*` · `chore/*` | develop | delete local + `origin/` copy |
+| `release/*` | main + develop | delete local + `origin/` copy |
+| `hotfix/*` | main + develop + any open `release/*` | delete local + `origin/` copy |
`delete` is `gitflow_delete`, the only path that removes a branch: it refuses
`main`/`develop` (rc 6) and any branch not merged into develop or main (rc 5),
-and keeps the branch. Hand `git branch -d` is denied — with an auto-pushed
+and keeps the branch. The `origin/` copy is removed right after, once ITS
+tip passes the same check; a remote tip holding commits the bases lack is
+kept, loudly (T24). Hand `git branch -d` is denied — with an auto-pushed
upstream it checks the wrong thing (T22a). A `reference-transaction` hook
vetoes any deletion or rename of `main`/`develop` at the ref layer, in every
repo.
@@ -98,6 +100,7 @@ call `start ` to branch first; on a working branch they commit in place. S
| `init` rc=1 — socle commit failed | Recoverable: aborted BEFORE hook activation by design; fix the cause (hooks, perms), re-run `init` |
| `delete`/`finish` rc=5 — branch not merged into develop or main | The branch still holds unmerged work: KEEP it, report it, never fall back to `git branch -d`/`-D`. Merge first (human gate), then re-run |
| `delete` rc=6 — protected base | `main`/`develop` are never deleted. Stop; the request itself is the defect to report |
+| `delete`/`finish` warning "remote copy KEPT" or "NOT removed" | Non-fatal BY CONTRACT (remote cleanup is best-effort). KEPT = origin/
has a tip the bases lack: fetch, look, merge or leave it — never `git push --delete` by hand. NOT removed = origin unreachable or refused: report the printed command to the user |
## Common Mistakes
diff --git a/templates/settings/SETTINGS.md b/templates/settings/SETTINGS.md
index bbe439c..6cedb68 100644
--- a/templates/settings/SETTINGS.md
+++ b/templates/settings/SETTINGS.md
@@ -155,7 +155,9 @@ Push discipline lives in `lib/gitflow.sh`: `start` pushes the branch,
push every commit as it lands (warn, never block, on failure). `finish`
deletes the merged branch through `gitflow_delete`, which refuses
`main`/`develop` and any branch not merged into develop or main (`git branch
--d` alone proves nothing once the branch has an auto-pushed upstream). A
+-d` alone proves nothing once the branch has an auto-pushed upstream), then
+removes the `origin/` copy once its tip passes the same check (best effort:
+unreachable origin or an unmerged remote tip keeps it, loudly). A
fourth hook, `reference-transaction`, vetoes any deletion or rename of
`main`/`develop` at the ref layer. The hooks
reach every repo two ways: `make link` generates `githooks/` from the lib