Skip to content

[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

@NiveditJain

Description

@NiveditJain

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]
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-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-lease returns allow (with default params).
  • git push --force-with-lease=origin/main returns allow.
  • git push --force-if-includes returns allow.
  • git push --force and git push -f still return deny.
  • New tests added under describe("block-force-push", …) in __tests__/hooks/builtin-policies.test.ts covering each row of the table above.
  • (Optional) policyParams.allowForceWithLease=false restores the current behaviour for users who want it.
  • CHANGELOG.md entry under ## Unreleased > Fixes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions