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.

Every pi-steering config starts with two default bundles layered on automatically: DEFAULT_RULES (four domain-agnostic safety rails) and DEFAULT_PLUGINS (the git plugin). These defaults apply even when your .pi/steering/index.ts passes no rules or plugins of its own — the loader merges them as the outermost layer so your explicit config always takes precedence on name collisions.

Default rules

The four rules in DEFAULT_RULES cover the most common classes of irreversible or disruptive bash commands. Each pattern runs against the AST-extracted command string per ref, so wrapper forms like sh -c 'git push --force' and chained forms like cd /repo && git push --force are caught the same way as bare invocations.

no-force-push

Blocks force-push to any remote. Allows --force-with-lease, which is the safe alternative because it fails if the remote has advanced since your last fetch.
^git\b(?:\s+-{1,2}[A-Za-z]\S*(?:\s+\S+)?)*\s+push\b.*(?:--force(?!-with-lease)|\s-f(?:\s|$))
The pre-subcommand flag slot (?:\s+-{1,2}[A-Za-z]\S*(?:\s+\S+)?)* ensures that forms like git -C /path push --force, git -c key=val push --force, and git --git-dir=/x push --force are all caught. The negative lookahead (?!-with-lease) rules out --force-with-lease without needing a separate unless clause. Block reason (written for the agent): Force push rewrites remote history and can destroy teammates’ work. Use git push --force-with-lease if you must, or create a new commit instead.

no-hard-reset

Blocks git reset --hard in any invocation form. Hard reset permanently discards all uncommitted changes in the working tree — there is no undo path once it completes.
^git\b(?:\s+-{1,2}[A-Za-z]\S*(?:\s+\S+)?)*\s+reset\s+--hard\b
The same pre-subcommand flag broadening as no-force-push catches git -C /other reset --hard and git -c key=val reset --hard. Block reason: Hard reset discards uncommitted changes permanently. Use git stash to save work first, or git reset --soft to keep changes staged.

no-rm-rf-slash

Blocks rm -rf / in any flag combination form: separated flags (-r -f), long-form flags (--recursive --force), mixed case (-Rf), reversed order (-fr), and operating on the filesystem root /. This rule has noOverride: true — it is a hard block that cannot be bypassed by any override comment or config setting.
^rm\b(?=.*(?:-[A-Za-z]*[rR][A-Za-z]*|--recursive))(?=.*(?:-[A-Za-z]*f[A-Za-z]*|--force)).*\s/(?:\s|$)
Two independent lookaheads ensure both the recursive flag and the force flag are present in any order or form, then check that / appears as a path argument. The basename anchor (^rm\b) ensures echo 'rm -rf /' is correctly not flagged. Block reason: Recursive force-delete from root is catastrophic and irreversible. Specify a safe path (e.g. a subdirectory of the project or a temp dir).
no-rm-rf-slash has noOverride: true — it is a hard block. Even when you set defaultNoOverride: false on your config (making all untagged rules advisory), this rule cannot be overridden by an inline # steering-override: comment. The noOverride: true field on the rule is explicit and takes precedence over the config-level default.

no-long-running-commands

Blocks dev servers, bundler watchers, and test runners that would loop indefinitely and stall the agent. The pattern covers npm, yarn, pnpm, npx, webpack, jest, tsc, nodemon, vite, astro, next, deno, and bun in their watch/dev/serve modes.
^(?:npm\s+(?:run\s+dev|start)|yarn\s+(?:dev|start)|pnpm\s+(?:run\s+)?(?:dev|start)|npx\s+.*--watch|webpack\s+(?:--watch|serve)|jest\s+--watch|nodemon\b|tsc\s+--watch|vite(?:\s+(?:dev|serve|preview))?(?!\s+[A-Za-z])|astro\s+(?:dev|preview)|next\s+dev|deno\s+task\s+(?:dev|start|serve)|bun\s+(?:dev|run\s+dev))\b
Block reason: Long-running dev servers and watchers block the agent loop. Ask the user to run it manually in another terminal, or use a background-process tool. The comment in the source explicitly notes this list is representative, not exhaustive — add your own watchers via .pi/steering.ts if a framework isn’t listed.

Default plugins

DEFAULT_PLUGINS ships one plugin: gitPlugin from pi-steering/plugins/git.

gitPlugin

The git plugin contributes:
  • Predicates: branch, upstream, commitsAhead, hasStagedChanges, isClean, remote — registered on PiSteeringPredicates so rules can write when: { branch: /^main$/ } without importing anything from the plugin at authoring time.
  • Rules: no-main-commit-github (github-flavored, first-match-wins) and no-main-commit (generic fallback) — both block direct commits to main, master, mainline, and trunk. Both rules are overridable: the agent can annotate a bash call with # steering-override: no-main-commit — <reason> to bypass the generic rule, or # steering-override: no-main-commit-github — <reason> for the github variant. Disable individually via disabledRules: ["no-main-commit"] / ["no-main-commit-github"], or opt out of the whole plugin with disabledPlugins: ["git"].
  • Tracker: branch — tracks in-chain git checkout and git switch so the branch predicate reflects the statically-resolved branch after a checkout within the same &&-chain.
  • Tracker extension: cwd.git — layers --git-dir= and --work-tree= flag parsing onto the built-in cwd tracker, so git --git-dir=/repo/.git status resolves the effective cwd correctly for downstream rules.
The git plugin is default-on because branch-aware and commit-discipline rules cover the most common agent footgun (committing to main by accident). Explicit opt-out is lower friction than requiring every user to wire the plugin in themselves.

Opting out

All three opt-out paths are typo-checked at compile time by defineConfig’s generics:
import { defineConfig } from "pi-steering";
import gitPlugin from "pi-steering/plugins/git";

// Disable one default rule — keeps the git plugin's predicates and tracker:
defineConfig({ disabledRules: ["no-force-push"] });

// Disable the git plugin entirely — drops predicates, tracker, and no-main-commit:
defineConfig({ disabledPlugins: ["git"] });

// Disable ALL defaults — drops DEFAULT_RULES and DEFAULT_PLUGINS entirely:
defineConfig({ disableDefaults: true });
When you pass plugins: [gitPlugin] explicitly, the git plugin’s rule and observer names join the AllRuleNames and AllPluginNames unions for typo-checking purposes. DEFAULT_PLUGINS provides runtime registration, but it’s the explicit plugins: [gitPlugin] in your config that enables compile-time typo detection on disabledRules: ["no-main-commit"]. The names of the four DEFAULT_RULES are always part of AllRuleNames regardless, since they’re projected from the DEFAULT_RULES array directly.
To replace a default rule with a stricter version — for example, tightening no-force-push to also block --force-with-lease — disable the original by name and add your own rule under a new name. Reusing the same name has two failure modes: with no disabledRules, both rules stay active (your customization silently double-fires); with disabledRules, both are dropped (silent fail-open). Always use a fresh name for replacements.
import { defineConfig } from "pi-steering";

export default defineConfig({
  disabledRules: ["no-force-push"],
  rules: [
    {
      name: "no-force-push-strict",
      tool: "bash",
      field: "command",
      // Also blocks --force-with-lease
      pattern: /^git\b.*\s+push\b.*(?:--force|--force-with-lease|-f(?:\s|$))/,
      reason: "Force push in any form is blocked. Coordinate with your team instead.",
    },
  ],
});

Build docs developers (and LLMs) love