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 git plugin is pi-steering’s default-on plugin, registered automatically via DEFAULT_PLUGINS in every config. It gates commits to protected branches through the no-main-commit and no-main-commit-github rules, and extends the when: clause with six predicates that expose git repository state — branch name, upstream tracking ref, commits-ahead count, staged change presence, working-tree cleanliness, and remote URL. All six predicates are walker-aware: when a bash chain contains git checkout feat && git commit, the commit evaluates against feat, not against whatever branch was current before the chain ran.

Predicates

Matches the current branch name against a pattern. The predicate performs a three-way discrimination on tracker state before ever shelling out:
  1. Tracker-resolved value — a git checkout <X> or git switch <X> with a statically-resolvable target was observed earlier in the same bash chain. The pattern is matched against X directly.
  2. Tracker unknown — a checkout was observed but the target was dynamic (e.g. git checkout $VAR). The onUnknown policy applies without shelling out, because git branch --show-current at this point would return the pre-checkout branch and silently defeat the walker.
  3. Tracker missing — no branch-changing command appeared in the chain. The predicate shells out via git branch --show-current in ctx.cwd.
Bare form (single pattern):
when: { branch: /^main$/ }
when: { branch: "^feat-" }  // string is treated as a regex source
Array form (OR-of-matches — fires when the branch matches any listed pattern):
when: { branch: [/^main$/, /^master$/, /^trunk$/] }
Spread form with onUnknown (defaults to "block" — fail-closed):
when: { branch: { pattern: /^main$/, onUnknown: "allow" } }
when: { branch: { pattern: [/^main$/, /^master$/], onUnknown: "allow" } }
onUnknown: "block" means: if the branch cannot be determined (dynamic checkout, exec failure, detached HEAD), the predicate reports “match” so the rule still fires. Use "allow" only when you want to permit commands on branches that can’t be resolved.
Matches the current branch’s configured upstream (resolved via git rev-parse --abbrev-ref @{upstream}). A branch without an upstream set causes a non-zero exit; the predicate then applies the onUnknown policy (default "block").There is no tracker for upstream today — upstream configuration isn’t changed by in-chain git commands at a rate that justifies modelling it statically. The per-tool-call exec cache ensures multiple upstream-gated rules share one git call.The predicate includes a walker-unknown-cwd guard: when the walker cannot statically resolve the effective cwd (cd "$VAR/pkg" && ...), shelling out would query the wrong repository. In that case the predicate surfaces "unknown" and the engine’s onUnknown policy projects to a definite verdict.
when: { upstream: /^origin\/main$/ }
when: { upstream: { pattern: "^origin/", onUnknown: "allow" } }
Matches when the number of commits ahead of a revision (default @{upstream}) satisfies every supplied comparator. At least one of eq, gt, or lt must be specified; all provided comparisons must pass (AND semantics).Bare formcommitsAhead: N is sugar for { eq: N }:
when: { commitsAhead: 1 }
Spread form:
when: { commitsAhead: { eq: 1 } }                       // exactly one ahead
when: { commitsAhead: { gt: 0 } }                       // at least one
when: { commitsAhead: { gt: 0, lt: 5 } }                // 1 through 4
when: { commitsAhead: { wrt: "origin/main", eq: 1 } }   // custom ref
  • eq?: number — exact equality (count === eq)
  • gt?: number — strict greater-than (count > gt)
  • lt?: number — strict less-than (count < lt)
  • wrt?: string — git revision to count against; defaults to "@{upstream}"
Returns false (rule skips) on exec failure or non-numeric output. Pair with upstream for fail-closed behavior when no upstream is configured.
Boolean predicate. Fires when the presence or absence of staged changes matches the declared value. Resolved via git diff --cached --quiet: exit 0 means no staged changes, exit 1 means staged changes exist.
when: { hasStagedChanges: true }   // fires when staged changes exist
when: { hasStagedChanges: false }  // fires when no staged changes exist
Returns false on exec failure rather than blocking (fail-open on error). Apply the spread form with onUnknown: "block" or layer with upstream if you need fail-closed behavior. Includes the walker-unknown-cwd guard — when the effective cwd is unresolvable, the predicate surfaces "unknown" rather than querying the wrong repository.
Boolean predicate. Fires when the working tree’s cleanliness at ctx.cwd matches the declared value. Resolved via git status --porcelain: empty stdout means clean.
when: { isClean: true }   // fires when working tree is clean
when: { isClean: false }  // fires when working tree is dirty
Returns false on exec failure. Includes the same walker-unknown-cwd guard as hasStagedChanges.
Matches the origin remote URL against a pattern (resolved via git config --get remote.origin.url). Useful for rules that should only apply in specific repositories — for example, detecting GitHub repos to emit PR-flow guidance.
when: { remote: /github\.com[/:]/ }
when: { remote: { pattern: /production/, onUnknown: "block" } }
Same arg shapes as branch (bare pattern, array, or spread with onUnknown). Non-zero exit from git (no origin configured) applies the onUnknown policy (default "block"). Includes the walker-unknown-cwd guard.

