Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/cad0p/pi-steering-hooks/llms.txt

Use this file to discover all available pages before exploring further.

The built-in no-force-push rule that ships with pi-steering’s default rule set deliberately permits git push --force-with-lease. The lease flag is the documented “safe” way to update a branch after a rebase — it fails if the remote ref has advanced since your last fetch, avoiding the silent overwrite that bare --force allows. For most teams, that carve-out is the right call. For some environments the lease carve-out is still too permissive. Shared long-lived branches (main, develop, regulated release branches), compliance contexts that treat any SHA rewrite as a violation, or teams that simply want the agent to never reach for any --force variant as a first-line fix — all of these benefit from a rule that closes the loophole entirely. The force-push-strict pack replaces the default rule with one that blocks every force variant without exception.

Configuration

import { defineConfig } from "pi-steering";

export default defineConfig({
  // Disable the shipped default so its less-strict block-reason
  // ("use --force-with-lease if you must") doesn't leak to the LLM
  // alongside our stricter variant.
  disabledRules: ["no-force-push"],
  rules: [
    {
      name: "no-force-push-strict",
      tool: "bash",
      field: "command",
      // Mirrors DEFAULT_RULES.no-force-push's pre-subcommand flag
      // slot but WITHOUT the --force-with-lease allowance.
      pattern:
        "^git\\b(?:\\s+-{1,2}[A-Za-z]\\S*(?:\\s+\\S+)?)*\\s+push\\b.*(?:--force\\b|\\s-f(?:\\s|$))",
      reason:
        "No force pushes of any kind, including --force-with-lease. Create a new commit, or reset + re-commit via a non-force path.",
    },
  ],
});

How the pattern works

The regex has two distinct structural parts: Pre-subcommand flag slot(?:\s+-{1,2}[A-Za-z]\S*(?:\s+\S+)?)* This optional repeating group matches the flags that git itself accepts before the subcommand name. It catches forms like:
  • git -C /repo push --force — the -C <path> flag changes the working directory
  • git -c key=val push --force — the -c flag sets a config value for this invocation
  • git --git-dir=/x push -f--git-dir overrides the repository path
Without this slot, a pattern anchored to ^git\s+push would miss all three forms above. Force flag match(?:--force\b|\s-f(?:\s|$)) This matches --force as a whole word (so --force-with-lease would not match --force alone if it appeared at that position), and -f as a short flag. The pattern also relies on the AST backend’s wrapper detection — sh -c 'git push --force', sudo git push --force, and xargs git push --force are all caught automatically without requiring the regex to encode those wrappers.

Why disabledRules: ["no-force-push"]

Without the disabledRules entry, both the default no-force-push and the new no-force-push-strict would be active simultaneously. On git push --force, both would fire — the agent would receive two block messages, and the less-strict one (“use --force-with-lease if you must”) would be visible alongside the strict one, sending a contradictory signal. Disabling the default ensures exactly one rule fires with exactly one, internally consistent reason.

Adversarial matrix

CommandResult
git push --force🚫 blocked
git push --force-with-lease🚫 blocked
git push -f🚫 blocked
git -C /repo push --force🚫 blocked
git -c core.autocrlf=false push --force🚫 blocked
git --git-dir=/x push -f🚫 blocked
sh -c 'git push --force'🚫 blocked
git push origin main✅ allowed
git push --force-if-includes✅ allowed
echo 'git push --force'✅ allowed

Install

cp examples/force-push-strict/steering.ts .pi/steering/index.ts
The loader accepts TypeScript only. If you have an existing JSON config from a prior pi-steering version, convert it first:
pi-steering import-json .pi/steering.json -o .pi/steering/index.ts

Build docs developers (and LLMs) love