Repository navigation
[CLAUDE ROUTINE]: CX/Bug enhancement — let block-force-push allow safer --force-with-lease / --force-if-includes so users keep using the recommended Git workflow #267
Description
Summary
The block-force-push builtin currently treats every flag whose name contains --force the same way, which means git push --force-with-lease and git push --force-if-includes are denied alongside the truly dangerous git push --force. These two flags are exactly the workflow Git's own docs recommend after a rebase — they refuse to overwrite anyone else's work — so blocking them gently nudges users away from the safest option.
This is a small but meaningful CX win: the policy keeps catching the dangerous form while letting the safe forms through, so users get protection and keep their recommended day-to-day workflow.
Where
src/hooks/builtin-policies.ts:136
// blockForcePush
const FORCE_PUSH_RE = /(?:--force|-f\b)/;src/hooks/builtin-policies.ts:637-644
function blockForcePush(ctx: PolicyContext): PolicyResult {
if (ctx.toolName !== "Bash") return allow();
const args = extractGitPushArgs(getCommand(ctx));
if (args.some((a) => FORCE_PUSH_RE.test(a))) {
return deny("Force-pushing is blocked");
}
return allow();
}Because /--force/ matches --force-with-lease and --force-if-includes as substrings, every safe variant is denied today. There is no test in __tests__/hooks/builtin-policies.test.ts (the describe("block-force-push", …) block only covers --force, -f, plain push, and a body-mention false-positive case) confirming the safe forms are allowed — confirming the gap is unintentional.
Why this matters
flowchart TD
A[User finishes rebase, wants to update remote feature branch] --> B{Which command?}
B -->|git push --force| C[Dangerous: silently overwrites teammate commits]
B -->|git push --force-with-lease| D[Safe: aborts if teammate has pushed since fetch]
B -->|git push --force-if-includes| E[Safer still: also requires local ref to include remote]
C --> F[block-force-push denies]
D --> F
E --> F
F --> G[User options today]
G --> H[Disable block-force-push entirely 🚨]
G --> I[Run git push -f anyway in another terminal 🚨]
G --> J[Give up on protection]
Concrete scenarios we see today:
- Solo rebase workflow. A developer rebases their branch onto fresh
main, then runsgit push --force-with-lease— the canonical "I rebased my own branch" command. The hook denies it. They either (a) disable the policy, losing the catch on real--force, or (b) start using--forcebecause "the policy already blocks it, might as well." Either way the protection is weaker after the encounter. - Pair / mob programming. Two devs pair-program on the same branch and use
--force-with-leaseprecisely because it refuses to clobber unfetched work. The policy denies the safer command while leaving--forcedenied too, removing the guardrail's reason to exist for them. - Onboarding new hires. New hires who learned
--force-with-leaseas the right answer in their last shop hit the deny on day one. The deny message ("Force-pushing is blocked") doesn't tell them their safer command was actually safer — so they conclude failproofai is overzealous and turn it off.
Proposed enhancement
Tighten the regex to match only the dangerous unqualified forms, and add explicit tests for the safe variants:
// blockForcePush
// --force or -f at a token boundary, but NOT --force-with-lease / --force-if-includes
const FORCE_PUSH_RE = /(?:--force(?![-\w])|(?:^|\s)-[A-Za-z]*f(?![-\w]))/;Concretely:
| Command | Today | Proposed |
|---|---|---|
git push --force |
deny ✅ | deny ✅ |
git push -f |
deny ✅ | deny ✅ |
git push --force-with-lease |
deny 🚨 | allow ✅ |
git push --force-with-lease=origin/main |
deny 🚨 | allow ✅ |
git push --force-if-includes |
deny 🚨 | allow ✅ |
git push origin --force-with-lease feature |
deny 🚨 | allow ✅ |
git push origin worktree/hn-fetch-job |
allow ✅ | allow ✅ |
A policyParams.allowForceWithLease flag (default true) would let teams keep the strict-everything behaviour by setting it to false if they prefer a more conservative posture.
Acceptance criteria
-
git push --force-with-leasereturnsallow(with default params). -
git push --force-with-lease=origin/mainreturnsallow. -
git push --force-if-includesreturnsallow. -
git push --forceandgit push -fstill returndeny. - New tests added under
describe("block-force-push", …)in__tests__/hooks/builtin-policies.test.tscovering each row of the table above. - (Optional)
policyParams.allowForceWithLease=falserestores the current behaviour for users who want it. - CHANGELOG.md entry under
## Unreleased > Fixes.
Activity
chore: add block-version-bumps custom policy
[luv-cut-0.0.10-beta.0] chore: cut 0.0.10-beta.0 release + block-vers…