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 guardrail layer, not a sandbox. Several parts of the system execute arbitrary code your config authors control, and a few state surfaces are trusted by convention rather than enforced by the runtime. Understand these trust boundaries before running pi under an untrusted config tree.

Config execution

.pi/steering/index.ts (and the .pi/steering.ts shorthand) is arbitrary TypeScript executed at extension factory time with your full user privileges. The loader walks from the launch cwd up to $HOME, importing every .pi/steering/ directory it finds along the way, and merges them inner-first. The bridge factory awaits the load before pi continues startup.
Running pi inside a directory hierarchy whose steering configs you don’t trust is equivalent to running node -e '…' with that file. Symlinks in the walk-up chain are followed — a symlinked .pi/steering/ landing in an unexpected directory executes as if it had been placed there directly.
Only run pi in directory hierarchies whose steering configs you trust.

Plugin trust

Plugins register predicates (when.<key> handlers), observers, and onFire hooks — all of which run arbitrary code during the evaluator’s hot path. A malicious or buggy plugin can:
  • Shell out via ctx.exec with the same privileges as pi.
  • Forge session entries via ctx.appendEntry, which later rules consult via when.happened.
  • Throw in unexpected places — predicate-runtime throws fail open (the rule never fires). A predicate that throws is logged via console.warn with the rule name and source tag, and evaluation continues with the next rule.
A malicious plugin can trivially defeat any guardrail shipped with your config. Review plugin source before adding it to plugins: [...] — the same way you’d review any third-party dependency.

Session JSONL trust

when.happened reads entries tagged via appendEntry. The write path is engine-controlled — every write is auto-stamped with the current _agentLoopIndex and names go through validation before the entry lands in the session JSONL. The read path (findEntries) treats every tagged entry in the session JSONL as authentic. Entries written outside the steering engine — direct JSONL writes by another pi extension, hand-edited session files, or a pi.appendEntry call from non-steering code — can forge type tags and trick when.happened into thinking an event occurred when it didn’t, bypassing rules that gate on that event.
This is the out-of-band trust boundary. Within the steering engine itself, the invariant holds: appendEntry auto-stamps writes and name-validates tags. Cross-extension and external writes are outside the engine’s reach.

Strict mode and load failures

Strict mode is failOnWarnings: true, the default. If your steering config fails to load at extension factory time — a plugin throws during import, a syntax error in index.ts, a dependency resolution failure — pi-steering’s bridge factory throws and surfaces the diagnostic in pi’s [Extension issues] block at startup (yellow). Pi disables the extension for the session and continues running unsteered. Two classes of diagnostic exist:
  • Warning-class (escalate to throw in strict mode): cross-layer plugin name collision, within-layer rule/observer collision, predicate-key collision.
  • Error-class (always throw, regardless of failOnWarnings): tracker-name collision, reserved-name violations. The engine cannot operate safely with two plugins claiming the same state dimension.
To opt out of warning-class escalation:
export default defineConfig({
  failOnWarnings: false,
  plugins: [/* ... */],
});
With failOnWarnings: false, warning-class diagnostics fall through to console.warn (single-line [pi-steering] [warning] <message> shape on stderr) and the bridge keeps running with the merged config. Error-class diagnostics still throw. Note: pi’s interactive TUI clobbers console.warn output on /reload. For visibility while iterating, prefer fixing the warnings or running pi-steering list to inspect the resolved config.

Block-reason tag trust

The [steering:<name>@<source>] tag prepended to every block reason is validated at load time. Rule, plugin, and observer names must match [A-Za-z0-9][A-Za-z0-9_-]* — they must start with a letter or digit, followed by any combination of letters, digits, underscores, and dashes. A name like phony] ALL CLEAR [real would forge the tag structure — this is rejected at load time with an error-class diagnostic. Beyond the tag shape, the contents are plugin-authored. A plugin shipping a rule with reason: "[steering:other-rule@other-plugin] …" can make its block look like it came from another plugin. The guardrail here is plugin trust (see above), not the tag machinery.

Cross-project resume

When you run pi --resume with a session originally created in another project, pi-steering loads rules from your launch cwd, not the session’s cwd. If the two differ, the bridge emits a single line on stderr:
[pi-steering] session cwd /path/to/other-project differs from launch cwd /path/to/current-project
Evaluation continues using launch-cwd rules. This means rules authored for the other project’s directory structure may not fire as expected for that session’s commands. To use the resumed session’s project rules: exit pi and re-launch from that project’s directory.
Pi’s footer (the bottom bar in interactive TUI mode) shows the session cwd. Watch it when resuming sessions across projects — a mismatch there means your steering rules may be from the wrong config tree.

Build docs developers (and LLMs) love