Built-in rules

no-main-commit

Blocks git commit on branches matching main, master, mainline, or trunk. The pattern anchors on the git commit subcommand and is bypass-proof against common wrapper forms — sh -c 'git commit', git -C /path commit, and git checkout main && git commit all trigger the rule. The rule is overridable on a per-invocation basis via an inline comment:
git commit -m "release" # steering-override: no-main-commit — intentional release commit

no-main-commit-github

A specialization of no-main-commit for github.com clones. It adds when: { remote: { pattern: /github\.com[/:]/, onUnknown: "allow" } } alongside the same protected-branch check, and emits PR-flow guidance in the block reason (gh pr merge) instead of the generic feature-branch reminder. First-match-wins ordering is load-bearing. no-main-commit-github is registered before no-main-commit in the plugin’s rule array. On a github clone on a protected branch both rules match — first-match-wins routes the github-flavored reason to github users. On non-github contexts the remote: predicate doesn’t match and the engine falls through to the generic rule. The remote: predicate uses onUnknown: "allow" so that repositories without an origin remote (fresh-init repos, repos with upstream but no origin) fall through to the generic no-main-commit rather than emitting misleading PR-flow guidance.

Opting out

All three opt-out paths require an explicit plugins: [gitPlugin] import so defineConfig’s generics can type-check the rule and plugin names. DEFAULT_PLUGINS gives runtime registration but doesn’t extend the TypeScript inference.
import gitPlugin from "pi-steering/plugins/git";

// Drop the rule but keep predicates + tracker:
defineConfig({ plugins: [gitPlugin], disabledRules: ["no-main-commit"] });

// Drop the whole git plugin:
defineConfig({ plugins: [gitPlugin], disabledPlugins: ["git"] });

// Drop EVERYTHING shipped (DEFAULT_RULES and DEFAULT_PLUGINS):
defineConfig({ disableDefaults: true });

Customizing the rule

The named exports from pi-steering/plugins/git include noMainCommit and noMainCommitGithub. Spread-and-override lets you reuse all fields — pattern, when, tool, field, noOverride — and only replace what you need.
import { defineConfig } from "pi-steering";
import gitPlugin, { noMainCommit } from "pi-steering/plugins/git";
import type { Rule } from "pi-steering";

const myNoMainCommit = {
  ...noMainCommit,
  name: "myorg-no-main-commit",
  reason: async (ctx) => {
    const original =
      typeof noMainCommit.reason === "function"
        ? await noMainCommit.reason(ctx)
        : noMainCommit.reason;
    return `${original}\n\nSee skill \`git-discipline@myorg\`.`;
  },
} as const satisfies Rule;

export default defineConfig({
  plugins: [gitPlugin],
  disabledRules: ["no-main-commit"],  // original off
  rules: [myNoMainCommit],            // replacement on
});
as const satisfies Rule preserves literal types so defineConfig’s cross-reference checks on happened.event, observer, and other fields still run on the replacement.
Always use a fresh name for replacement rules. Reusing the plugin rule’s name has two failure modes, both bad:
  1. Same name + no disabledRules — both rules are kept. The plugin rule fires alongside your customized version and paths you intended to exempt still get the original message. The customization silently fails to apply.
  2. Same name + disabledRules — the filter applies to ALL rules with that name across both the user config and the plugin. Neither rule fires. Silent fail-open — the worst outcome for a safety rule.
Use a fresh name (e.g. myorg-no-main-commit) and pair it with disabledRules: ["no-main-commit"].

Branch tracker

The git plugin registers a branch tracker under Plugin.trackers. The walker threads this tracker through every ref in a bash chain, so git checkout feat && git commit evaluates the commit rule against feat — the branch the agent is checking out to — not the branch that was current before the chain ran. The tracker performs a three-way discrimination on its stored value:
  • Value — a git checkout <name> or git switch <name> with a statically-resolvable target was observed; the tracker holds the resolved name.
  • Unknown — a checkout was observed but the target was dynamic (git checkout $VAR); the tracker holds "unknown" to signal “a change happened but I can’t name the new value.”
  • Missing (sentinel NO_CHECKOUT_IN_CHAIN) — no branch-changing command appeared in the chain; the predicate falls back to shelling out.

Tracker extensions

The git plugin extends the built-in cwd tracker with a --git-dir= and --work-tree= flag parser registered under trackerExtensions.cwd.git. This lets the walker correctly track the effective working directory when git is invoked with explicit directory overrides — for example, git --git-dir=/repo/.git --work-tree=/repo commit evaluates the commit at /repo, not at the shell’s ambient cwd.

Build docs developers (and LLMs) love