You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
functionblockForcePush(ctx: PolicyContext): PolicyResult{if(ctx.toolName!=="Bash")returnallow();constargs=extractGitPushArgs(getCommand(ctx));if(args.some((a)=>FORCE_PUSH_RE.test(a))){returndeny("Force-pushing is blocked");}returnallow();}
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]
Loading
Concrete scenarios we see today:
Solo rebase workflow. A developer rebases their branch onto fresh main, then runs git 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 --force because "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-lease precisely because it refuses to clobber unfetched work. The policy denies the safer command while leaving --force denied too, removing the guardrail's reason to exist for them.
Onboarding new hires. New hires who learned --force-with-lease as 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-includesconstFORCE_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.
Summary
The
block-force-pushbuiltin currently treats every flag whose name contains--forcethe same way, which meansgit push --force-with-leaseandgit push --force-if-includesare denied alongside the truly dangerousgit 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:136src/hooks/builtin-policies.ts:637-644Because
/--force/matches--force-with-leaseand--force-if-includesas substrings, every safe variant is denied today. There is no test in__tests__/hooks/builtin-policies.test.ts(thedescribe("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:
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.--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.--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:
Concretely:
git push --forcegit push -fgit push --force-with-leasegit push --force-with-lease=origin/maingit push --force-if-includesgit push origin --force-with-lease featuregit push origin worktree/hn-fetch-jobA
policyParams.allowForceWithLeaseflag (defaulttrue) would let teams keep the strict-everything behaviour by setting it tofalseif 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.describe("block-force-push", …)in__tests__/hooks/builtin-policies.test.tscovering each row of the table above.policyParams.allowForceWithLease=falserestores the current behaviour for users who want it.## Unreleased > Fixes.