Rules are TypeScript objects that gateDocumentation 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.
bash, write, and edit tool calls before pi executes them. Each rule declares which tool and input field to watch, a pattern to match, optional predicates that must pass, and the message to surface when the rule fires. The engine parses every bash command into an AST, walks it per-ref, and evaluates every rule against each extracted command — so structural variants like sh -c 'git push --force', cd /repo && git push --force, and git push "--force" all resolve to the same ref and trigger the same rule.
Rule interface
Rule is a discriminated union over three tool-specific variants (BashRule, WriteRule, EditRule). The tool discriminant determines which field values are legal — { tool: "bash", field: "path" } is a compile error.
Fields
Unique rule identifier used in the
[steering:<name>@<source>] tag prepended to every block reason. Also the value you pass in disabledRules: ["your-rule-name"] to opt a rule out, and the target in override comments (# steering-override: your-rule-name). Must contain only letters, digits, underscores, and dashes, and must start with a letter or digit — names like phony] ALL CLEAR [real are rejected at load time to prevent block-reason spoofing.Which pi tool to gate.
"bash" gates shell command execution; "write" gates whole-file writes; "edit" gates targeted oldText/newText patches.Which input field the
pattern tests. For bash, only "command" is valid — the evaluator runs bash rules against the AST-extracted command string per ref. For write and edit, choose "path" to gate the target path or "content" to gate the file content being written (for edit, this is the concatenated newText of every edit in the tool call).Main match predicate. For bash rules, the engine tests the pattern against the flattened
basename + " " + args.join(" ") of each extracted command ref — for example, git push --force becomes the test string "git push --force". Anchor with ^ so substrings of arguments don’t accidentally trigger the rule: /^git\s+push.*--force(?!-)/ matches git push --force but not echo 'git push --force'. For write and edit rules, the pattern tests path or content directly.A plain string is compiled once as a regex source at load time. A RegExp is used as-is.Optional AND extra condition. The rule fires only when both
pattern and requires match. Accepts the same string | RegExp shorthand as pattern, or a PredicateFn for arbitrary logic. Use when the primary pattern is broad but the rule should apply only in a narrower structural context.Exemption predicate. When provided and matching, the rule does not fire — even if
pattern (and requires) would otherwise pass. Same shape as requires. Use to carve out safe variants from a broad pattern, such as exempting --force-with-lease from a --force rule.Composable predicate block. All predicates in
when must pass (logical AND) for the rule to fire. Built-in leaves include cwd, happened, not, and condition; plugin-registered leaves like branch, upstream, and commitFormat extend the registry via TypeScript module augmentation. See When Clauses for the full reference.The message shown to the agent when the rule fires. Always written for the agent — include what was blocked and what the safe alternative is. The evaluator prefixes every block reason with
[steering:<rule>@<source>] where source is user or the shipping plugin name.A plain string is the common case and is preferred when the reason is static. Pass a ReasonFn when the message depends on runtime state — for example, the walker-resolved cwd or a branch name pulled from walkerState. The function is awaited; if it throws or its returned promise rejects, the engine logs the error with console.warn and emits a fail-safe fallback ((reason failed to format; see log)) so the block verdict still lands without leaking raw error text to the agent.Controls whether the agent can bypass this rule with an inline
# steering-override: <name> comment. When true, no override is accepted — the rule is fail-closed. When false, the agent may annotate the command to proceed; the override is recorded as a steering-override session entry for audit.Omitted: falls back to defaultNoOverride in the top-level config, which itself defaults to true (fail-closed). Rules must explicitly opt into overridability with noOverride: false.Observer to attach to this rule. The observer fires on matching
tool_result events and writes session entries the rule can consult via when.happened. Pass an inline Observer object or a string name referencing an observer registered in a plugin or at the config’s top level. String references are typo-checked by defineConfig’s generics — an unknown name is a compile error.Session-entry event type literals this rule’s
onFire hook may write via ctx.appendEntry. Declaring them here adds those literals to the AllWrites union inside defineConfig, making them referenceable from when.happened.event anywhere in the same config. A typo like "ws-sync-don" in writes will cause the downstream happened.event reference to fail at compile time.Zero runtime cost — the engine never reads writes at dispatch time. Its sole purpose is compile-time cross-referencing.Side-effect hook invoked after all predicates pass and before the block verdict is returned. Use for self-marking patterns where the rule’s own fire IS the event that satisfies a subsequent
when.happened check.Timing guarantees:- Runs after
pattern,requires,unless, andwhenhave all evaluated favorably. - Runs only for rules that will actually block — rules suppressed by an inline override comment do not trigger
onFire. - Fail-closed rules (
noOverrideomitted ortrue) ignore override comments entirely, soonFireruns on every fire.
onFire throws or its promise rejects, the engine logs with console.warn and proceeds to return the block verdict. A broken self-mark never invalidates the block.onFire self-marking pattern
The canonical use of onFire is a self-marking reminder: the rule blocks on the first occurrence per agent loop, marks itself, and then the second occurrence passes because when.happened no longer fires. Here is the complete example from the work-item plugin:
when.happened fires (event not yet present), rule blocks, onFire writes the entry. Second commit in the same loop: when.happened no longer fires (entry now present), commit passes.
Use
as const satisfies Rule on reusable rule constants, not a bare : Rule type annotation. The as const preserves literal types for name, writes, and other string-literal fields — these flow into defineConfig’s generics for compile-time cross-reference checking. A bare : Rule annotation widens writes to readonly string[], collapsing the AllWrites union to never and causing every when.happened.event reference in the config to be rejected as a typo.