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.

pi-steering is a deterministic guardrail layer that sits between your pi agent and the tools it invokes. You declare TypeScript rules that gate bash, write, and edit tool calls; the engine parses every command with a full bash AST, walks a per-call tracker state, matches against your rules, and returns a block verdict before pi executes anything. The key difference from every regex-on-raw-string approach is that pi-steering reasons about shell structure, not text patterns — which is the only correct way to make safety rules that adversarial or accidental shell composition cannot bypass.

The Problem With Substring Matching

Naive substring or regex matching on the raw command string has two failure modes that make guardrails unreliable in practice. False positives fire on safe commands that merely mention a forbidden pattern as data:
echo 'git push --force'   # the string "git push --force" is an argument to echo, not a git invocation
A raw-string rule for git push --force blocks this harmless echo, creating friction and eroding trust in the guardrail. False negatives let dangerous commands through via shell wrapper indirection:
sh -c 'git push --force'   # raw match sees "sh", misses the git invocation inside
sudo git push --force       # raw match may anchor on "git push" but miss the "sudo" prefix
cd /repo && git push --force  # raw match evaluates against the wrong command
pi-steering resolves both failure modes by parsing the command with a full bash AST, then walking the resulting syntax tree. The basename of each extracted command ref is git in all three wrapper cases above — not sh, not sudo, not the raw string — so the same ^git\s+push.*--force(?!-) pattern fires correctly on all of them. And echo 'git push --force' never fires because the extracted ref’s basename is echo.

Three-Layer Architecture

┌──────────────────────────────────────────────┐
│  unbash  (bash AST parser, 3rd-party)        │
└──────────────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  unbash-walker  (this repo, Phase 1)          │
│    extractAllCommandsFromAST                 │
│    expandWrapperCommands                     │
│    effectiveCwd                              │
│    CommandRef + basename normalization       │
└──────────────────────────────────────────────┘


┌──────────────────────────────────────────────┐
│  pi-steering  (this repo, Phase 2)           │
│    rule schema (pattern / requires /         │
│      unless / when.cwd / reason)             │
│    walk-up + merge + session_start loader    │
│    inline override comments + audit          │
└──────────────────────────────────────────────┘
The unbash parser produces a structured AST from the raw command string. unbash-walker traverses that AST to extract individual command refs (one per chained command), resolve their effective working directories, and normalize basenames regardless of path prefix or wrapper depth. pi-steering builds on top of this to provide a complete rule engine with config loading, plugin composition, session-level state, and pi lifecycle integration.

Key Features

AST-Backed Matching

Every bash command is fully parsed before evaluation. Rules match against structured command refs, not raw strings — wrapper commands like sh -c, sudo, env -i, and similar are automatically expanded so inner commands are visible to your rules.

Per-Command cwd Scoping

The walker resolves the effective working directory for each extracted ref in a chain. A rule scoped with when.cwd evaluates cd /tmp && git commit against /tmp, not the session root — giving you precise, per-ref cwd constraints.

Plugin-First Composition

Plugins are TypeScript modules that bundle predicates, rules, observers, and walker trackers under a single name. Users opt in via plugins: [...]; plugins ship as npm packages. The git plugin ships by default; community plugins use the pi-steering-package keyword for discoverability.

Stateful Observers with `when.happened`

Observers watch tool_result events and write typed session entries. Rules gate on those entries via when.happened, enabling “must run X before Y” guardrails that survive across tool calls within the same user prompt — without any per-turn bookkeeping in your rules.

Compile-Time Safety via `defineConfig`

The defineConfig helper threads generics through your entire config so that rule names, plugin names, observer names, and when.happened.event references are all type-checked at tsc --noEmit time. Typos become compile errors, not silent misconfiguration.

`&&`-Chain Speculative Allow

Agents frequently chain commands like sync && cr. pi-steering synthesizes speculative session entries for &&-reachable refs so a rule requiring a prior observer event isn’t naively blocked when the observer and the guarded command appear in the same chain.

Walk-Up Config Merge

The loader walks from the session’s launch directory up to $HOME, importing every .pi/steering/index.ts it finds. Inner layers (closer to the project root) take precedence over outer layers on name collisions, so per-project rules can override user-level defaults without breaking them.

Hot Config Reload

Running /reload inside pi re-evaluates your .pi/steering/index.ts without restarting pi. The loader cache-busts the dynamic import so edits to your config (and to plugin .ts source, when the plugin ships source as its entry) take effect immediately.

Package Ecosystem

pi-steering ships as four related packages in the cad0p/pi-steering-hooks monorepo:
  • pi-steering — the core engine. Exports defineConfig, DEFAULT_RULES, DEFAULT_PLUGINS, the full rule/plugin/observer schema types, and walker primitives re-exported from unbash-walker. This is the package you install to use pi-steering.
  • unbash-walker — the AST utility layer. Provides extractAllCommandsFromAST, expandWrapperCommands, cwdTracker, CommandRef, basename normalization, and the Tracker / Modifier API for walker extensibility. General-purpose infrastructure planned for extraction into its own repository once the PoC proves its value.
  • pi-steering-flags — the first official community-style plugin. Ships requiresFlag and allowlistedFlagsOnly predicates plus helper primitives for building flag-constraint rules.
  • pi-steering-commit-format — ships the commitFormat predicate and a commitFormatFactory for composing custom commit-message format checkers. Bundled formats include Conventional Commits 1.0.0 (Angular preset type allowlist) and bracketed JIRA-style references.

Requirements

pi-steering requires Node ≥ 22. The config loader reads .pi/steering/index.ts files via Node’s native type-stripping (no tsx or ts-node runtime dependency). On older Node versions the loader throws with an upgrade message at startup.

Get Started

Quickstart

Install pi-steering, write your first rule, and test it against your pi agent in under five minutes.

Build docs developers (and LLMs) love