diff --git a/agents/client-handover-writer.md b/agents/client-handover-writer.md
index c9a7271..0273722 100644
--- a/agents/client-handover-writer.md
+++ b/agents/client-handover-writer.md
@@ -571,7 +571,7 @@ If `PENDING_CHANGES` non-empty → invoke /commit-change skill via subagent:
`git branch --show-current` → `
`;
`git remote get-url origin >/dev/null 2>&1 && echo origin || echo no-origin`;
`git rev-list --count origin/
..
2>/dev/null || echo unknown` →
-`ahead`. Validate `
` against `^[A-Za-z0-9._/-]+$` before using it
+`ahead`. Validate `
` against `^[A-Za-z0-9._/][A-Za-z0-9._/-]*$` before using it
anywhere (git accepts shell metacharacters in branch names). On mismatch:
state = `unknown (branch name contains characters this pipeline refuses to
interpolate: push by hand after renaming the branch)`, interpolate NOTHING,
diff --git a/lib/gitflow-test.sh b/lib/gitflow-test.sh
index 0c2d82d..c6bdd67 100644
--- a/lib/gitflow-test.sh
+++ b/lib/gitflow-test.sh
@@ -6,6 +6,7 @@
# (the chk helper EVALs its second arg; single-quoted assertion strings are
# intentional — they must not expand at definition time.)
set -uo pipefail
+export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null
HERE="$(cd "$(dirname "$0")" && pwd)"
# Do NOT override GITFLOW_GITIGNORE_TEMPLATE: the lib self-resolves it from its
# own location (../templates), which is correct in both the repo and installed.
diff --git a/lib/gitflow.sh b/lib/gitflow.sh
index 6732258..db008b6 100644
--- a/lib/gitflow.sh
+++ b/lib/gitflow.sh
@@ -81,7 +81,8 @@ gitflow_push_mode() {
case "$rc:$val" in
0:false) echo manual ;;
0:true|1:*) echo auto ;;
- *) raw=$(git config gitflow.autopush 2>/dev/null)
+ *) raw=$(git config gitflow.autopush 2>/dev/null | LC_ALL=C tr -cd '[:print:]' 2>/dev/null)
+ raw=${raw:0:64}
if [ -n "$raw" ]; then
echo "gitflow.sh push-mode: gitflow.autopush='$raw'" \
"is not a boolean (git rc $rc)" >&2
diff --git a/skills/capitalize/SKILL.md b/skills/capitalize/SKILL.md
index b421f33..1045007 100644
--- a/skills/capitalize/SKILL.md
+++ b/skills/capitalize/SKILL.md
@@ -317,7 +317,7 @@ journal-only example.
Surgical scope is the helper's (stages ONLY `.claude/memory` + `.claude/tasks`,
changed-paths-filtered, never `git add -A`). Do NOT hand-roll git here.
-## STEP 5C — AUTO-PERSIST THE MEMORY (finish + push)
+## STEP 5C — AUTO-PERSIST THE MEMORY (finish; the lib pushes in auto-push mode)
Memory's value is cross-session persistence — a commit stranded on an unmerged
`chore/` branch is invisible to the next session sitting on develop, so the
@@ -337,11 +337,11 @@ Skip this step entirely (go to STEP 6, which prints the hold note) on
Otherwise, from the `chore/` branch, THREE separate Bash calls, never combined. INVARIANT: no `git push` inside any Bash call of this skill (push-guard reads command text; the lib pushes develop itself in auto-push mode). The hints that tell the USER what to type (`! git push …`) are prose, kept on single lines.
-1. `bash "$HOME/.claude/lib/gitflow.sh" finish chore ` — merge → develop, delete branch, push develop in auto-push mode. rc≠0 → skip calls 2-3, go to STEP 6 with the `finish failed` line: rc 4 = conflict, develop mid-merge, `chore/` kept, NOT merged; rc 1 = checkout failed, NOT merged; rc 5/2/6 come from the delete AFTER the merge: check `git merge-base --is-ancestor chore/ develop` and report `merged, branch not deleted (rc )` when it holds, `NOT merged` otherwise. Never say "merged" without that check.
+1. `bash "$HOME/.claude/lib/gitflow.sh" finish chore ` — merge → develop, delete branch, push develop in auto-push mode. rc≠0 → go to STEP 6 with the `finish failed` line (rc 1/4 skip calls 2-3; rc 5/2/6 with the ancestor check true still run them so the push state is reported): rc 4 = conflict, develop mid-merge, `chore/` kept, NOT merged; rc 1 = checkout failed, NOT merged; rc 5/2/6 come from the delete AFTER the merge: check `git merge-base --is-ancestor chore/ develop` and report `merged, branch not deleted (rc )` when it holds, `NOT merged` otherwise. Never say "merged" without that check.
2. `bash "$HOME/.claude/lib/gitflow.sh" push-mode` → `auto | manual | invalid` (stderr names an invalid value).
3. `git rev-list --count origin/develop..develop 2>/dev/null || echo unknown` → `ahead` (0 = on origin; `unknown` = no origin/develop ref, e.g. no origin remote).
-Outcomes, evaluated IN THIS ORDER (all require finish rc 0):
+Outcomes, evaluated IN THIS ORDER (finish rc 0, or rc 5/2/6 with the branch merged — the wording then starts with `merged, branch not deleted (rc ) —` instead of `merged to develop —`):
- **push mode `invalid`** → `merged to develop — gitflow.autopush= is not a boolean: the lib and hooks still push on an invalid value until run D (origin/develop is commit(s) behind, or unknown); fix the value by hand`.
- **`ahead` = 0** → `develop pushed` (auto-push mode did it).
@@ -368,16 +368,17 @@ CAPITALIZE COMPLETE — ()
Then the closing line — pick by the STEP 5C persist result (`` = `Context
flushed` for pre-wipe, `Session closed` for ritual):
-- **auto-persisted (default — branched off develop, pushed)** → `✅ + persisted to origin/develop (). Next session: read .claude/memory/ at startup.`
-On the `--no-push` path (and on any 5B-committed path where 5C did not run) read TWO facts first, each its own Bash call: `bash "$HOME/.claude/lib/gitflow.sh" push-mode` and `git rev-list --count origin/chore/..chore/ 2>/dev/null || echo unknown` (`branch_ahead`). `` below is the verb's word.
+- **auto-persisted (5C: finish rc 0 AND `ahead` = 0)** → `✅ + persisted to origin/develop (). Next session: read .claude/memory/ at startup.`
+
+On the `--no-push` path ONLY read TWO facts first, each its own Bash call: `bash "$HOME/.claude/lib/gitflow.sh" push-mode` and `git rev-list --count origin/chore/..chore/ 2>/dev/null || echo unknown` (`branch_ahead`). `` below is the verb's word. The WORKING-branch and rc 3 paths have no `chore/` and never use the mode.
- **--no-push, `branch_ahead` = 0** → `✅ + committed on chore/ — pushed to origin by the hooks (auto-push mode), NOT merged (--no-push). Merge when ready.`
- **--no-push, `branch_ahead` > 0 or unknown** → `✅ + committed on chore/ — this disk only, not pushed (), NOT merged. You: ! git push -u origin chore/; merge when ready.` With push mode `invalid`, append ` gitflow.autopush= is not a boolean: fix it by hand`.
- **manual (merged, `ahead` > 0)** → `✅ + merged to develop — manual push mode: not pushed. You: ! git push origin develop`
-- **not on origin (merged, `ahead` unknown)** → `✅ + merged to develop — not on origin (no origin/develop ref).`
+- **not on origin (merged, `ahead` unknown)** → `✅ + merged to develop — not on origin (no origin/develop ref).` Push mode manual → add `You: ! git push origin develop once a remote exists`.
- **invalid (merged)** → `⚠️ + merged to develop — gitflow.autopush= is not a boolean; lib/hooks still push on it until run D (origin/develop behind). Fix the value by hand.`
- **push failed after merge** → `✅ + merged to develop — ⚠️ push FAILED (); merged locally, push manually.`
-- **finish failed** → `⚠️ + finish rc : — chore/ kept, NOT merged (or: merged, branch not deleted); resolve by hand.`
+- **finish failed** → `⚠️ + finish rc : — chore/ kept, NOT merged; resolve by hand.` (rc 1/4 only; rc 5/2/6 with the branch merged use the outcome lines above with the `merged, branch not deleted (rc )` prefix, so the push state is still reported.)
- **WORKING branch (rode a feature branch)** → `✅ + committed on . Integrates when the branch merges.`
- **commit skipped (rc 3)** → keep the ✅ on the WRITE but make the gap loud, never
buried: `✅ — ⚠️ NOT committed (); entries safe on disk, commit manually.`