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.
pi-steering is TypeScript-first. All types are exported from the package root via import type { ... } from "pi-steering". This page documents the most important types for rule authors and plugin authors, organized by the surface they belong to.
Core config types
SteeringConfig
Top-level config shape. What a user’s .pi/steering.ts default-exports, either via defineConfig or satisfies SteeringConfig. All fields are optional — the loader merges multiple layers into a single effective config.
interface SteeringConfig {
defaultNoOverride?: boolean;
disabledRules?: readonly string[];
disabledPlugins?: readonly string[];
disableDefaults?: boolean;
failOnWarnings?: boolean;
plugins?: readonly Plugin[];
rules?: readonly Rule[];
observers?: readonly Observer[];
}
The five array-typed fields are readonly. This is load-bearing: DefineConfigInput extends SteeringConfig and narrows each slot with const-generic-aware subtypes for typo-checking. See defineConfig for the narrowed input shape.
Rule
A single steering rule — discriminated union over the three gateable pi tools. The tool discriminant determines which field values are valid.
type Rule<ObsName extends string = string, Writes extends string = string> =
| BashRule<ObsName, Writes>
| WriteRule<ObsName, Writes>
| EditRule<ObsName, Writes>;
All three variants share BaseRule:
interface BaseRule<ObsName extends string = string, Writes extends string = string> {
/** Unique rule identifier. Used in override comments and audit logs. */
name: string;
/** Main match predicate tested against the chosen field value. */
pattern: Pattern;
/** Optional extra AND predicate. Rule fires only if this also matches. */
requires?: Pattern | PredicateFn;
/** Exemption predicate. When provided and matches, rule does NOT fire. */
unless?: Pattern | PredicateFn;
/** Composable predicate block. See TopLevelWhenClause. */
when?: TopLevelWhenClause<Writes>;
/** Message shown to the agent when blocked. */
reason: string | ReasonFn;
/** Override policy. Omitted: falls back to SteeringConfig.defaultNoOverride. */
noOverride?: boolean;
/** Observer attached to this rule — inline or referenced by name. */
observer?: Observer | ObsName;
/** Session-entry event types this rule's onFire may write. */
writes?: readonly string[];
/** Side-effect hook invoked when the rule fires, before the block verdict. */
onFire?: (ctx: PredicateContext) => void | Promise<void>;
}
Tool-specific discriminants:
interface BashRule extends BaseRule {
tool: "bash";
field: "command"; // Only valid field for bash rules
}
interface WriteRule extends BaseRule {
tool: "write";
field: "path" | "content";
}
interface EditRule extends BaseRule {
tool: "edit";
field: "path" | "content";
}
Observer
A reactive hook that runs on tool_result events. Typically writes session-entry records that later predicates read via when.happened.
interface Observer {
/** Unique name. Referenced from Rule.observer as a string. */
name: string;
/** Session-entry event types this observer's onResult may write. */
writes?: readonly string[];
/** Filter narrowing which tool_result events fire onResult. */
watch?: ObserverWatch;
/** Called on every matching tool_result event. */
onResult: (event: ToolResultEvent, ctx: ObserverContext) => void | Promise<void>;
}
Plugin
Distribution unit for rule packs and extension points. Plugins register predicates, rules, observers, trackers, and tracker extensions.
interface Plugin {
/** Unique plugin identifier. */
name: string;
/** Predicate handlers keyed by the when.<key> slot they register. */
predicates?: Record<string, AnyPredicateHandler>;
/** Rules the plugin suggests. Users opt out via disabledRules. */
rules?: readonly Rule[];
/** Observers the plugin ships. Referenced by name from rules. */
observers?: readonly Observer[];
/** New walker state dimensions the plugin introduces. */
trackers?: Record<string, Tracker<unknown>>;
/** Modifiers added to an existing tracker. */
trackerExtensions?: Record<
string,
Record<string, Modifier<unknown> | readonly Modifier<unknown>[]>
>;
}
ObserverWatch
Filter applied to tool_result events before Observer.onResult runs. Omitting watch entirely fires the observer on every tool result.
interface ObserverWatch {
/** Only fire on results from this tool. */
toolName?: "bash" | "read" | "write" | "edit" | (string & {});
/**
* Per-field regex constraints on the tool INPUT.
* Observer fires only when every listed field matches.
*/
inputMatches?: Record<string, Pattern>;
/** Constrain by exit code or success/failure classification. */
exitCode?: number | "success" | "failure" | "any";
}
Predicate types
PredicateContext
Context passed to every predicate — both escape-hatch PredicateFns and plugin-registered PredicateHandlers.
interface PredicateContext {
/** Session cwd (or effective cwd of the command for bash rules). */
cwd: string;
/** Which pi tool is being gated. */
tool: "bash" | "write" | "edit";
/** Tool input — evaluator populates whichever fields apply to tool. */
input: PredicateToolInput;
/**
* Engine-maintained agent-loop counter.
* Bumped on each pi agent_start event.
*/
agentLoopIndex: number;
/** Run a shell command. Memoized per (cmd, args, cwd) within a single tool_call. */
exec: (cmd: string, args: string[], opts?: ExecOpts) => Promise<ExecResult>;
/** Append a typed entry into pi's session JSONL. */
appendEntry: <T>(customType: string, data?: T) => void;
/** Read all prior typed entries of the given custom type. */
findEntries: <T>(customType: string) => Array<{ data: T; timestamp: number }>;
/**
* Walker state snapshot for the command being evaluated.
* Populated only for bash rules. Undefined for write / edit rules.
*/
walkerState?: Readonly<WhenWalkerState>;
}
WhenWalkerState
Shape of the walker-state snapshot on PredicateContext.walkerState. The type is open-ended — plugins register new tracker dimensions that appear as additional keys.
interface WhenWalkerState {
/** Effective cwd at this ref, per cwdTracker. "unknown" when unresolvable. */
readonly cwd: string;
/** Env map at this ref, per envTracker. */
readonly env: EnvState;
/** Additional tracker and reserved keys (e.g. branch, events). */
readonly [key: string]: unknown;
}
TopLevelWhenClause
The composable predicate block attached to Rule.when. Each plugin-registered predicate (filtered for reserved keys) gets a typed leaf field. The not?: operator allows one level of negation.
Key built-in leaves:
// cwd — matches the command's effective cwd
when: { cwd: /\/workspace\// }
when: { cwd: { pattern: /\/workspace\//, onUnknown: "allow" } }
// happened — fires when an event has NOT occurred in scope
when: { happened: { event: "sync-done", in: "agent_loop" } }
// condition — escape-hatch arbitrary logic
when: { condition: (ctx) => ctx.input.args?.length === 0 }
// not — logical negation of a sub-clause
when: { not: { cwd: /\/personal\//, onUnknown: "block" } }
TopLevelWhenClause is generic over Writes (the union of declared writes literals in scope), which narrows happened.event and happened.since to known event strings inside defineConfig.
Pattern and Patterns
/** A plain string (treated as regex source) or a RegExp. */
type Pattern = string | RegExp;
/** A single Pattern or an OR-of-patterns array (any match counts). */
type Patterns = Pattern | Pattern[];
PredicateVerdict
/** Trinary verdict returned by predicate handlers. */
type PredicateVerdict = boolean | "unknown";
"unknown" signals the handler could not resolve its value. The engine applies the onUnknown policy ("block" by default) to project back to a definite boolean.
PredicateFn
Escape-hatch predicate — arbitrary user logic evaluated with a PredicateContext. Used as the value of when.condition.
type PredicateFn = (ctx: PredicateContext) => boolean | Promise<boolean>;
PredicateHandler<T>
Plugin-registered predicate. Differs from PredicateFn in that the first argument is the structured value the user supplied under their when.<key> slot.
type PredicateHandler<A = unknown> = (
args: A,
ctx: PredicateContext,
) => PredicateVerdict | Promise<PredicateVerdict>;
Use definePredicate<T> to declare typed handlers without casting. See also AnyPredicateHandler (= PredicateHandler<any>) — the type-erased alias used at Plugin.predicates registry boundaries.
ReasonFn
Dynamic block-reason function. When Rule.reason is a function, the evaluator invokes it with the same PredicateContext the predicates saw, then prefixes the result with [steering:<rule>@<source>].
type ReasonFn = (ctx: PredicateContext) => string | Promise<string>;
Use the function form when the reason text depends on runtime state — for example, the walker’s effective cwd or a resolved branch name. If the function throws, the evaluator logs to console.warn and emits a fallback reason; the block verdict still lands.
Walker types
These types are re-exported from unbash-walker for plugin authors. See the unbash-walker API reference for full documentation on the functions that produce and consume them.
CommandRef
type CommandRef = {
node: Command; // The unbash AST Command node
source: string; // The original bash source string
group: number; // Group ID — commands in the same && / || chain share a group
joiner?: "|" | "&&" | "||" | ";"; // Operator connecting this command to the next
};
Word
An unbash word with structured access to its value:
.value — the lexical value (quote-stripped, normalized)
.text — the raw source token (preserves quoting)
PredicateContext.input.args carries Word[] for bash rules, enabling quote-aware argument inspection without splitting on whitespace.
Tracker<T>
Interface plugin authors implement to introduce new walker state dimensions:
interface Tracker<T> {
/** Starting value. Overridden per-call via walk()'s initialState parameter. */
initial: T;
/** Sentinel value emitted when a modifier returns undefined (unresolvable). */
unknown: T;
/**
* Modifiers keyed by command basename.
* An array value applies modifiers left-to-right on the same ref.
*/
modifiers: Record<string, Modifier<T> | Modifier<T>[]>;
/**
* How subshell boundaries affect this dimension.
* Defaults to "isolated" (changes inside a subshell don't escape).
*/
subshellSemantics?: "isolated" | "global";
}
Modifier<T, TAll>
The two modifier scope variants:
// "sequential" — propagates to this command AND all subsequent siblings
type Modifier<
T,
TAll extends Record<string, unknown> = Record<string, unknown>
> =
| {
scope: "sequential";
apply(args: readonly Word[], current: T, allState: Readonly<TAll>): T | undefined;
}
| {
scope: "per-command";
apply(args: readonly Word[], current: T, allState: Readonly<TAll>): T | undefined;
};
"sequential" — use for shell-level state changes (cd, git checkout). Updates the threaded value that propagates to subsequent commands.
"per-command" — use for per-invocation overrides (git -C DIR, make -C DIR). Updates only the snapshot recorded for this command; the threaded value is unchanged.
Return undefined from apply to signal “can’t resolve statically.” The walker substitutes the tracker’s unknown sentinel.
EnvState
type EnvState = ReadonlyMap<string, string>;
The shape envTracker produces per ref. Read via ctx.walkerState.env.get("NAME"). Returns undefined for names the walker hasn’t seen.
Testing types
These types are exported from pi-steering/testing (and re-exported from the package root).
Harness
Returned by loadHarness. Build-once, invoke-many handle for driving the engine against a scenario in tests.
interface Harness {
evaluate: EvaluatorRuntime["evaluate"];
dispatch: ObserverDispatcher["dispatch"];
readonly config: SteeringConfig;
readonly resolved: ResolvedPluginState;
readonly diagnostics: readonly SteeringDiagnostic[];
}
Unlike production, loadHarness does NOT throw on error-class diagnostics — tests assert directly on harness.diagnostics. Check harness.diagnostics before treating the harness verdict as production-faithful.
LoadHarnessOptions
interface LoadHarnessOptions {
/** The config under test. */
readonly config: SteeringConfig;
/**
* Prepend DEFAULT_PLUGINS and DEFAULT_RULES to the config.
* Default: false.
*/
readonly includeDefaults?: boolean;
/**
* Host for exec / appendEntry. Defaults to an in-memory stub
* whose exec rejects with a clear error.
*/
readonly host?: EvaluatorHost;
}
MatrixCase and MatrixResult
Used by runMatrix for adversarial batch evaluation:
interface MatrixCase {
readonly name: string;
readonly event: ToolCallEvent | ToolCallShorthand;
readonly expect:
| "block"
| "allow"
| { readonly block: true; readonly rule?: string };
readonly cwd?: string;
}
interface MatrixResult {
readonly total: number;
readonly passed: number;
readonly failed: number;
readonly cases: ReadonlyArray<MatrixCaseResult>;
}
runMatrix never throws — failures surface in result.cases. Pair with formatMatrix to render a human-readable ASCII report.
MockContextOptions
Knobs for mockContext and (a subset for) testPredicate:
interface MockContextOptions {
cwd?: string; // Defaults to "/tmp/test"
agentLoopIndex?: number; // Defaults to 0
tool?: "bash" | "write" | "edit"; // Defaults to "bash"
input?: PredicateToolInput;
walkerState?: Partial<WhenWalkerState> & Record<string, unknown>;
exec?: (cmd: string, args: readonly string[], opts?: ExecOpts) => ExecResult | Promise<ExecResult>;
entries?: ReadonlyArray<MockEntry>;
toolCallEvents?: Readonly<Record<string, readonly SyntheticEntry[]>>;
}
Build entries for when.happened tests via priorEntry(customType, data, { agentLoopIndex }) — this stamps the reserved _agentLoopIndex tag in the exact shape the live engine produces.
Diagnostic types
SteeringDiagnostic
Structured issue surfaced while loading a steering config. The shape is stable — tooling and tests dispatch on kind without parsing message.
interface SteeringDiagnostic {
/** "warning" — safe to ignore in fail-soft mode; "error" — always throws. */
type: "warning" | "error";
/** Discriminator for programmatic dispatch. */
kind: SteeringDiagnosticKind;
/** Human-readable message with context. */
message: string;
/** Source file path, when applicable. */
path?: string;
}
SteeringDiagnosticKind
Stable enum of diagnostic categories:
| Kind | Surface | Meaning |
|---|
"layer-form-coexistence" | Loader | Both .pi/steering.ts and .pi/steering/index.ts exist at the same directory |
"layer-import-failed" | Loader | A config layer’s dynamic import threw |
"layer-stray-file" | Loader | A non-.ts file lives under .pi/steering/ |
"plugin-name-collision" | Loader | Two layers register a plugin with the same name |
"rule-name-collision" | Loader | A single layer declares two rules with the same name |
"observer-name-collision" | Loader | A single layer declares two observers with the same name |
"tracker-name-collision" | Loader | Two plugins register a tracker under the same name (hard error) |
"predicate-collision" | Plugin merger | Two plugins register a predicate under the same when.<key> |
"observer-collision" | Plugin merger | Two plugins ship an observer with the same name |
"rule-collision" | Plugin merger | Two plugins ship a rule with the same name |
"extension-orphan" | Plugin merger | trackerExtensions references an unregistered tracker |
"reserved-tracker-name" | Plugin merger | Plugin attempts to register a tracker under a reserved name |
"reserved-predicate-key" | Plugin merger | Plugin attempts to register a predicate under not, onUnknown, etc. |
"invalid-name" | Plugin merger | A plugin / rule / observer name contains disallowed characters |
Constants
DEFAULT_RULES
The four rules the engine ships by default:
| Name | What it blocks |
|---|
"no-force-push" | git push --force / -f (allows --force-with-lease) |
"no-hard-reset" | git reset --hard |
"no-rm-rf-slash" | rm -rf / and equivalent flag orderings |
"no-long-running-commands" | Dev servers and watch-mode processes (npm/yarn/pnpm dev, vite, next dev, etc.) |
Disable individual defaults via disabledRules. Disable all defaults via disableDefaults: true.
DEFAULT_PLUGINS
const DEFAULT_PLUGINS = [gitPlugin] as const satisfies readonly Plugin[];
Ships the git plugin (pi-steering/plugins/git), which contributes the branch, upstream, commitsAhead, hasStagedChanges, isClean, and remote predicates, the no-main-commit rule, a branch walker tracker, and cwd tracker extensions for --git-dir / --work-tree flags.
AGENT_LOOP_INDEX_KEY
const AGENT_LOOP_INDEX_KEY = "_agentLoopIndex" as const;
The reserved session-entry payload key the engine stamps on every appendEntry write. Exposed so plugin authors inspecting raw session entries via findEntries can reference the constant instead of hardcoding the underscore-prefixed string literal. Also used by priorEntry in the testing surface.